§ボディパーサー
§ボディパーサーとは?
HTTP リクエストはヘッダーとボディで構成されます。ヘッダーは通常小さく、メモリに安全にバッファリングできるため、Play では RequestHeader
クラスを使用してモデル化されます。しかし、ボディは非常に長くなる可能性があり、メモリにバッファリングされず、ストリームとしてモデル化されます。ただし、多くのリクエストボディペイロードは小さく、メモリ内でモデル化できるため、ボディストリームをメモリ内のオブジェクトにマッピングするために、Play は BodyParser
抽象化を提供します。
Play は非同期フレームワークであるため、従来の InputStream
をリクエストボディの読み取りに使用することはできません。入力ストリームはブロッキングであり、read
を呼び出すと、それを呼び出したスレッドはデータが利用可能になるまで待機する必要があります。代わりに、Play は Akka Streams と呼ばれる非同期ストリーミングライブラリを使用します。Akka Streams は Reactive Streams の実装であり、多くの非同期ストリーミングAPI がシームレスに連携できるようにする SPI です。そのため、従来の InputStream
ベースのテクノロジーは Play に適していませんが、Akka Streams と Reactive Streams の周りの非同期ライブラリのエコシステム全体は、必要なものすべてを提供します。
§アクションの詳細
以前は、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
型はリクエストボディの型です。BodyParser
で処理できる限り、String
、NodeSeq
、Array[Byte]
、JsonValue
、java.io.File
など、リクエストボディとして任意の Scala 型を使用できます。
要約すると、Action[A]
は BodyParser[A]
を使用して HTTP リクエストから型 A
の値を取得し、アクションコードに渡される Request[A]
オブジェクトを構築します。
§組み込みボディパーサーの使用
ほとんどの一般的な Web アプリケーションでは、カスタムボディパーサーを使用する必要はなく、Play の組み込みボディパーサーを使用するだけで済みます。これには、JSON、XML、フォームのパーサーに加えて、プレーンテキストボディを String として、バイトボディを ByteString
として処理するパーサーが含まれます。
§デフォルトのボディパーサー
ボディパーサーを明示的に選択しない場合に使用されるデフォルトのボディパーサーは、受信した Content-Type
ヘッダーを確認し、それに応じてボディを解析します。たとえば、Content-Type
が application/json
の場合は JsValue
として解析され、Content-Type
が application/x-www-form-urlencoded
の場合は Map[String, Seq[String]]
として解析されます。
デフォルトのボディパーサーは、型 AnyContent
のボディを生成します。AnyContent
でサポートされているさまざまな型は、asJson
など、as
メソッドを介してアクセスできます。これはボディ型の Option
を返します。
def save: Action[AnyContent] = Action { (request: Request[AnyContent]) =>
val body: AnyContent = request.body
val jsonBody: Option[JsValue] = body.asJson
// Expecting json body
jsonBody
.map { json => Ok("Got: " + (json \ "name").as[String]) }
.getOrElse {
BadRequest("Expecting application/json request body")
}
}
以下は、デフォルトのボディパーサーでサポートされている型のマッピングです。
- text/plain:
String
、asText
を介してアクセスできます。 - application/json:
JsValue
、asJson
を介してアクセスできます。 - application/xml、text/xml または application/XXX+xml:
scala.xml.NodeSeq
、asXml
を介してアクセスできます。 - application/x-www-form-urlencoded:
Map[String, Seq[String]]
、asFormUrlEncoded
を介してアクセスできます。 - multipart/form-data:
MultipartFormData
、asMultipartFormData
を介してアクセスできます。 - その他のコンテンツタイプ:
RawBuffer
、asRaw
を介してアクセスできます。
デフォルトのボディパーサーは、解析を試みる前に、リクエストにボディがあるかどうかを判断しようとします。HTTP 仕様によると、Content-Length
または Transfer-Encoding
ヘッダーのいずれかの存在はボディの存在を示しているため、パーサーはこれらのヘッダーのいずれかが存在する場合、または空でないボディが明示的に設定されている FakeRequest
の場合にのみ解析されます。
すべてのケースでボディの解析を試行する場合は、下記で説明されている anyContent
ボディパーサーを使用できます。
§明示的なボディパーサーの選択
ボディパーサーを明示的に選択する場合は、Action
の apply
または async
メソッドにボディパーサーを渡すことで実行できます。
Play はすぐに使える多くのボディパーサーを提供します。これは、コントローラーに注入できる PlayBodyParsers
トレイトを介して利用できます。
たとえば、JSON ボディを期待するアクションを定義するには(前の例のように)
def save: Action[JsValue] = Action(parse.json) { (request: Request[JsValue]) =>
Ok("Got: " + (request.body \ "name").as[String])
}
今回は、ボディの型が JsValue
であることに注意してください。これにより、Option
でなくなったため、ボディを簡単に操作できます。Option
ではない理由は、JSON ボディパーサーがリクエストに Content-Type
が application/json
であることを検証し、リクエストがその期待を満たさない場合は 415 Unsupported Media Type
レスポンスを返すためです。したがって、アクションコードで再度確認する必要はありません。
もちろん、これはクライアントが適切に動作し、リクエストに正しい Content-Type
ヘッダーを送信する必要があることを意味します。もう少し緩くしたい場合は、tolerantJson
を代わりに使用できます。これは Content-Type
を無視し、関係なくボディを JSON として解析しようとします。
def save: Action[JsValue] = Action(parse.tolerantJson) { (request: Request[JsValue]) =>
Ok("Got: " + (request.body \ "name").as[String])
}
次に、リクエストボディをファイルに保存する別の例を示します。
def save: Action[File] = Action(parse.file(to = new File("/tmp/upload"))) { (request: Request[File]) =>
Ok("Saved the request content to " + request.body)
}
§ボディパーサーの組み合わせ
前の例では、すべてのリクエストボディが同じファイルに保存されます。これは少し問題がありますよね?リクエストセッションからユーザー名を取得し、各ユーザーに固有のファイルを提供するカスタムボディパーサーをもう1つ書きましょう。
val storeInUserFile = parse.using { request =>
request.session
.get("username")
.map { user => parse.file(to = new File("/tmp/" + user + ".upload")) }
.getOrElse {
sys.error("You don't have the right to upload here")
}
}
def save: Action[File] = Action(storeInUserFile) { request => Ok("Saved the request content to " + request.body) }
注記: ここでは、独自の BodyParser を実際に記述しているわけではなく、既存のものを組み合わせているだけです。これは多くの場合で十分であり、ほとんどのユースケースをカバーするはずです。
BodyParser
を最初から記述することについては、高度なトピックセクションで説明します。
§最大コンテンツ長
テキストベースのボディパーサー(text、json、xml、formUrlEncoded など)は、すべてのコンテンツをメモリにロードする必要があるため、最大コンテンツ長を使用します。デフォルトでは、解析する最大コンテンツ長は 100KB です。これは、application.conf
で play.http.parser.maxMemoryBuffer
プロパティを指定することで上書きできます。
play.http.parser.maxMemoryBuffer=128K
raw パーサーや multipart/form-data
など、ディスクにコンテンツをバッファリングするパーサーの場合、最大コンテンツ長は play.http.parser.maxDiskBuffer
プロパティを使用して指定され、デフォルトは 10MB です。multipart/form-data
パーサーは、データフィールドの合計についてもテキストの最大長プロパティを適用します。
特定のアクションのデフォルトの最大長を上書きすることもできます。
// Accept only 10KB of data.
def save: Action[String] = Action(parse.text(maxLength = 1024 * 10)) { (request: Request[String]) =>
Ok("Got: " + text)
}
maxLength
を使用して任意のボディパーサーをラップすることもできます。
// Accept only 10KB of data.
def save: Action[Either[MaxSizeExceeded, File]] = Action(parse.maxLength(1024 * 10, storeInUserFile)) {
request =>
Ok("Saved the request content to " + request.body)
}
§カスタムボディパーサーの作成
カスタムボディパーサーは、BodyParser
トレイトを実装することで作成できます。このトレイトは単なる関数です。
trait BodyParser[+A] extends (RequestHeader => Accumulator[ByteString, Either[Result, A]])
この関数のシグネチャは最初は少しわかりにくいかもしれませんが、分解してみましょう。
この関数は RequestHeader
を受け取ります。これはリクエストに関する情報の確認に使用できます。最も一般的には、Content-Type
を取得するために使用され、ボディを正しく解析できます。
この関数の戻り値の型は、Accumulator
です。Accumulatorは、Pekko StreamsのSink
をラップした薄いレイヤーです。Accumulatorは、非同期的に要素のストリームを結果に蓄積します。Pekko StreamsのSource
を渡して実行できます。これにより、Accumulatorが完了したときに償還されるFuture
が返されます。本質的にはSink[E, Future[A]]
と同じですが、実際にはこの型のラッパーに過ぎません。しかし、大きな違いは、Accumulator
がmap
、mapFuture
、recover
などの便利なメソッドを提供し、Promiseのように結果を操作できるのに対し、Sink
ではそのような操作をすべてmapMaterializedValue
呼び出しでラップする必要があることです。
apply
メソッドが返すAccumulatorは、ByteString
型の要素を消費します。これらは基本的にバイトの配列ですが、byte[]
とは異なり、ByteString
は不変であり、スライスや追加などの多くの操作が一定時間で実行されます。
Accumulatorの戻り値の型はEither[Result, A]
です。Result
を返すか、A
型のボディを返します。エラーが発生した場合(例えば、ボディの解析に失敗した場合、Content-Type
がボディパーサーが受け入れる型と一致しなかった場合、またはメモリバッファの上限を超えた場合)は、一般的にResult
が返されます。ボディパーサーが結果を返すと、アクションの処理はショートサーキットされます。ボディパーサーの結果がすぐに返され、アクションは呼び出されません。
§ボディの別の場所への転送
ボディパーサーを作成する一般的なユースケースの1つは、実際にはボディを解析したくない場合、つまり、別の場所にストリーム化したい場合です。これを行うには、カスタムボディパーサーを定義できます。
import javax.inject._
import scala.concurrent.ExecutionContext
import org.apache.pekko.util.ByteString
import play.api.libs.streams._
import play.api.libs.ws._
import play.api.mvc._
class MyController @Inject() (ws: WSClient, val controllerComponents: ControllerComponents)(
implicit ec: ExecutionContext
) extends BaseController {
def forward(request: WSRequest): BodyParser[WSResponse] = BodyParser { req =>
Accumulator.source[ByteString].mapFuture { source =>
request
.withBody(source)
.execute("POST")
.map(Right.apply)
}
}
def myAction: Action[WSResponse] = Action(forward(ws.url("https://example.com"))) { req => Ok("Uploaded") }
}
§Pekko Streamsを使用したカスタム解析
まれに、Pekko Streamsを使用してカスタムパーサーを作成する必要がある場合があります。ほとんどの場合、最初にボディをByteString
にバッファリングするだけで十分です。これにより、命令型メソッドとランダムアクセスをボディで使用できるため、はるかに簡単な方法で解析できます。
ただし、それが不可能な場合(例えば、解析する必要があるボディがメモリに収まらないほど長い場合)は、カスタムボディパーサーを作成する必要があります。
Pekko Streamsの使用方法の完全な説明は、このドキュメントの範囲を超えています。開始するには、Pekko Streamsドキュメントを読むのが最適です。ただし、以下は、Pekko Streamsクックブックの「ByteStringsのストリームからの行の解析」ドキュメントに基づいたCSVパーサーを示しています。
import org.apache.pekko.stream.scaladsl._
import org.apache.pekko.util.ByteString
import play.api.libs.streams._
import play.api.mvc.BodyParser
val Action = inject[DefaultActionBuilder]
val csv: BodyParser[Seq[Seq[String]]] = BodyParser { req =>
// A flow that splits the stream into CSV lines
val sink: Sink[ByteString, Future[Seq[Seq[String]]]] = Flow[ByteString]
// We split by the new line character, allowing a maximum of 1000 characters per line
.via(Framing.delimiter(ByteString("\n"), 1000, allowTruncation = true))
// Turn each line to a String and split it by commas
.map(_.utf8String.trim.split(",").toSeq)
// Now we fold it into a list
.toMat(Sink.fold(Seq.empty[Seq[String]])(_ :+ _))(Keep.right)
// Convert the body to a Right either
Accumulator(sink).map(Right.apply)
}
§遅延ボディ解析
デフォルトでは、ボディ解析はアクション合成の前に実行されます。ただし、アクション合成によって定義された一部(またはすべて)のアクションが処理された後に、ボディ解析を遅らせることも可能です。詳細はこちらを参照してください。
次へ: アクション合成
このドキュメントに誤りを見つけましたか?このページのソースコードはこちらにあります。ドキュメントガイドラインを読んだ後、プルリクエストを自由に送ってください。質問やアドバイスを共有したいですか?コミュニティフォーラムにアクセスして、コミュニティと会話を始めましょう。