Skip to content

airframe-http: RPC binds raw JSON request body to a String type function argument #1100

@xerial

Description

@xerial

Problem description

@RPC 
trait Service {
  def hello(name:String): Response
}

client.service.hello("world") -> creates {"name":"world"} request body

hello({"name":"world"}) will be called

Details

The current workaround is wrapping the RPC argument with a model class. However, this is not intuitive. So we should be able to extract the parameter list from the JSON body at this part:

val contentBytes = adapter.contentBytesOf(request)
if (contentBytes.nonEmpty) {
val msgpack =
adapter.contentTypeOf(request).map(_.split(";")(0)) match {
case Some("application/x-msgpack") =>
contentBytes
case Some("application/json") =>
// JSON -> msgpack
MessagePack.fromJSON(contentBytes)
case _ =>
// Try parsing as JSON first
Try(JSON.parse(contentBytes))
.map { jsonValue =>
JSONCodec.toMsgPack(jsonValue)
}
.getOrElse {
// If parsing as JSON fails, treat the content body as a regular string
StringCodec.toMsgPack(adapter.contentStringOf(request))
}
}
argCodec.unpackMsgPack(msgpack)

This code implicitly assumes that the content body will be mapped to a single argument for non GET requests.

POST requests usually do not depend on query string parameters and we expect POST to send request object using the content body. So sbt-airframe generates JSON request body for primitive values because we need to wrap them with a JSON object (or array) for application/json content type:

// Primitive values (or its Option) cannot be represented in JSON, so we need to wrap them with a map
val params = Seq.newBuilder[String]
httpClientCallInputs.foreach { x => params += s""""${x.name}" -> ${x.name}""" }
clientCallParams += s"Map(${params.result.mkString(", ")})"
typeArgBuilder += Surface.of[Map[String, Any]]

If the RPC interface has primitive type values, using query string can be an option now that we have a fix for #1087. Or we should assume the content body has Map[String, Any] type like {"name":"world"}.

Possible solution

We need to clarify how to map HTTP requests to RPC/Endpoint calls in the protocol:

Unary functions (1 argument, except Request and HttpContect)

def method(p:T): R

The current protocol is as follows:

  • Rule 1) If T is a primitive type, read it from the query_string (for GET) or the request body (for POST) using JSON/MsgPack representation Map("p" -> value). This difference comes from the requirement that GET requests should not convey request bodies in general.
  • Rule 2) If T is a complex type (not a primitive), read its parameters of T from the query_string (for GET) or read the entire T from the request body (for POST) described in JSON/MsgPack.

It seems Rule 2 is causing some inconsistency, so we may need to consolidate these two rules by requiring HTTP client to wrap body contents with JSON object (or MessgePack Map):

  • New Rule: RPC/Endpoint requests should describe the method arguments in Map(parameter_name -> JSON/msgpack) format in query_string (for GET) or query_string + request body (for POST/PUT/DELETE/PATCH).
    • To read the request body, the content-type header must be application/json or application/x-msgpack, otherwise the request body will not be mapped to RPC/Endpoint method arguments. This requirement is necessary to send binary data from file (e.g., PUT can be used to send binary data in the content body: application/octet-stream, multi-part/form-data, etc.)

cc: @shimamoto @takezoe

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions