ドキュメント

§ボディパーサー

§ボディパーサーとは?

HTTP リクエストはヘッダーとボディで構成されます。ヘッダーは通常小さく、メモリに安全にバッファリングできるため、Play では RequestHeader クラスを使用してモデル化されます。しかし、ボディは非常に長くなる可能性があり、メモリにバッファリングされず、ストリームとしてモデル化されます。ただし、多くのリクエストボディペイロードは小さく、メモリ内でモデル化できるため、ボディストリームをメモリ内のオブジェクトにマッピングするために、Play は BodyParser 抽象化を提供します。

Play は非同期フレームワークであるため、従来の InputStream をリクエストボディの読み取りに使用することはできません。入力ストリームはブロッキングであり、read を呼び出すと、それを呼び出したスレッドはデータが利用可能になるまで待機する必要があります。代わりに、Play は Akka Streams と呼ばれる非同期ストリーミングライブラリを使用します。Akka Streams は Reactive Streams の実装であり、多くの非同期ストリーミングAPI がシームレスに連携できるようにする SPI です。そのため、従来の InputStream ベースのテクノロジーは Play に適していませんが、Akka Streams と Reactive Streams の周りの非同期ライブラリのエコシステム全体は、必要なものすべてを提供します。

§アクションの詳細

以前は、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 型はリクエストボディの型です。BodyParser で処理できる限り、StringNodeSeqArray[Byte]JsonValuejava.io.File など、リクエストボディとして任意の Scala 型を使用できます。

要約すると、Action[A]BodyParser[A] を使用して HTTP リクエストから型 A の値を取得し、アクションコードに渡される Request[A] オブジェクトを構築します。

§組み込みボディパーサーの使用

ほとんどの一般的な Web アプリケーションでは、カスタムボディパーサーを使用する必要はなく、Play の組み込みボディパーサーを使用するだけで済みます。これには、JSON、XML、フォームのパーサーに加えて、プレーンテキストボディを String として、バイトボディを ByteString として処理するパーサーが含まれます。

§デフォルトのボディパーサー

ボディパーサーを明示的に選択しない場合に使用されるデフォルトのボディパーサーは、受信した Content-Type ヘッダーを確認し、それに応じてボディを解析します。たとえば、Content-Typeapplication/json の場合は JsValue として解析され、Content-Typeapplication/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")
    }
}

以下は、デフォルトのボディパーサーでサポートされている型のマッピングです。

デフォルトのボディパーサーは、解析を試みる前に、リクエストにボディがあるかどうかを判断しようとします。HTTP 仕様によると、Content-Length または Transfer-Encoding ヘッダーのいずれかの存在はボディの存在を示しているため、パーサーはこれらのヘッダーのいずれかが存在する場合、または空でないボディが明示的に設定されている FakeRequest の場合にのみ解析されます。

すべてのケースでボディの解析を試行する場合は、下記で説明されている anyContent ボディパーサーを使用できます。

§明示的なボディパーサーの選択

ボディパーサーを明示的に選択する場合は、Actionapply または 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-Typeapplication/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 を最初から記述することについては、高度なトピックセクションで説明します。

§最大コンテンツ長

テキストベースのボディパーサー(textjsonxmlformUrlEncoded など)は、すべてのコンテンツをメモリにロードする必要があるため、最大コンテンツ長を使用します。デフォルトでは、解析する最大コンテンツ長は 100KB です。これは、application.confplay.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 StreamsSinkをラップした薄いレイヤーです。Accumulatorは、非同期的に要素のストリームを結果に蓄積します。Pekko StreamsのSourceを渡して実行できます。これにより、Accumulatorが完了したときに償還されるFutureが返されます。本質的にはSink[E, Future[A]]と同じですが、実際にはこの型のラッパーに過ぎません。しかし、大きな違いは、AccumulatormapmapFuturerecoverなどの便利なメソッドを提供し、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)
}

§遅延ボディ解析

デフォルトでは、ボディ解析はアクション合成の前に実行されます。ただし、アクション合成によって定義された一部(またはすべて)のアクションが処理された後に、ボディ解析を遅らせることも可能です。詳細はこちらを参照してください。

次へ: アクション合成


このドキュメントに誤りを見つけましたか?このページのソースコードはこちらにあります。ドキュメントガイドラインを読んだ後、プルリクエストを自由に送ってください。質問やアドバイスを共有したいですか?コミュニティフォーラムにアクセスして、コミュニティと会話を始めましょう。