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
Problem description
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:
airframe/airframe-http/.jvm/src/main/scala/wvlet/airframe/http/router/HttpRequestMapper.scala
Lines 82 to 103 in 3a2d703
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:
airframe/airframe-http/.jvm/src/main/scala/wvlet/airframe/http/codegen/HttpClientIR.scala
Lines 145 to 149 in 3a2d703
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): RThe current protocol is as follows:
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):
application/jsonorapplication/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