Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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 リクエストを特別に扱っていました。デフォルトの ResponseWriterWrite を呼び出すと ErrBodyNotAllowed が返され、ハンドラに何かがおかしいことを伝えていました。これは HEAD リクエストの高速パスを最適化するためでしたが、以下の理由から問題がありました。

  1. HEAD リクエストは非常に稀であるため、高速パスを持つ価値がありません。
  2. http.Handler が処理を行い、Write を何もしない(nop)ようにしても、非常に高速です。
  3. これにより、アプリケーションに醜いエラーハンドリングが強制されていました。 例: https://code.google.com/p/go/source/detail?r=6f596be7a31e および関連するコード。
  4. net/http パッケージは現在 Content-Type のスニッフィングを行いますが、HEAD リクエストではそれが得られませんでした。
  5. net/http パッケージは現在、小さな(数KBの)レスポンスに対して Content-Length のカウントを行いますが、HEAD リクエストでは行いませんでした。
  6. 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 リクエストではボディが送信されないため、ハンドラが誤ってボディを書き込もうとするのを防ぎ、かつ高速パスを提供することを意図していました。

この特殊な扱いは、以下の問題を引き起こしていました。

  1. 稀なユースケースへの過剰な最適化: コミットメッセージにもあるように、HEAD リクエストはウェブ上では非常に稀です。そのため、この稀なケースのために特別な高速パスを用意し、複雑なロジックを導入するメリットが薄れていました。
  2. ハンドラの複雑化: ErrBodyNotAllowed が返されるため、アプリケーションのハンドラは HEAD リクエストの場合に Write がエラーを返すことを考慮し、特別なエラーハンドリングロジックを記述する必要がありました。これはコードの可読性を損ない、開発者の負担を増やしていました。
  3. 機能の欠落: net/http パッケージは、GET リクエストに対しては自動的に Content-Type のスニッフィング(内容からMIMEタイプを推測する機能)や、小さなレスポンスに対する Content-Length の自動計算を行っていました。しかし、HEAD リクエストではこれらの機能が提供されていませんでした。これは、HEAD リクエストが GET と同じヘッダを返すというプロトコルの原則に反していました。
  4. ErrBodyNotAllowed の無意味さ: WriteErrBodyNotAllowed を返す頃には、ハンドラはすでにレスポンスボディを生成するための重い計算や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
    • クライアントはこれを見て、ボディの解釈方法を決定します。
  • Content-Length:
    • レスポンスボディのバイト単位のサイズを示します。
    • クライアントはこれを見て、ボディの受信が完了したかどうかを判断できます。
    • HEAD リクエストのレスポンスでは、ボディは送信されませんが、もし GET リクエストであれば送信されるであろうボディの Content-Length を示すべきです。
  • Transfer-Encoding:
    • メッセージボディに適用されたエンコーディング形式を示します。
    • 最も一般的なのは chunked で、これはボディのサイズが事前に不明な場合に、チャンク(塊)に分割して送信することを示します。Content-Length とは排他的です。

Goの net/http パッケージ

  • http.Handler インターフェース:
    • HTTPリクエストを処理するためのインターフェースで、ServeHTTP(w ResponseWriter, r *Request) メソッドを持ちます。
    • w はレスポンスを書き込むための ResponseWriterr は受信したリクエストを表します。
  • 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 が呼び出された場合に返していたエラーです。

技術的詳細

このコミットの技術的詳細は、主に net/http パッケージ内部での HEAD リクエストの処理ロジックの変更にあります。

以前のGoの net/http パッケージでは、HEAD リクエストが来ると、ResponseWriter の実装が Write メソッドに対して ErrBodyNotAllowed を返すように設計されていました。これは、ハンドラが HEAD リクエストに対して誤ってボディを書き込もうとするのを防ぐためのものでした。しかし、このアプローチは以下の点で非効率的かつ不便でした。

  1. エラーハンドリングの強制: ハンドラは Write の戻り値のエラーを常にチェックし、ErrBodyNotAllowed であれば特別な処理(通常は何もしない)を行う必要がありました。これにより、ハンドラのコードが複雑化しました。
  2. Content-Type スニッフィングの欠如: HEAD リクエストではボディが送信されないため、net/http パッケージは Content-Type の自動スニッフィングを行いませんでした。これは、HEAD レスポンスが GET レスポンスと同じヘッダを持つべきというHTTPの原則に反していました。ハンドラが明示的に Content-Type を設定しない限り、クライアントはリソースのタイプを知ることができませんでした。
  3. 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プロトコルの仕様に準拠した一貫性のある挙動が実現されます。ハンドラは GETHEAD の違いを意識することなく、同じロジックでレスポンスを生成できるようになります。ただし、ハンドラが HEAD リクエストに対して特別な高速パス(例えば、ボディを生成せずにヘッダだけを返す)を実装したい場合は、引き続き ResponseWriter.Header() を使ってヘッダを直接設定し、Write を呼び出さないようにする必要があります。

コアとなるコードの変更箇所

このコミットにおける主要なコード変更は、src/pkg/net/http/server.go ファイルに集中しています。テストファイル (serve_test.gotransport_test.go) も変更され、新しい挙動を検証しています。

src/pkg/net/http/server.go

  1. 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
    }
    
  2. 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.0Keep-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 {
    
  3. response.bodyAllowed メソッドの変更:

    • レスポンスボディが許可されるかどうかの判定から、w.req.Method != "HEAD" の条件が削除されます。これにより、HEAD リクエストでもボディが「許可される」と見なされるようになります(ただし、実際には送信されない)。
    // 変更前:
    // return w.status != StatusNotModified && w.req.Method != "HEAD"
    // 変更後:
    return w.status != StatusNotModified
    
  4. response.finishRequest メソッドの変更:

    • Content-Length と実際に書き込まれたバイト数が一致しない場合の接続クローズロジックから、w.req.Method != "HEAD" の条件が追加されます。これは、HEAD リクエストではボディが書き込まれないため、contentLengthwritten が一致しないのが正常な挙動であるためです。
    // 変更前:
    // 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.Writeio.CopyErrBodyNotAllowed を返すことを期待していましたが、新しいテストではエラーが返されないことを確認します。
    • Content-Typetext/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 リクエストを内部的にどのように扱うかを根本的に変更した点にあります。特に重要なのは、ResponseWriterWrite メソッドの挙動と、Content-Type および Content-Length の自動処理に関するロジックです。

src/pkg/net/http/server.go の変更点

  1. chunkWriter.Write メソッド:

    • このメソッドは、http.ResponseWriterWrite メソッドの実装の一部です。以前は、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 リクエストの場合はそれが自動的に無視されるため、コードの簡潔性が向上します。
  2. 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-Typetext/html; charset=utf-8 となっていること、そして Content-Length10 となっていることを検証します。これは、HEAD リクエストであっても Content-Type スニッフィングと Content-Length カウントが正しく機能していることを示します。

これらの変更により、Goの net/http パッケージは HEAD リクエストの処理において、よりHTTPプロトコルの仕様に忠実になり、開発者にとってより使いやすいAPIを提供することになります。ハンドラは GETHEAD の違いを意識することなく、同じロジックでレスポンスを生成できるようになり、コードの保守性が向上します。

関連リンク

参考にした情報源リンク