Akka HTTP Client API を使って HTTP リクエストを送信してみました。
やりたいこと
Client API とは
Client API にはいくつかの種類があります [1]。
今回は、最も基本的な APIである Request-Level Client-Side API を用います。
これらの API は HttpRequest オブジェクトと HttpResponse オブジェクトを利用してリクエストを扱います [2]。
HttpRequest
まず、HttpRequest について見ていきましょう。
HttpRequest クラスの定義は以下のようになっています。
/** * The immutable model HTTP request model. */ final class HttpRequest( val method: HttpMethod, val uri: Uri, val headers: immutable.Seq[HttpHeader], val attributes: Map[AttributeKey[_], _], val entity: RequestEntity, val protocol: HttpProtocol) extends jm.HttpRequest with HttpMessage {
apply メソッドには引数のデフォルト値が定義されているので、簡単にインスタンスを生成することができます。
def apply( method: HttpMethod = HttpMethods.GET, uri: Uri = Uri./, headers: immutable.Seq[HttpHeader] = Nil, entity: RequestEntity = HttpEntity.Empty, protocol: HttpProtocol = HttpProtocols.`HTTP/1.1`) = new HttpRequest(method, uri, headers, Map.empty, entity, protocol)
例えば、URI だけ指定してインスタンスを生成することができます。
HttpRequest(uri = "https://akka.io")
リクエストのメソッドを変えたい場合は method に引数を渡せばよいし、ヘッダーを追加したい場合は headers に Seq[HttpHeader]
を渡せばよいです。 RequestEntity
はリクエストに含まれるデータで、ContentType と Raw データを与えます。
RequestEntity
RequestEntity は HttpEntity を継承したトレイトです。HttpEntity にはいくつかの apply メソッドが定義されており、それぞれ HttpEntity.Strict や UniversalEntity、HttpEntity.Chunked のインスタンスを生成しています。
sealed trait UniversalEntity extends jm.UniversalEntity with MessageEntity with BodyPartEntity final case class Strict(contentType: ContentType, data: ByteString) extends jm.HttpEntity.Strict with UniversalEntity final case class Chunked(contentType: ContentType, chunks: Source[ChunkStreamPart, Any]) extends jm.HttpEntity.Chunked with MessageEntity
POST リクエストで JSON を送りたい場合、ContentType には application/json
を指定します。ContentTypes オブジェクトに用意されているので、それを使います。
val `application/json` = ContentType(MediaTypes.`application/json`)
例えば、JSON データ data: JsValue
を API に送信したい場合、JsValue
を ByteString
に変換して、HttpEntity.Strict#apply
メソッドに渡します。
HttpEntity.Strict( contentType = ContentTypes.`application/json`, data = ByteString(Json.prettyPrint(data)) )
HttpResponse
Client API を用いて HttpRequest を送信したあと、サーバーから帰ってくるレスポンスは HttpResponse として得られます。
/** * The immutable HTTP response model. */ final class HttpResponse( val status: StatusCode, val headers: immutable.Seq[HttpHeader], val attributes: Map[AttributeKey[_], _], val entity: ResponseEntity, val protocol: HttpProtocol) extends jm.HttpResponse with HttpMessage {
ResponseEntity は HttpEntity を継承したトレイトです。ResponseEntity からデータを取り出す(すなわち、API から返ってきたデータを取り出す)ときは dataBytes メソッドを呼び出します。
/** * A stream of the data of this entity. */ def dataBytes: Source[ByteString, Any]
ここで、 Source
は akka-stream 特有のデータ型であり、データを遅延評価で Output する役割を持ちます [3, 4]。
Request-Level Client-Side API
Request-Level Client-Side API は、Akka の HTTP Client の機能で最も便利なものです [5]。その内部は Host-Level Client-Side API の上で構築されており、HTTP レスポンスをより簡潔で使いやすいようにしているらしいです。設定に応じて、Flow-Based のものか Future-Based のものかを選択できます。Future 型で返ってきて欲しいので、Future-Based のものを用いることにします。
Future-Based
Http().singleRequest(...)
メソッドを用いることによって、 Future[HttpResponse]
型のレスポンスを取得することができます。
リクエストは URI の絶対パスか、有効な Host
ヘッダーを持つ必要があります。持たない場合、Future はエラーと主に終了します。
例
以下は Future-Based なリクエスト送信の例です [6]。
/* * Copyright (C) 2020 Lightbend Inc. <https://www.lightbend.com> */ package docs.http.scaladsl import akka.actor.typed.ActorSystem import akka.actor.typed.scaladsl.Behaviors import akka.http.scaladsl.Http import akka.http.scaladsl.model._ import scala.concurrent.Future import scala.util.{ Failure, Success } object HttpClientSingleRequest { def main(args: Array[String]): Unit = { implicit val system = ActorSystem(Behaviors.empty, "SingleRequest") // needed for the future flatMap/onComplete in the end implicit val executionContext = system.executionContext val responseFuture: Future[HttpResponse] = Http().singleRequest(HttpRequest(uri = "http://akka.io")) responseFuture .onComplete { case Success(res) => println(res) case Failure(_) => sys.error("something wrong") } } }
implicit な ActorSystem
は、Http()
でインスタンス生成するために必要な変数です。Akka のおまじないのようなものらしいです。アクターモデルについては知識がないので、ここでは言及しません。
処理の基本的な流れは次のようになっていることがわかります。
- HttpRequest のインスタンスを生成する。
- Http().singleRequest メソッドに HttpRequest を渡すことによって、リクエストを送信する。
- レスポンスは Future に包んで返される。Future 型の値を扱うようにレスポンスを扱える。
実装: JSON データの送信
Akka HTTP Client について一通り見終わったので、JSON データを POST で送信するように実装してみます。
ローカルに API サーバーを立てて POST リクエストを送る実装は以下のようになりました。
サーバーに送る JSON データはこのようになっています。
{ "user": { "name": "tomochin", "location": "Tokyo", "age": "18" }, "message": "JSON Post test!" }
サーバー側で POST リクエストを処理するメソッドは以下のように実装しています。
def jsonTest() = Action { implicit req => val body: AnyContent = req.body val json: Option[JsValue] = body.asJson // Expecting json body json.map { json => Ok("Got: " + Json.prettyPrint(json)) }.getOrElse { BadRequest("Expecting application/json request body") } }
実際に実行してみると・・・
と、レスポンスの内容を確認できました!
参考文献
[1] 5. Client API - Akka HTTP, https://doc.akka.io/docs/akka-http/current/client-side/index.html, 2020-08-13閲覧。
[2] HttpRequest and HttpResponse - Akka HTTP, https://doc.akka.io/docs/akka-http/current/client-side/request-and-response.html, 2020-08-13閲覧。
[3] Akka Streams についての基礎概念, https://qiita.com/xoyo24/items/299ee3e624f4afe2d27a, 2020-08-13閲覧。
[4] Basics and working with Flows - Akka Documentation, https://doc.akka.io/docs/akka/current/stream/stream-flows-and-basics.html, 2020-08-13閲覧。
[5] Request-Level Client-Side API - Akka HTTP, https://doc.akka.io/docs/akka-http/current/client-side/request-level.html, 2020-08-13閲覧。
[6] akka-http/docs/src/test/scala/docs/http/scaladsl/HttpClientSingleRequest.scala, https://github.com/akka/akka-http/blob/v10.2.0/docs/src/test/scala/docs/http/scaladsl/HttpClientSingleRequest.scala, 2020-08-13閲覧。