結論
Transfer-Encoding: chunked にしたい
Content-Length を指定しなければ Tomcat が自動でやる
Transfer-Encoding を自分で書く
❌ 推奨されない(Tomcat が重複設定してしまう)
chunked 通信を制御したい
StreamingResponseBody, OutputStream, InputStreamResource などを使う
原因:Transfer-Encoding を自分で設定すると、Tomcatが重複設定する
Spring MVC や ResponseEntity のヘッダー設定が Tomcat の低レベルの挙動と競合した場合に発生する現象です。
Transfer-Encoding: chunked を自分でヘッダーに追加するのは基本的にNGです。
Spring MVC で ResponseEntity を使って以下のように書いた場合:
HttpHeaders headers = new HttpHeaders();
headers.set("Transfer-Encoding", "chunked"); // ❌ 自分で設定
return new ResponseEntity<>(data, headers, HttpStatus.OK);
このようにすると:
あなたが Transfer-Encoding: chunked を明示的にヘッダーに入れる
Spring から Tomcat に書き込みが渡るとき、Tomcat が「ボディ長が不明だ」と判断して もう一度 Transfer-Encoding: chunked を追加
結果:重複して2回 Transfer-Encoding: chunked が出力される
クライアントによってはレスポンス解析に失敗する
正しいやり方:SpringやTomcatに任せる
Transfer-Encoding: chunked は、自分で書かず、**ボディのサイズを指定しない(Content-Length を設定しない)**だけにします。
Tomcat が自動的に chunked に切り替えます。
例:Content-Length を外して chunked を有効にする(正しいやり方)
@GetMapping("/chunked")
public void writeChunked(HttpServletResponse response) throws IOException {
response.setContentType("application/octet-stream");
// Content-Length を設定しない ⇒ Tomcat が Transfer-Encoding: chunked に自動設定
ServletOutputStream out = response.getOutputStream();
for (int i = 0; i < 5; i++) {
out.write(("chunk " + i + "\n").getBytes(StandardCharsets.UTF_8));
out.flush();
Thread.sleep(500); // 遅延を入れて擬似的なチャンク送信
}
out.close();
}
のようにすれば:
Content-Length がない
Spring も Transfer-Encoding を指定しない
Tomcat が HTTP/1.1 のルールに従って 自動で Transfer-Encoding: chunked を1つだけ追加
補足:ResponseEntity で chunked を自然に使うには
以下のように InputStreamResource や StreamingResponseBody を使うと、自動で chunked になります:
@GetMapping("/stream")
public ResponseEntity<StreamingResponseBody> stream() {
StreamingResponseBody body = outputStream -> {
for (int i = 0; i < 10; i++) {
outputStream.write(("chunk " + i + "\n").getBytes(StandardCharsets.UTF_8));
outputStream.flush();
Thread.sleep(500);
}
};
return ResponseEntity.ok()
.contentType(MediaType.TEXT_PLAIN)
.body(body); // Content-Length 不明 ⇒ 自動で chunked
}
Servlet Filter で Transfer-Encoding: chunked ヘッダーを削除
Servlet Filter の doFilter の中で、次のようにヘッダーを削除することはできます:
public class ChunkedHeaderRemovingFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletResponse httpResp = (HttpServletResponse) response;
HttpServletResponseWrapper responseWrapper = new HttpServletResponseWrapper(httpResp) {
@Override
public void setHeader(String name, String value) {
if (!"Transfer-Encoding".equalsIgnoreCase(name)) {
super.setHeader(name, value);
}
}
@Override
public void addHeader(String name, String value) {
if (!"Transfer-Encoding".equalsIgnoreCase(name)) {
super.addHeader(name, value);
}
}
};
chain.doFilter(request, responseWrapper);
}
}
XML Config に Filter を登録する場合
<filter>
<filter-name>chunkedHeaderRemovingFilter</filter-name>
<filter-class>your.package.ChunkedHeaderRemovingFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>chunkedHeaderRemovingFilter</filter-name>
<url-pattern>/*</url-pattern> <!-- 必要に応じて対象範囲を制限 -->
</filter-mapping>
Java Config での Filter 登録方法
- フィルターのクラス
package your.package;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.IOException;
public class ChunkedHeaderRemovingFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletResponse httpResp = (HttpServletResponse) response;
HttpServletResponseWrapper wrapper = new HttpServletResponseWrapper(httpResp) {
@Override
public void setHeader(String name, String value) {
if (!"Transfer-Encoding".equalsIgnoreCase(name)) {
super.setHeader(name, value);
}
}
@Override
public void addHeader(String name, String value) {
if (!"Transfer-Encoding".equalsIgnoreCase(name)) {
super.addHeader(name, value);
}
}
};
chain.doFilter(request, wrapper);
}
}
-
@Configurationによる登録
package your.package;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<ChunkedHeaderRemovingFilter> chunkedHeaderRemovingFilter() {
FilterRegistrationBean<ChunkedHeaderRemovingFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new ChunkedHeaderRemovingFilter());
registration.addUrlPatterns("/*"); // 必要に応じて絞り込む
registration.setOrder(1); // 優先順位。小さいほど先に実行される
return registration;
}
}
しかし、レスポンスのボディ書き込み後に Tomcat が自動的に Transfer-Encoding を追加する
Tomcat(や他の Servlet コンテナ)は次のように動作します:
レスポンスの Content-Length が 明示されていない
HTTP プロトコルが 1.1 以降
ストリームやチャンク形式で出力されている(StreamingResponseBody など)
この条件に合致すると、Tomcat は あなたのFilterが終わった後に Transfer-Encoding: chunked を自動で追加します。
つまり、Filter で削除しても最終レスポンスの段階では復活している可能性が非常に高いです。
どうすればTransfer-Encoding を完全に削除できるか?
Tomcatが Transfer-Encoding を追加しないように設計するしかない
Content-Length を明示的に設定する(→ 固定長応答にする)
チャンクが不要なら StreamingResponseBody や OutputStream で逐次出力しない
Tomcat のカスタム Response Wrapper を使ってヘッダーの追加を完全にブロック(非推奨)