仕様はよく読むもんだ

ServletResponseに対してGZipOutputStreamによる圧縮を施しているのだが、RequestDispatcher#includeを実行すると圧縮したストリームの内容が壊れるところまでは判明した。

前エントリをきっかけにJava Servlet Specification Version 2.4を読み直した所、ピコンと閃き3日ごしのトラブルの原因が判明した。
問題になっているコンテンツの圧縮処理はサーブレットフィルタによりServletOutputStreamをGZipOutputStreamで置き換えることで実装しているのだが(よくあるパターンだ)、該当フィルタのweb.xmlにおけるフィルタマッピングの定義が原因だった。


    GZipFilter
    *.jsp

この記述、ServletAPI2.3までは何も問題がないが、ServletAPI2.4準拠のコンテナでは問題が発生する"可能性"がある。ServletAPI2.4の仕様ではフィルタの適用範囲は、リクエスト処理だけからリクエストディスパッチャが及ぶ範囲まで広げられているからだ。

サーブレットフィルタの範囲の拡張に関しては以下のリンクが非常に解り易い。
Tomcatを使いやすくする: Tomcat 5のフィルター処理の技 - developerWorks > Java technology >


リンク中にもあるが、サブエレメントで指定できる値は以下の4種類。

  • REQUEST
    • クライアントから発信されたリクエストにフィルターをかける (Servlet 2.3準拠のコンテナーのデフォルトの動作とまったく同じ)。
  • FORWARD
    • リクエストがWebリソース間でDispatcher.forward() 呼び出しを使って転送される際に、そのリクエストにフィルターをかける。
  • INCLUDE
    • WebリソースがDispatcher.include() 呼び出しを使ってインクルードされる際にリクエストにフィルターをかける
  • ERROR
    • エラー処理リソース (基本的には、エラーが発生したときにコンテナーが管理する転送メカニズム) にフィルターをかける。

問題なのは"REQUEST"の指定を行うことにより、初めてServletAPI2.3の振舞いになるという仕様なのであり、エレメントを指定しないままServletAPI2.4で使用した場合は、コンテナの実装にもよるが意図しない箇所でフィルタが適用されてしまうことになる。

つまり今回の問題をJSPの処理を例に取って書くとWeblogic9.2jの場合は

JSPをリクエスト → {GZipFilter} → JSPWriterによる出力 → RequestDispatcher#include → {GZipFilter}

とGZipFilterの適用が重複していたのである。当然だがストリームに対してGZip圧縮が重複すればブラウザ側でデータの伸張が上手く行かないのは当たり前だ。
ServletAPI2.4に準拠したWeblogic9.2jの場合、リクエストにだけフィルタを適用するには、以下のように明示的にフィルタマッピングを書く必要がある。


    GZipFilter
    *.jsp
    REQUEST

これでGZipFilterは内部で動的にincludeされたコンテンツに対しては重複して適用されない。

SevletAPI2.4でフィルタの定義が変更されたのは知っていたが、このように振舞いまで影響が出るとは予想だにしなかった。

しかし、解せないのは同じくServletAPI2.4に準拠しているTomcat6.0では全く問題が発生しなかったことである。(これが逆に原因の判明が遅れた原因だった)この辺はJSRに追加されたSRV.6.2.5にも記述はあるが、フィルタ適用の範囲を指定した場合の振舞いは規定しているが、指定しなかった場合の振舞いを規定していないぽい。
一つだけはっきりしているのは、またここでベンダによる実装の違いが発生しているということだ。
ここからは想像だが、Tomcat6.0は過去のweb.xmlとの互換性を重視しており、範囲を何も指定しない場合は"REQUEST"を指定した場合と同様の振舞いとしたのだろう。