Documentation

You are viewing the documentation for the 2.1.5 release in the 2.1.x series of releases. The latest stable release series is 2.4.x.

§ボディパーサー

§ボディパーサーの概要

HTTP PUT や POST リクエストはボディを含みます。このボディは Content-Type リクエストヘッダで指定さえしておけば、どんなフォーマットであっても構いません。Play では ボディパーサー がリクエストボディを Scala の値に変換します。

しかし、HTTP リクエストのリクエストボディはとても大きなサイズになる可能性があり、 ボディパーサー が全てのデータセットがメモリにロードされるのを単純に待ってからパースを行うというのは現実的ではありません。BodyParser[A] は基本的に Iteratee[Array[Byte],A] です。これは、ボディパーサーはバイトデータの塊を(webブラウザがデータをアップロードし続ける限り)入力として受け取り、結果として A 型の値を計算する、ということを意味します。

いくつか例を見てみましょう。

これらに加えて、 ボディパーサー はリクエストボディのパースを始める前に 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 関数が実行され、計算されたボディの値がリクエストに渡されます。

§アクションについての詳細

以前、ActionRequest => 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 ヘッダの内容に応じて、ボディを何として処理すべきかを決定します。

例えば、以下のように利用します。

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: アクションの合成