2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Transfer-Encoding: chunked を自分でヘッダーに追加するのは基本的にNG

2
Posted at

結論

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 登録方法

  1. フィルターのクラス
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);
   }
}
  1. @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 を使ってヘッダーの追加を完全にブロック(非推奨)

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?