[インデックス 17069] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http
パッケージにおけるHTTP HEAD
リクエストの処理方法を変更するものです。具体的には、HEAD
リクエストを GET
リクエストと同様に扱うように修正し、これまでの特殊な処理によって生じていたいくつかの問題点を解消します。
コミット
commit ebe91d11051ac5e9ecf1bdacc1bcdfbe7bcbafa7
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Tue Aug 6 18:33:03 2013 -0700
net/http: treat HEAD requests like GET requests
A response to a HEAD request is supposed to look the same as a
response to a GET request, just without a body.
HEAD requests are incredibly rare in the wild.
The Go net/http package has so far treated HEAD requests
specially: a Write on our default ResponseWriter returned
ErrBodyNotAllowed, telling handlers that something was wrong.
This was to optimize the fast path for HEAD requests, but:
1) because HEAD requests are incredibly rare, they're not
worth having a fast path for.
2) Letting the http.Handler handle but do nop Writes is still
very fast.
3) this forces ugly error handling into the application.
e.g. https://code.google.com/p/go/source/detail?r=6f596be7a31e
and related.
4) The net/http package nowadays does Content-Type sniffing,
but you don't get that for HEAD.
5) The net/http package nowadays does Content-Length counting
for small (few KB) responses, but not for HEAD.
6) ErrBodyNotAllowed was useless. By the time you received it,
you had probably already done all your heavy computation
and I/O to calculate what to write.
So, this change makes HEAD requests like GET requests.
We now count content-length and sniff content-type for HEAD
requests. If you Write, it doesn't return an error.
If you want a fast-path in your code for HEAD, you have to do
it early and set all the response headers yourself. Just like
before. If you choose not to Write in HEAD requests, be sure
to set Content-Length if you know it. We won't write
"Content-Length: 0" because you might've just chosen to not
write (or you don't know your Content-Length in advance).
Fixes #5454
R=golang-dev, dsymonds
CC=golang-dev
https://golang.org/cl/12583043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ebe91d11051ac5e9ecf1bdacc1bcdfbe7bcbafa7
元コミット内容
net/http: treat HEAD requests like GET requests
HEAD
リクエストへのレスポンスは、ボディがないことを除けば GET
リクエストへのレスポンスと同じであるべきです。
HEAD
リクエストは実世界では非常に稀です。
Goの net/http
パッケージはこれまで HEAD
リクエストを特別に扱っていました。デフォルトの ResponseWriter
で Write
を呼び出すと ErrBodyNotAllowed
が返され、ハンドラに何かがおかしいことを伝えていました。これは HEAD
リクエストの高速パスを最適化するためでしたが、以下の理由から問題がありました。
HEAD
リクエストは非常に稀であるため、高速パスを持つ価値がありません。http.Handler
が処理を行い、Write
を何もしない(nop)ようにしても、非常に高速です。- これにより、アプリケーションに醜いエラーハンドリングが強制されていました。
例:
https://code.google.com/p/go/source/detail?r=6f596be7a31e
および関連するコード。 net/http
パッケージは現在Content-Type
のスニッフィングを行いますが、HEAD
リクエストではそれが得られませんでした。net/http
パッケージは現在、小さな(数KBの)レスポンスに対してContent-Length
のカウントを行いますが、HEAD
リクエストでは行いませんでした。ErrBodyNotAllowed
は役に立ちませんでした。それを受け取った時には、おそらくすでに重い計算やI/Oを終えて、書き込む内容を計算し終えていたでしょう。
したがって、この変更は HEAD
リクエストを GET
リクエストのように扱います。
これにより、HEAD
リクエストに対しても Content-Length
のカウントと Content-Type
のスニッフィングが行われるようになります。Write
を呼び出してもエラーは返されません。
もしコード内で HEAD
の高速パスが必要な場合は、以前と同様に、早期に処理を行い、すべてのレスポンスヘッダを自分で設定する必要があります。HEAD
リクエストで Write
を行わないことを選択した場合でも、Content-Length
が分かっていれば必ず設定してください。Content-Length: 0
は書き込まれません。なぜなら、単に書き込みを行わないことを選択しただけかもしれないからです(または Content-Length
が事前に分からない場合)。
Fixes #5454
変更の背景
このコミットの背景には、HTTPプロトコルにおける HEAD
メソッドの本来の意図と、Goの net/http
パッケージがこれまで採用していた特殊な処理方法との間の乖離がありました。
HTTP HEAD
メソッドは、GET
メソッドと全く同じヘッダを返すことを期待されていますが、レスポンスボディは含みません。これは、リソースのメタデータ(例: Content-Type
, Content-Length
, Last-Modified
など)を取得したいが、実際のコンテンツは不要な場合に効率的です。例えば、ファイルが更新されたかどうかを確認したり、ダウンロードする前にファイルサイズを知りたい場合などに利用されます。
しかし、Goの net/http
パッケージは、HEAD
リクエストに対して ResponseWriter.Write
メソッドが ErrBodyNotAllowed
エラーを返すという特殊な挙動をしていました。これは、HEAD
リクエストではボディが送信されないため、ハンドラが誤ってボディを書き込もうとするのを防ぎ、かつ高速パスを提供することを意図していました。
この特殊な扱いは、以下の問題を引き起こしていました。
- 稀なユースケースへの過剰な最適化: コミットメッセージにもあるように、
HEAD
リクエストはウェブ上では非常に稀です。そのため、この稀なケースのために特別な高速パスを用意し、複雑なロジックを導入するメリットが薄れていました。 - ハンドラの複雑化:
ErrBodyNotAllowed
が返されるため、アプリケーションのハンドラはHEAD
リクエストの場合にWrite
がエラーを返すことを考慮し、特別なエラーハンドリングロジックを記述する必要がありました。これはコードの可読性を損ない、開発者の負担を増やしていました。 - 機能の欠落:
net/http
パッケージは、GET
リクエストに対しては自動的にContent-Type
のスニッフィング(内容からMIMEタイプを推測する機能)や、小さなレスポンスに対するContent-Length
の自動計算を行っていました。しかし、HEAD
リクエストではこれらの機能が提供されていませんでした。これは、HEAD
リクエストがGET
と同じヘッダを返すというプロトコルの原則に反していました。 ErrBodyNotAllowed
の無意味さ:Write
がErrBodyNotAllowed
を返す頃には、ハンドラはすでにレスポンスボディを生成するための重い計算やI/Oを終えていることがほとんどでした。つまり、エラーを受け取っても手遅れであり、リソースの無駄遣いを防ぐ効果はほとんどありませんでした。
これらの問題点を解決し、net/http
パッケージの挙動をよりシンプルで、HTTPプロトコルの仕様に準拠したものにするために、この変更が提案されました。
前提知識の解説
このコミットを理解するためには、以下のHTTPプロトコルとGoの net/http
パッケージに関する基本的な知識が必要です。
HTTPメソッド: GETとHEAD
-
GETメソッド:
- 指定されたURIからリソースを取得するために使用されます。
- リクエストにはボディを含まず、レスポンスにはリソースのヘッダとボディの両方が含まれます。
- 冪等(何度実行しても結果が変わらない)かつ安全(リソースの状態を変更しない)なメソッドとされています。
-
HEADメソッド:
GET
メソッドと全く同じヘッダを返すことを期待されていますが、レスポンスボディは含みません。- リソースのメタデータ(例:
Content-Type
,Content-Length
,Last-Modified
,ETag
など)のみを取得したい場合に利用されます。 - 例えば、大きなファイルをダウンロードする前にそのサイズを確認したり、リソースが更新されたかどうかを最終更新日時ヘッダで確認したりする際に使われます。
GET
と同様に冪等かつ安全なメソッドです。
HTTPレスポンスヘッダ
- Content-Type:
- レスポンスボディのメディアタイプ(MIMEタイプ)を示します。例:
text/html; charset=utf-8
,application/json
。 - クライアントはこれを見て、ボディの解釈方法を決定します。
- レスポンスボディのメディアタイプ(MIMEタイプ)を示します。例:
- Content-Length:
- レスポンスボディのバイト単位のサイズを示します。
- クライアントはこれを見て、ボディの受信が完了したかどうかを判断できます。
HEAD
リクエストのレスポンスでは、ボディは送信されませんが、もしGET
リクエストであれば送信されるであろうボディのContent-Length
を示すべきです。
- Transfer-Encoding:
- メッセージボディに適用されたエンコーディング形式を示します。
- 最も一般的なのは
chunked
で、これはボディのサイズが事前に不明な場合に、チャンク(塊)に分割して送信することを示します。Content-Length
とは排他的です。
Goの net/http
パッケージ
http.Handler
インターフェース:- HTTPリクエストを処理するためのインターフェースで、
ServeHTTP(w ResponseWriter, r *Request)
メソッドを持ちます。 w
はレスポンスを書き込むためのResponseWriter
、r
は受信したリクエストを表します。
- HTTPリクエストを処理するためのインターフェースで、
http.ResponseWriter
インターフェース:- HTTPレスポンスを構築するためにハンドラが使用するインターフェースです。
Write([]byte) (int, error)
: レスポンスボディにデータを書き込みます。WriteHeader(statusCode int)
: HTTPステータスコードを書き込みます。Header() Header
: レスポンスヘッダを操作するためのHeader
マップを返します。
http.DetectContentType
:- バイトスライス(通常はレスポンスボディの最初の数バイト)の内容を調べて、そのMIMEタイプを推測するGoの関数です。
net/http
パッケージは、ハンドラがContent-Type
ヘッダを設定しなかった場合に、この関数を使って自動的にContent-Type
を設定しようとします(コンテンツスニッフィング)。
ErrBodyNotAllowed
:- Goの
net/http
パッケージが以前、HEAD
リクエストや304 Not Modified
のようなボディを許可しないレスポンスに対してWrite
が呼び出された場合に返していたエラーです。
- Goの
技術的詳細
このコミットの技術的詳細は、主に net/http
パッケージ内部での HEAD
リクエストの処理ロジックの変更にあります。
以前のGoの net/http
パッケージでは、HEAD
リクエストが来ると、ResponseWriter
の実装が Write
メソッドに対して ErrBodyNotAllowed
を返すように設計されていました。これは、ハンドラが HEAD
リクエストに対して誤ってボディを書き込もうとするのを防ぐためのものでした。しかし、このアプローチは以下の点で非効率的かつ不便でした。
- エラーハンドリングの強制: ハンドラは
Write
の戻り値のエラーを常にチェックし、ErrBodyNotAllowed
であれば特別な処理(通常は何もしない)を行う必要がありました。これにより、ハンドラのコードが複雑化しました。 Content-Type
スニッフィングの欠如:HEAD
リクエストではボディが送信されないため、net/http
パッケージはContent-Type
の自動スニッフィングを行いませんでした。これは、HEAD
レスポンスがGET
レスポンスと同じヘッダを持つべきというHTTPの原則に反していました。ハンドラが明示的にContent-Type
を設定しない限り、クライアントはリソースのタイプを知ることができませんでした。Content-Length
カウントの欠如: 同様に、HEAD
リクエストではContent-Length
の自動計算も行われませんでした。これは、クライアントがリソースのサイズを事前に知ることができないことを意味し、効率的な通信を妨げました。
このコミットでは、これらの問題を解決するために、HEAD
リクエストを GET
リクエストとほぼ同じように扱うように変更します。
Write
メソッドの挙動変更:HEAD
リクエストの場合でもResponseWriter.Write
はエラーを返さなくなります。代わりに、書き込まれたデータは内部的に「食べられる(eat)」、つまり破棄されます。これにより、ハンドラはGET
リクエストと同じロジックでWrite
を呼び出すことができ、エラーハンドリングの複雑さが解消されます。Content-Type
スニッフィングの有効化:HEAD
リクエストに対しても、GET
リクエストと同様にDetectContentType
を用いたContent-Type
の自動スニッフィングが行われるようになります。これにより、ハンドラが明示的にContent-Type
を設定しなくても、適切なヘッダが返される可能性が高まります。Content-Length
カウントの有効化:HEAD
リクエストに対しても、GET
リクエストと同様にContent-Length
の自動計算が行われるようになります。これにより、クライアントはHEAD
レスポンスからリソースのサイズを正確に知ることができます。
この変更により、HEAD
リクエストの処理がよりシンプルになり、HTTPプロトコルの仕様に準拠した一貫性のある挙動が実現されます。ハンドラは GET
と HEAD
の違いを意識することなく、同じロジックでレスポンスを生成できるようになります。ただし、ハンドラが HEAD
リクエストに対して特別な高速パス(例えば、ボディを生成せずにヘッダだけを返す)を実装したい場合は、引き続き ResponseWriter.Header()
を使ってヘッダを直接設定し、Write
を呼び出さないようにする必要があります。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は、src/pkg/net/http/server.go
ファイルに集中しています。テストファイル (serve_test.go
と transport_test.go
) も変更され、新しい挙動を検証しています。
src/pkg/net/http/server.go
-
chunkWriter.Write
メソッドの変更:- 以前は
HEAD
リクエストの場合にErrBodyNotAllowed
を返す可能性がありましたが、この変更により、HEAD
リクエストの場合は書き込まれたデータを単に破棄し、len(p), nil
を返すようになります。
// 変更前: // if cw.res.req.Method == "HEAD" { // return 0, ErrBodyNotAllowed // } // 変更後: if cw.res.req.Method == "HEAD" { // Eat writes. return len(p), nil }
- 以前は
-
chunkWriter.writeHeader
メソッドの変更:isHEAD
変数が導入され、HEAD
リクエストであるかどうかの判定が明確になります。Content-Length
の設定ロジックが変更され、HEAD
リクエストであってもlen(p) > 0
の場合にContent-Length
が設定されるようになります。これは、Write
が呼び出された場合にそのボディの長さがContent-Length
として反映されることを意味します。
// 変更前: // if w.handlerDone && header.get("Content-Length") == "" && w.req.Method != "HEAD" { // 変更後: if w.handlerDone && header.get("Content-Length") == "" && (!isHEAD || len(p) > 0) {
HTTP/1.0
のKeep-Alive
接続に関するロジックで、HEAD
リクエストの場合もContent-Length
が考慮されるようになります。
// 変更前: // if w.req.wantsHttp10KeepAlive() && (w.req.Method == "HEAD" || hasCL) { // 変更後: if w.req.wantsHttp10KeepAlive() && (isHEAD || hasCL) {
Content-Type
のスニッフィングがHEAD
リクエストでも行われるように条件が変更されます。
// 変更前: // if !haveType && w.req.Method != "HEAD" { // 変更後: if !haveType {
-
response.bodyAllowed
メソッドの変更:- レスポンスボディが許可されるかどうかの判定から、
w.req.Method != "HEAD"
の条件が削除されます。これにより、HEAD
リクエストでもボディが「許可される」と見なされるようになります(ただし、実際には送信されない)。
// 変更前: // return w.status != StatusNotModified && w.req.Method != "HEAD" // 変更後: return w.status != StatusNotModified
- レスポンスボディが許可されるかどうかの判定から、
-
response.finishRequest
メソッドの変更:Content-Length
と実際に書き込まれたバイト数が一致しない場合の接続クローズロジックから、w.req.Method != "HEAD"
の条件が追加されます。これは、HEAD
リクエストではボディが書き込まれないため、contentLength
とwritten
が一致しないのが正常な挙動であるためです。
// 変更前: // if w.contentLength != -1 && w.bodyAllowed() && w.contentLength != w.written { // 変更後: if w.req.Method != "HEAD" && w.contentLength != -1 && w.bodyAllowed() && w.contentLength != w.written {
src/pkg/net/http/serve_test.go
TestHeadResponses
テストが大幅に修正されます。- 以前は
ResponseWriter.Write
やio.Copy
がErrBodyNotAllowed
を返すことを期待していましたが、新しいテストではエラーが返されないことを確認します。 Content-Type
がtext/html; charset=utf-8
としてスニッフィングされること、およびContent-Length
が正しく10
とカウントされることを検証します。
- 以前は
src/pkg/net/http/transport_test.go
TestTransportHeadResponses
テストに、res.Body
を読み込んでもデータがないこと(len(all) != 0
)を確認するアサーションが追加されます。これは、HEAD
リクエストのレスポンスボディが空であることを保証するためです。
これらの変更により、net/http
パッケージは HEAD
リクエストを GET
リクエストとより一貫性のある方法で処理するようになり、ハンドラ側の複雑さを軽減し、HTTPプロトコルの仕様に準拠した挙動を実現します。
コアとなるコードの解説
このコミットの核心は、net/http
パッケージが HEAD
リクエストを内部的にどのように扱うかを根本的に変更した点にあります。特に重要なのは、ResponseWriter
の Write
メソッドの挙動と、Content-Type
および Content-Length
の自動処理に関するロジックです。
src/pkg/net/http/server.go
の変更点
-
chunkWriter.Write
メソッド:- このメソッドは、
http.ResponseWriter
のWrite
メソッドの実装の一部です。以前は、HEAD
リクエストの場合にErrBodyNotAllowed
を返していました。これは、ハンドラがHEAD
リクエストに対してボディを書き込もうとすると、その試みをエラーとして通知するためでした。 - 変更後:
この変更により、if cw.res.req.Method == "HEAD" { // Eat writes. return len(p), nil }
HEAD
リクエストの場合でもWrite
はエラーを返さなくなりました。代わりに、書き込まれたデータp
は単に破棄されます(// Eat writes.
コメントが示すように)。そして、書き込まれたバイト数len(p)
とnil
エラーが返されます。これにより、ハンドラはGET
リクエストと同じようにWrite
を呼び出すことができ、ErrBodyNotAllowed
のチェックと特別なエラーハンドリングが不要になります。ハンドラはボディを書き込むつもりでコードを書いても、HEAD
リクエストの場合はそれが自動的に無視されるため、コードの簡潔性が向上します。
- このメソッドは、
-
chunkWriter.writeHeader
メソッド:- このメソッドは、レスポンスヘッダが実際にクライアントに送信される直前に呼び出されます。
Content-Length
の自動計算:
以前はif w.handlerDone && header.get("Content-Length") == "" && (!isHEAD || len(p) > 0) { w.contentLength = int64(len(p)) setHeader.contentLength = strconv.AppendInt(cw.res.clenBuf[:0], int64(len(p)), 10) }
HEAD
リクエストの場合、Content-Length
の自動計算は行われませんでした。変更後、!isHEAD || len(p) > 0
という条件が追加されました。これは、「HEAD
リクエストではない場合」または「HEAD
リクエストだが、Write
が呼び出されてデータが渡された場合」にContent-Length
を設定するという意味です。これにより、HEAD
リクエストであっても、ハンドラがWrite
を呼び出した際に、そのボディの長さがContent-Length
ヘッダとして適切に設定されるようになります。これは、HEAD
レスポンスがGET
レスポンスと同じヘッダを持つべきというHTTPの原則に準拠するための重要な変更です。Content-Type
スニッフィング:
以前はif !haveType { setHeader.contentType = DetectContentType(p) }
HEAD
リクエストの場合、Content-Type
の自動スニッフィングは行われませんでした。変更後、w.req.Method != "HEAD"
という条件が削除され、Content-Type
ヘッダが明示的に設定されていない限り、DetectContentType
を使ってボディの内容からContent-Type
を推測し、設定するようになりました。これにより、HEAD
リクエストでも適切なContent-Type
ヘッダが返されるようになり、クライアントはリソースのタイプを正確に知ることができます。
src/pkg/net/http/serve_test.go
の変更点
TestHeadResponses
テストは、これらの内部的な変更が外部からどのように見えるかを検証します。- テストハンドラ内で
w.Write([]byte("<html>"))
やio.Copy(w, strings.NewReader("789a"))
を呼び出してもエラーが発生しないことを確認します。 - レスポンスヘッダから
Content-Type
がtext/html; charset=utf-8
となっていること、そしてContent-Length
が10
となっていることを検証します。これは、HEAD
リクエストであってもContent-Type
スニッフィングとContent-Length
カウントが正しく機能していることを示します。
- テストハンドラ内で
これらの変更により、Goの net/http
パッケージは HEAD
リクエストの処理において、よりHTTPプロトコルの仕様に忠実になり、開発者にとってより使いやすいAPIを提供することになります。ハンドラは GET
と HEAD
の違いを意識することなく、同じロジックでレスポンスを生成できるようになり、コードの保守性が向上します。
関連リンク
- Go Issue 5454:
net/http: HEAD requests should get Content-Type sniffing
- このコミットが修正したIssue - Go Code Review 12583043:
net/http: treat HEAD requests like GET requests
- このコミットのコードレビューページ
参考にした情報源リンク
- HTTP/1.1 Semantics and Content (RFC 7231):
HEAD
メソッドの定義 - Go
net/http
パッケージのドキュメント - Go
http.DetectContentType
のドキュメント - Go
http.ResponseWriter
のドキュメント - Go
http.ErrBodyNotAllowed
のドキュメント Content-Type
スニッフィングに関する一般的な情報 (MDN Web Docs)Content-Length
ヘッダに関する一般的な情報 (MDN Web Docs)