playframeworkのdeferBodyParsing

おそらく、あまり知られていないというか、雑にググっても日本語で書かれた情報が引っかからないのですが、自分も少し前まで知らなかったのですが、諸事情により調査する機会があったので解説のようなものを書いておきます。

とはいえ、大まかには公式ドキュメント見てもらえばいいわけですが・・・。

playframework 2.8以前には存在せず、2.9や3.0から入った機能です。

名前の通り

「bodyのparseを遅延させる」

という機能です。遅延させるだけなら2.8以前でも他の方法で遅延させることは可能なのですが、これは全体にまとめて適用可能です。

(これも公式ドキュメントに書いてありますが) 遅延させたい時の具体的なイメージとしては

  • bodyがjsonなどでparse処理必要
    • jsonではなくてもなんでもいいが・・・そもそもbody丸ごとバイナリかtextとしてどこかに保存するようなものでない限り、parse的な処理は存在する方が普通な気がする
  • かつbodyがある程度の大きさになる場合がある
  • あるいはbodyの大きさはあまり関係なく、セキュリティ頑張る場合に、できるだけ余計な処理をしたくない、など
  • 認証認可などの何かの処理が最初に来る
  • その「認証認可などの何かの処理」は、headerあるいはrequest parameterなどで判断可能であり、その処理にbodyは必要ない

という場合に、現状のplayframeworkでは何も考えずにjson用のbody parserを渡すコードを書くと

「認証認可などの何かの処理」

をする前に

jsonのbodyのparse」

が実行されてしまうわけですが、先に実行されるのは無駄になる場合がありますよね???

という話です。 ただ

「それなら基本的に有効に設定した方がいいのでは???むしろそれがデフォルトの挙動になっていてくれよ!」

と思うかもしれませんが、そうなってない理由というか

「基本的に有効に設定」

するには現状のplayframeworkでは、超重大な問題があります。それは

  • 設定で有効にしたら、それ以外何もせずに恩恵を受けれるわけではなく、controllerのAction記述部分の処理を絶対に書き換えないといけない
  • 具体的には、今までbodyが渡ってくる部分で単に null がかえるようになるので、もう一回明示的にparseするような処理を書く必要がある
  • その場合の記述がダサいというか、for式で全部書けるわけでもなくて辛い(?)
  • 「今までbodyが渡ってくる部分で単に null がかえるようになる」は、そもそも型安全ではないというか、テストしっかり書かないと、忘れて事故る可能性がある
  • しかも、テストの書き方などによっては「deferBodyParsingが有効の状態でテストされるのか?無効の状態でテストされるのか?」がしっかり理解しないとわかりにくいため、そこ理解せずにテスト書いて対策したつもりでも、結局事故る可能性がある

などです。

というわけで、現状の自分が調べた感覚としては、(これ以降に書く代案もあるので)、上記の問題のある程度が解決するようないい案がない限り、基本的に全く使用をオススメしません。

実は(この後の代案と比較して)何かデメリットを上回るメリットの存在を知ってる人がいたら教えてください。

playframeworkをJavaで使ってる場合は便利な可能性があるのかも・・・という気がしますが、特に調べてないし、調べる気もないし、Java版に全く詳しくないので割愛。

さて

「これ以降に書く代案」

ですが、以下の using (あるいはwhen?)というメソッドなどを使えば、おそらくほぼ同等のことが可能です。

別の細かいデメリットが発生する可能性はなくはないですが、こちらはおそらく型安全になってnullの心配をする必要はなくなるはずです。

for式でまとめて書けないあたりのデメリットは同じですが…

あるいは少しパフォーマンス劣るけど、そもそも明示的に raw のparserを使えば、余計な中身のparseは避けることも可能です。

rawだと、中身のparseを避けることは可能だけど、内容が一時的とはいえメモリ上かtmp fileに保存はされてしまうので、単にraw使うのはその点が微妙ですが。

さて、ここから上記の using 使う場合の具体的なコードやパフォーマンスの話をしたいのですが、現状ではその場合のベストプラクティスが結局よくわかってない、という問題があります。

usingの引数は

RequestHeader => BodyParser[A]

となっているので、大まかなイメージとしては以下のように使えば良いです

val myBodyParser = bodyParsers.using { header =>
  if (header使った認証認可などの処理) {
    // 成功した場合
    bodyParsers.json    
  } else {
    // 失敗して、bodyをparseしたくない場合
    // ここにbodyを全くparseしないparser
  }
}

// この独自body parserをActionに渡す

val hello = Action.async(myBodyParser) { implicit request =>

}

ここにおいて、自分が実験した限り

「bodyを全くparseしないparser」

に具体的に何をおくべきなのか?が難しいです。

playframework公式で、以下のように、empty、ignore、errorなどがありますが

https://github.com/playframework/playframework/blob/561f4921e5da477658b8e6b34bd48292b42ac9fe/core/play/src/main/scala/play/api/mvc/BodyParsers.scala#L332-L343

実験した限り、これはものによっては、request bodyが大きい場合、そもそも

「request送信途中にresponse返し始める」

という挙動になり、server側で実行時に警告が出ます。

では

「bodyは全部読むのだけど、ひたすら読みつつ単に捨てるbody parserが必要か?」

と思ったので、独自に書いてみた場合は、例えばこうです

 val myEmptyParser: BodyParser[Unit] = BodyParser("my-body-parser") { request =>
    Accumulator
      .strict[ByteString, Unit](
        { maybeStrictBytes =>
          Future.successful(
            ()
          )
        }, {
          Sink.fold[Unit, ByteString](()) { (bf, bs) => }
        }
      )
      .map(Right.apply)
  }

これを使うと

「request送信途中にresponse返し始める」

という件は解決しますし、ベンチマークとった限りこれでもrawのbody parserよりは2倍くらい効率がいいのですが、果たしてこれがベストなのか?がわかりません。

serverで警告が出るというか、serverの状態が変になるのはまずいですが、

client側が悪意あって攻撃してる場合すらも想定するなら、出来るだけ速やかに接続を無理やり切って、bodyのparseは一切しないような挙動の方が望ましいのかもしれませんが、

そういう挙動と、serverでもclientでも変なエラーにならずに任意のstatus codeをしっかり返すような挙動にするべきか?などを色々考慮したり使い分ける場合に、結局どういう実装にするのがベストなのか???という。

また、あまり理解せずに中途半端な知識で独自のbody parserを書くと、bodyが大きすぎる攻撃などに対して、逆に脆弱になる可能性すらあり得るかもしれません。

rawのbody parserよりは効率がいいなら、少なくとも上記の

「最後まで全部読むけど捨てる」

でいいのかもしれませんが、それ以上さらに頑張る場合のいい感じの方法を知っていたら募集しています

or

理解したらさらに後で書くかもしれません。

いずれにせよ「それ以上さらに頑張る」場合、playframeworkやpekko-httpやpekko-streamの内部構造を知ってそこを触る必要がある可能性や、 (すでに上記でも少し触ってますが)

そもそもhttpのprotocol自体の問題として

「request送信途中にresponse返し始める」

はアリなのか?そういう場合というか、無理やり接続切りたいような場合のベストプラクティスは???

といった論点に発展して、思ったより面倒というか奥が深い気がしました。