[インデックス 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)