何が起きているのか?
Spring Bootでログインフィルターや権限チェックを構築している場面を想像してみてください。すべてが完璧に見えますが、コンソールに java.lang.IllegalStateException が出力されて爆発します。メッセージは明確です。ユーザーをリダイレクトしようとしましたが、レスポンスはすでに「コミット(確定)」されています。
HTTPレスポンスを物理的な手紙に例えてみましょう。一度ポストに投函(バッファがフラッシュ)してしまうと、後から住所を書き換えたり、新しい切手を貼ったりすることはできません。Java Webの用語で言えば、サーバーがHTTPヘッダーや最初のデータチャンクをクライアントに送信した時点で、レスポンスの状態はロックされます。その後はステータスコードを変更したり、リダイレクトを実行したりすることはできなくなります。
なぜこれが起こるのか
HTTPのライフサイクルは厳格です。データは一方向に流れます。通常、このエラーは以下の4つの技術的なミスが原因で発生します。
- 「ゴースト」実行:
sendRedirect()を呼び出した後もコードの実行が続き、メソッドの後半でさらにデータを書き込もうとした。 - バッファオーバーフロー: Apache Tomcatなどのほとんどのサーバーは、デフォルトのバッファサイズが 8,192バイト(8KB) です。コードが9KBのデータを書き込むと、サーバーは自動的にバッファをフラッシュし、レスポンスをコミットします。
- フィルターの二重処理: セキュリティフィルターがエラーを処理してユーザーをリダイレクトしたにもかかわらず、誤って
chain.doFilter()を呼び出し、リクエストを次のコンポーネントに渡してしまった。 - 手動フラッシュ:
response.flushBuffer()を呼び出したか、PrintWriterを時期尚早にクローズした。
解決方法
1. 必須の 'return' 文
これは最も頻繁に見られる間違いです。多くの開発者は sendRedirect() が return 文のように機能すると誤解していますが、そうではありません。これは単にヘッダーを設定するだけです。手動で停止しない限り、メソッドの残りの部分は実行され続けます。
間違った方法:
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
if (session.getAttribute("user") == null) {
response.sendRedirect("/login");
}
// これも実行されます!コミット済みのレスポンスに書き込もうとします。
response.getWriter().write("Welcome to the dashboard");
}
正しい方法:
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
if (session.getAttribute("user") == null) {
response.sendRedirect("/login");
return; // すぐにメソッドを終了する
}
response.getWriter().write("Welcome to the dashboard");
}
2. フィルターロジックの整理
認証に Filter を使用している場合、すでに処理を終えたリクエストをチェーンの先に渡さないようにしてください。エラーやリダイレクトを送信した時点で、チェーンはその場所で終了させる必要があります。
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
HttpServletResponse res = (HttpServletResponse) response;
if (isTokenInvalid(request)) {
res.sendRedirect("/login");
return; // ここで停止! chain.doFilter() を呼び出さないでください
}
chain.doFilter(request, response);
}
3. 安全策として isCommitted() を使用する
複数のユーティリティがレスポンスを操作する可能性がある複雑なアプリケーションでは、isCommitted() メソッドを使用してください。これは、完了したレスポンスを修正しようとしてアプリがクラッシュするのを防ぐための、単純なブーリアンチェックです。
if (!response.isCommitted()) {
response.sendRedirect(targetUrl);
} else {
log.warn("Cannot redirect to {} because the response is already out the door.", targetUrl);
}
4. Spring Boot と @ControllerAdvice
手動での HttpServletResponse への書き込みと、グローバル例外ハンドラーを併用する場合は注意が必要です。レスポンスの一部を書き込んだ後に例外をスローすると、Springの BasicErrorController が500エラーページを送信しようとします。すでに最初のレスポンスの書き込みを開始しているため、この衝突によって IllegalStateException が発生します。
修正を確認する方法
- ネットワークタブを確認する: ブラウザの開発者ツール(F12)を開きます。同じURLに対して 302 Found の直後に 500 Internal Server Error が表示されている場合、ロジックに漏れがあります。
- スタックトレースを追う: スタックトレースは通常、レスポンスを修正しようとした「2回目」の試行を指し示しています。コード内でその10〜20行上を確認し、実際にレスポンスをコミットさせた「1回目」の修正箇所を見つけてください。
- バッファの使用状況を確認する: 大きなページでエラーが発生する場合は、ループ内で
out.flush()を呼び出していないか確認してください。
まとめ
Response already committed エラーは、本質的にはHTTPプロトコルにおける交通違反のようなものです。修正するには、サーバーにリダイレクトやレスポンスの終了を指示した後、そのリクエストパスにおけるそれ以降の実行をすべて停止させるようにしてください。多くの場合、単純な return; だけで十分です。

