Help us understand the problem. What is going on with this article?

Play の BodyParser を組み合わせる

More than 1 year has passed since last update.

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 勉強会)。できたのが↓です。

https://gist.github.com/tkawachi/9f89782d665d37b21bc983863dc03e35

というメソッドを生やしたので 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 が出したエラーが出力となります。)

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away