Tomcat をコンテナとした Servlet のコード上で Content-Length ヘッダを設定していたのですが、なぜか HTTP レスポンスのヘッダには Content-Length が出力されないという事象が確認されました。 これは一体なぜなのだろうと調べていると、当該レスポンスのヘッダに Transfer-Encoding: Chunked が出力されていることに気付きました。
Chunked は HTTP/1.1 で定義されている方式です。RFC 2068 には以下のような記述があり、Chunked と Content-Length を共存させてはいけない (MUST NOT) ことが分かります。)
Messages MUST NOT include both a Content-Length header field and the "chunked" transfer coding. If both are received, the Content-Length MUST be ignored.
同様に、HttpServletRequest の JavaDoc にも、その旨が明記されています。
HTTP 1.1 のチャンクドエンコーディングを使用する場合 (レスポンスに Transfer-Encoding ヘッダを持つことを意味します) は、Content-Length ヘッダを設定しないでください。
では、この Transfer-Encoding とはどのような方式なのか。 RFC では、HTTP の特徴として「メッセージボディのサイズを決定することが難しいこと」があり、メッセージボディをチャンク(Chunk) に分け、チャンク毎のサイズとともに送信することで、受信側でそのサイズを確認できるというようなことが書かれています。
チャンクに分けるといってもイマイチ分からないので、実際にチャンク通信を行う CGI *1を作ってみました。 作ってみましたと言っても、下記の通り 単なる Hello World な CGI ですが、チャンク通信が実際にどのようなものなのかが分かります。下記の CGI を実行すると、ブラウザには "6hello 5world0" ではなく、"hello world" が表示されます。
#!/usr/bin/perl print <<EOF; HTTP/1.1 200 OK Content-Type: text/html Transfer-Encoding: chunked 6 hello 5 world 0 EOF
上記の例では、"hello " と "world" をそれぞれ Chunk に分けて送信しています。個々のチャンクには chunk-size があり、これで各チャンクの内容が何オクテットなのかを示します。チャンク通信の最後は 0 のみからなる行と空行によって示します。
例えば、巨大なデータを動的に生成しクライアントに返却することを考えると、正確に Content-Length を生成するためにはそのデータを一旦バッファに貯めておくなどする必要が生じます。チャンク通信にすることで、もっと細かな単位で情報をクライアントに送信することができるようになるって感じでしょうか。
Reference
*1:無理矢理 HTTP/1.1 にしているので、NPH (No Parsed Header)の CGI として作っています