Play では次のようにリクエストボディにアクセスします。
def save = Action { request: Request[AnyContent] =>
val body: AnyContent = request.body
//
デフォルトでは上記のように request.body
は AnyContent
型です。
Content-Type
ヘッダの値によって適切な型として parse されます。たとえば Content-Type
が application/json
であれば JSON として parse され、request.body.asJson
で JSON が取り出せます。
今回はパースされたボディと共に、生のリクエストボディにアクセスしたいと思いました。
Play では BodyParser[A]
が HTTP リクエストボディを解釈します。
その定義 を見るとわかるように RequestHeader => Accumulator[ByteString, Either[Result, A]]
を継承しています。
trait BodyParser[+A] extends (RequestHeader => Accumulator[ByteString, Either[Result, A]]) {
Accumulator
は akka-stream の Sink
のようなもので、ここではリクエストボディのバイト列(いくつかに分かれているかもしれない)を受け取って最終的に Either[Result, A]
を返します。
最初に考えたのは、生のボディを渡してくれる BodyParser を使っておいて、 Action の中で ByteString
から目的の型へパースする方法。しかし、ByteString から目的の型へパースするのは BodyParser の役割そのものであり、そこは BodyParser を使いまわしたいと思い直しました。
つぎに考えたのは BodyParser[A] => BodyParser[(A, ByteString)]
という関数をつくる方法。これはこれで動きそうなのですが、ByteString はメモリ上に乗ることになるので、リクエストボディが長いときに使ってしまうとメモリがあふれる危険があります。
リクエストボディが長いときにはディスクに書いてほしいものです。まさにそれをしてくれるのが raw BodyParser です。
任意の BodyParser と raw BodyParser を組み合わせるとやりたいことが実現できそうです。
実際には任意の BodyParser 2つを組み合わせる形で実装してみました(at #rpscala 勉強会)。できたのが↓です。
と
というメソッドを生やしたので bodyParsers.json と bodyParsers.raw
といった形で BodyParser を合成できます。JSON としてパースつつ、生のリクエストボディにアクセスできます。
実装上のポイントとして Broadcast[ByteString](2, eagerCancel = false)
で eagerCancel
を false にしている点が挙げられます。これは2つの BodyParser にそれぞれ生のリクエストボディを流し込む中継ポイントです。eagerCancel を true にした場合、どちらかの BodyParser がエラーとなった場合(たとえば JSON を期待する BodyParser が JSON ではない文字列を読み込んだ場合)に、即座に中継ポイントがキャンセルされ、もう一方の BodyParser もその時点で終了します。効率は良いのですが、結果の合成の結果、エラーを起こした方ではない BodyParser の出力結果が優先されることがあります。そうなると直感的ではないエラーがリクエスト元に返されることになるため、ここでは eagerCancel を false にしています。一方がエラーになってももう一方はパース処理を続けるため、効率は悪いのですがエラーがわかりにくいという問題は起きません。(両方の BodyParser がエラーになった場合には最初の BodyParser が出したエラーが出力となります。)