📏

Go: HTTPリクエストのContent-Lengthを正しくセットする方法

2022/12/26に公開

結論

  • そもそも、ほとんどの場合はよしなにやってくれるので、Content-Lengthを自分でセットしなければならない場面は非常にまれ
    • bodyの長さが「自明」であれば、その長さが自動的にセットされる
    • そうでなければ、自動的にTransfer-Encoding: chunkedで送信される
  • Content-Lengthを自分でセットするには、http.RequestContentLengthフィールドに値をセットするのが正解
// ✅
var size int64 = ...
req.ContentLength = size
  • Headerに直接Content-Lengthをセットしても無視される
// ❌
var size int64 = ...
req.Header.Set("Content-Length", strconv.FormatInt(size, 10))

ほとんどの場合は自分でセットしなくていい

GoでHTTPリクエストを行う処理を書く際に、Content-Lengthのことを気にする必要はほぼありません。これは、標準ライブラリのhttpパッケージが「いい感じ」にやってくれているおかげです。

http.RequestContentLengthフィールドを明示的にセットしなかった場合、bodyの性質に応じて以下のどちらかの動作をします。

bodyの長さが「自明」な場合

具体的にいうと、bodyの具体的な型が

  • *bytes.Buffer
  • *bytes.Reader
  • *strings.Reader

のうちのいずれかの場合です。これらの型の値は長さの情報を持っており、Len()メソッドで簡単に取得できます。

この場合は、http.NewRequest(WithContext)内でbody.Len()の値がContentLengthフィールドに自動的にセットされます(コード)。

bodyの長さが「自明」ではない場合

上記以外の場合が該当します。

この場合は、Transfer-Encoding: chunkedを利用し、リクエストボディを小分けにして送信するようになります。この方法には、リクエストボディ全体の長さ(= Content-Length)が事前に分からなくてもよいという特長があります。

まとめると、送信するデータの長さが事前に分かっているならそれが自動的に設定されるし、長さが事前に分からなければ「長さの情報を事前に送信しなくてもいい方法」でリクエストを行うようになっている、ということです。

自分でセットしないといけない場合

以上を踏まえると、Content-Lengthヘッダを自分でセットしなければならない状況というのは、bodyの長さが自明でなく、かつ事前(リクエストボディを送り始める前)にリクエストボディ全体の長さをサーバに送信しなければならない場合に限られます。

後半の条件に当てはまる場合、サーバは411 Length Requiredというエラーレスポンスを返すことになっています。筆者はS3のpresigned URLを使ってファイルをアップロードしようとした際にこのエラーレスポンスに遭遇しました。

Headerの罠

411エラーというのはめったにお目にかかるものではありませんが、Length Requiredというメッセージから「Content-Lengthヘッダをセットしてあげればよさそうだ」と推測できます。しかし、ここで焦って以下のように修正してしまうと、問題は思ったように解決してくれません。

var size int64 = ...
req.Header.Set("Content-Length", strconv.FormatInt(size, 10))
resp, err := httpCli.do(req)

なぜなら、http.RequestHeaderに直接セットされたContent-Lengthヘッダの内容は無視されてしまうからです。この場合は当該ヘッダをセットしなかったときと同じ挙動となるため、「Content-LengthをセットしたはずなのにLength Requiredと怒られる」という一見不可解な状況に陥ります。

焦らず、落ち着いて、http.RequestContentLengthフィールドをセットしましょう。これでContent-Lengthヘッダが意図通りに送信されます。

var size int64 = ...
req.ContentLength = size
resp, err := httpCli.do(req)

分かってしまえば「なんだそんなことか」という感じですが、めったに見ないエラーの解決の鍵が普段あまり意識していないところにあるとなると、意外と気づけないものです。

ちょっと深掘り

ドキュメントを読む

このContent-Lengthヘッダまわりの挙動については、httpパッケージのドキュメントコメントに断片的に記されています。

For client requests, certain headers such as Content-Length and Connection are automatically written when needed and values in Header may be ignored. See the documentation for the Request.Write method.

(抄訳)
クライアントリクエストにおいて、Content-LengthやConnectionといったヘッダは必要に応じて自動的に書き込まれ、Headerに設定した値が無視されることがある。Request.Writeメソッドのドキュメントを参照のこと。

This method consults the following fields of the request:

Host
URL
Method (defaults to "GET")
Header
ContentLength
TransferEncoding
Body

(抄訳)
このメソッドはリクエストのフィールドのうち以下のものを考慮する:

(略)

コードを読む

さらに、httpパッケージの関連コードを追うことで、実際に送信されるContent-Lengthヘッダは基本的にhttp.Request.ContentLengthフィールドの値のみに基づいて決まり、Headerに直接セットされた値は完全に無視されることがわかります。

Content-Lengthヘッダを書き込む処理の概要

該当箇所: http.Request.writeメソッド(Writeの内部処理、コード)

  1. newTransferWriter()で、transferWriterを生成(コード)。これはRequestの内容をHTTPプロトコルに則って書き込むメソッドを持つ。
    • ここで、Content-Lengthヘッダの値に対応するtransferWriter.ContentLengthの値はRequest.ContentLengthのみを考慮して設定される(コード)
  2. transferWriter.writeHeaderメソッドでContent-Lengthヘッダを実際に書き込む(コード)
  3. Header.writeSubsetメソッドでtransferWriterが関知しないヘッダを書き込む(コード)が、このときHeaderにセットされたContent-Lengthは無視される(reqWriteExcludeHeader(コード)に含まれるため)

実験してみる

様々な設定のもとでHTTPリクエストを送信して内容を観察・比較することで、HTTPリクエスト関連の挙動に対する理解を深めることができました。実験プログラムをGitHubに置いておいたので、興味のある方はぜひ動かしてみてください。

https://github.com/jiftechnify/go-httpcli-req-observation

おわりに

GoでHTTPリクエストを行う際にContent-Lengthヘッダを正しく設定する方法、あるいはhttp.Request.HeaderにセットしたContent-Lengthが無視されるという罠についてまとめました。

読者の方々が同様の問題に遭遇した際に、冷静に対処する助けになれば幸いです。

参考文献

GitHubで編集を提案

Discussion