You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Scala中如何编写Akka HttpResponse的Circe/Jackson编解码器

Akka HttpResponse编解码器中Entity部分的处理方案

我需要将客户端的请求响应存储到文件中,已成功实现HttpRequest的编解码器,但在Scala中为Akka的HttpResponse编写编解码器时,不清楚如何处理HttpResponse的Entity部分。

已实现的HttpRequest编解码器及示例

HttpRequest示例代码:

val demo = HttpRequest(
  method = HttpMethods.GET,
  uri = "myUri",
  headers = generateHeaders(Map.empty),
  entity = "{\"customerReferenceIds\":[{\"customerId\":\"9600007934256702\",\"customerIdType\":\"CUSTOMER_ID\"}]}",
)

demo.asJson.spaces2

HttpRequest编解码器:

implicit val HttpRequestEncoder: Encoder[HttpRequest] = new Encoder[HttpRequest] {
  final def apply(x: HttpRequest): Json = Json.obj(
    ("method", Json.fromString(x.method.value)),
    ("Uri", Json.fromString(x.uri.toString())) ,
    ("headers", x.headers.map(y => y.name -> y.value).toMap.asJson),
    ("entity", Json.fromString(JsonUtil.toJson(x.entity)))
  )
}

implicit val HttpRequestDecoder: Decoder[HttpRequest] = new Decoder[HttpRequest] {
  final def apply(c: HCursor): Decoder.Result[HttpRequest] =
    for {
      method <- c.downField("method").as[String]
      url <- c.downField("Uri").as[String]
      header <- c.downField("headers").as[Map[String,String]]
      entity <- c.downField("entity").as[String]
    } yield {
      HttpRequest(
        method = HttpMethods.getForKeyCaseInsensitive(method).getOrElse(HttpMethods.GET),
        uri = url,
        headers = generateHeaders(header),
        entity = HttpEntity(ContentTypes.`application/json`, JsonUtil.toJson(entity))
      )
    }
}

尝试的HttpResponse编解码器(存在Entity处理问题)

// HttpResponse
implicit val HttpResponseEncoder: Encoder[HttpResponse] = new Encoder[HttpResponse] {
  final def apply(x: HttpResponse): Json = {
    Json.obj(
      ("response", Json.fromString(JsonUtil.toJson(x.entity))),
      ("status", Json.fromInt(x.status.intValue()))
    )
  }
}

implicit val HttpResponseDecoder: Decoder[HttpResponse] = new Decoder[HttpResponse] {
  final def apply(c: HCursor): Decoder.Result[HttpResponse] =
    for {
      entity <- c.downField("response").as[String]
      status <- c.downField("status").as[Int]
    } yield {
      HttpResponse(
        status = StatusCode.int2StatusCode(status),
        entity = HttpEntity(ContentTypes.`application/json`, JsonUtil.toJson(entity))
      )
    }
}

调试中观察到的Entity状态

调试器中显示Entity为DefaultFullHttpEntity类型,包含以下核心属性:

  • contentType:值为application/json
  • data:ByteString类型的原始字节数据
  • length:内容长度
    当前data处于未解析的原始字节状态,直接序列化会导致内容无法正确转换为可读的JSON格式。

修正后的HttpResponse编解码器

核心思路

Akka的HttpEntity(尤其是DefaultFullHttpEntity)的内容存储为ByteString,需要先将其解码为字符串并保留contentType信息,才能正确序列化和反序列化。同时要处理无Entity的边界情况。

import akka.http.scaladsl.model.{HttpEntity, ContentType, HttpResponse, StatusCode, HttpMethods}
import akka.util.ByteString
import io.circe.{Encoder, Decoder, Json, HCursor}
import scala.concurrent.duration._

// 先实现HttpEntity.Strict的编解码器
implicit val httpEntityEncoder: Encoder[HttpEntity.Strict] = Encoder.instance { entity =>
  val charset = entity.contentType.charsetOption.getOrElse(java.nio.charset.StandardCharsets.UTF_8)
  Json.obj(
    "contentType" -> Json.fromString(entity.contentType.toString),
    "content" -> Json.fromString(entity.data.decodeString(charset))
  )
}

implicit val httpResponseEncoder: Encoder[HttpResponse] = Encoder.instance { response =>
  // 将Entity转换为Strict类型(需指定超时)
  val strictEntity = response.entity.toStrict(5.seconds).map(_.asJson).getOrElse(Json.Null)
  Json.obj(
    "status" -> Json.fromInt(response.status.intValue),
    "headers" -> response.headers.map(h => h.name -> h.value).toMap.asJson,
    "entity" -> strictEntity
  )
}

// 反解码器部分
implicit val httpEntityDecoder: Decoder[HttpEntity.Strict] = Decoder.instance { c =>
  for {
    contentTypeStr <- c.downField("contentType").as[String]
    content <- c.downField("content").as[String]
  } yield {
    val contentType = ContentType.parse(contentTypeStr).right.get
    val charset = contentType.charsetOption.getOrElse(java.nio.charset.StandardCharsets.UTF_8)
    HttpEntity.Strict(contentType, ByteString.fromString(content, charset))
  }
}

implicit val httpResponseDecoder: Decoder[HttpResponse] = Decoder.instance { c =>
  for {
    statusCode <- c.downField("status").as[Int].map(StatusCode.int2StatusCode)
    headers <- c.downField("headers").as[Map[String, String]].map(generateHeaders)
    entity <- c.downField("entity").as[Option[HttpEntity.Strict]].map(_.getOrElse(HttpEntity.Empty))
  } yield {
    HttpResponse(
      status = statusCode,
      headers = headers,
      entity = entity
    )
  }
}

关键说明

  1. Entity严格化:通过toStrict将流式Entity转换为内存中的Strict Entity,确保能完整读取内容。
  2. 字符集一致性:序列化和反序列化时使用Entity自带的字符集,避免乱码。
  3. 边界情况处理:用Option处理空Entity,避免无Entity时的序列化错误。
  4. 保留ContentType:序列化时存储ContentType信息,反序列化时据此还原正确的Entity类型。

内容的提问来源于stack exchange,提问作者Freez

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.12 14:05:46