§ボディパーサー
§ボディパーサーの概要
HTTP PUT や POST リクエストはボディを含みます。このボディは Content-Type
リクエストヘッダで指定さえしておけば、どんなフォーマットであっても構いません。Play では ボディパーサー がリクエストボディを Scala の値に変換します。
しかし、HTTP リクエストのリクエストボディはとても大きなサイズになる可能性があり、 ボディパーサー が全てのデータセットがメモリにロードされるのを単純に待ってからパースを行うというのは現実的ではありません。BodyParser[A]
は基本的に Iteratee[Array[Byte],A]
です。これは、ボディパーサーはバイトデータの塊を(webブラウザがデータをアップロードし続ける限り)入力として受け取り、結果として A
型の値を計算する、ということを意味します。
いくつか例を見てみましょう。
- text ボディパーサーはバイトデータの塊を String に積み上げていき、計算された String を結果として返します(
Iteratee[Array[Byte],String]
)。 - file ボディパーサーはバイトデータの塊をローカルファイルに保存し、
java.io.File
への参照を結果として返します(Iteratee[Array[Byte],File]
)。 - s3 ボディパーサーはバイトデータの塊を Amazon S3 へ保存し、S3 オブジェクトの ID を結果として返します(
Iteratee[Array[Byte],S3ObjectId]
)。
これらに加えて、 ボディパーサー はリクエストボディのパースを始める前に HTTP リクエストヘッダを参照して、いくつか事前条件のチェックをすることがあります。例えば、特定の HTTP ヘッダが正しくセットされていることをチェックしたり、ユーザが大きなファイルをアップロードしようとしたときに本当にその権限を持っているのかチェックする、というようなボディーパーサーが考えられます。
ノート: これがボディパーサーが厳密には
Iteratee[Array[Byte],A]
ではなくIteratee[Array[Byte],Either[Result,A]]
であることの理由です。つまり、リクエストボディを元に適切な値を計算できないと判断した場合、ボディパーサー自身が直接的に HTTP レスポンスを送信することがあります(よくあるのは400 BAD_REQUEST
,412 PRECONDITION_FAILED
,413 REQUEST_ENTITY_TOO_LARGE
です)。
ボディパーサーは処理を終えると即座に A
型の値を返却し、その後に対応する Action
関数が実行され、計算されたボディの値がリクエストに渡されます。
§アクションについての詳細
以前、Action
は Request => Result
型の関数だと説明しました。しかし、これは厳密には正しくありません。Action
トレイトをより正確に見てみましょう。
trait Action[A] extends (Request[A] => Result) {
def parser: BodyParser[A]
}
まず、ジェネリック型 A
が存在すること、そしてアクションは BodyParser[A]
を定義しなければならないことがわかります。Request[A]
は次のように定義されています。
trait Request[+A] extends RequestHeader {
def body: A
}
A
はリクエストボディの型です。例えば、String
, NodeSeq
, Array[Byte]
, JsonValue
, java.io.File
など、その型を処理できるボディパーサーが定義されていれてさえいれば、あらゆる Scala の型をリクエストボディの型として指定できます。
まとめると、Action[A]
は BodyParser[A]
であり、HTTP リクエストから A
型の値を受け取り、アクションのコードに渡される Request[A]
型のオブジェクトを組み立てます。
§デフォルトのボディパーサー: AnyContent
前の例では、ボディパーサーを明示的に指定していませんでした。一体なぜ動いたのでしょうか? 実は、ボディパーサーを自分で指定しなかった場合、Play はボディを play.api.mvc.AnyContent
として処理するデフォルトのボディパーサーを使います。
このボディパーサーは Content-Type
ヘッダの内容に応じて、ボディを何として処理すべきかを決定します。
- text/plain:
String
- application/json:
JsValue
- text/xml:
NodeSeq
- application/form-url-encoded:
Map[String, Seq[String]]
- multipart/form-data:
MultipartFormData[TemporaryFile]
- その他の Content-Type:
RawBuffer
例えば、以下のように利用します。
def save = Action { request =>
val body: AnyContent = request.body
val textBody: Option[String] = body.asText
// Expecting text body
textBody.map { text =>
Ok("Got: " + text)
}.getOrElse {
BadRequest("Expecting text/plain request body")
}
}
§ボディパーサーの指定
Play で利用できるボディパーサーは play.api.mvc.BodyParsers.parse
に定義されています。
例えば、(前の例と同様に) テキストのボディを受け取るようなアクションは次のように定義します。
def save = Action(parse.text) { request =>
Ok("Got: " + request.body)
}
コードがどれくらいシンプルになったかお分かりでしょうか? この理由は、parse.text
ボディパーサーが何か問題を見つけた時に 400 BAD_REQUEST
レスポンスを返してくれるからです。自分のコードで再度チェックする必要がなく、request.body
が間違いなく String
型のボディであることも保証されます。
また、以下のように記述することもできます。
def save = Action(parse.tolerantText) { request =>
Ok("Got: " + request.body)
}
この方法では、Content-Type
ヘッダの内容に関わらず、リクエストボディは常に String
としてロードされます。
Tip: Play に含まれるすべてのボディパーサーに、同じような
tolerant
版が用意されています。
次の例では、リクエストボディをファイルとして保存します。
def save = Action(parse.file(to = new File("/tmp/upload"))) { request =>
Ok("Saved the request content to " + request.body)
}
§ボディーパーサーの合成
前の例では、全てのリクエストボディは同じファイルに保存されます。これは問題だと思いませんか? そこで、リクエストのセッションからユーザ名を抽出して、ユーザ毎にユニークなファイルを使うようなボディパーサーを自作してみましょう。
val storeInUserFile = parse.using { request =>
request.session.get("username").map { user =>
file(to = new File("/tmp/" + user + ".upload"))
}.getOrElse {
error(Unauthorized("You don't have the right to upload here"))
}
}
def save = Action(storeInUserFile) { request =>
Ok("Saved the request content to " + request.body)
}
Note: ここでは全く新しい BodyParser を定義することはせずに、既存のものを組み合わせました。大抵はこの方法で必要十分であり、ほとんどのユースケースをカバーできるはずです。
BodyParser
をフルスクラッチで記述する方法については、本ドキュメントの上級者向けの節でご説明します。
§最大 content length
テキストベースのボディパーサー ( text, json, xml, formUrlEncoded のような。) は全てのコンテンツを一旦メモリにロードする必要があるため、最大 content length が設定されています。
デフォルトでは content length は 100KB ですが、コード中で指定することもできます。
// Accept only 10KB of data.
def save = Action(parse.text(maxLength = 1024 * 10)) { request =>
Ok("Got: " + text)
}
Tip: デフォルトの content length は
application.conf
から次のように定義できます。
parsers.text.maxLength=128K
maxLength
であらゆるボディパーサーをラップすることもできます。
// Accept only 10KB of data.
def save = Action(maxLength(1024 * 10, parser = storeInUserFile)) { request =>
Ok("Saved the request content to " + request.body)
}
Next: アクションの合成