ともちんの Tech ブログ

Akka HTTP Cilent API を使って JSON データを送信する

Akka HTTP Client API を使って HTTP リクエストを送信してみました。

やりたいこと

  • API サーバーに POST リクエストを送って JSON データを送信したい
  • 返ってきたレスポンスの body を確認したい

Client API とは

Client API にはいくつかの種類があります [1]。

  • Request-Level Client-Side API
  • Host-Level Client-Side API
  • Connection-Level Client-Side API

今回は、最も基本的な 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.StrictUniversalEntityHttpEntity.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: JsValueAPI に送信したい場合、JsValueByteString に変換して、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 のおまじないのようなものらしいです。アクターモデルについては知識がないので、ここでは言及しません。

処理の基本的な流れは次のようになっていることがわかります。

  1. HttpRequest のインスタンスを生成する。
  2. Http().singleRequest メソッドに HttpRequest を渡すことによって、リクエストを送信する。
  3. レスポンスは 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")
  }
}

実際に実行してみると・・・

f:id:tomoki0606:20200814011819p:plain

と、レスポンスの内容を確認できました!

参考文献

[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閲覧。