LoginSignup
7
2

More than 5 years have passed since last update.

Play の BodyParser を組み合わせる

Posted at

Play では次のようにリクエストボディにアクセスします。

def save = Action { request: Request[AnyContent] =>
  val body: AnyContent = request.body
  // 

デフォルトでは上記のように request.bodyAnyContent 型です。
Content-Type ヘッダの値によって適切な型として parse されます。たとえば Content-Typeapplication/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 が出したエラーが出力となります。)

7
2
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
7
2