problem: Proxy fails to serialize JSON

reason: expects full JSON response from upstream, which is replaces with result field only
This commit is contained in:
Igor Artamonov
2020-06-29 22:44:44 -04:00
parent 04958784ec
commit c2636cd81e
5 changed files with 200 additions and 60 deletions

View File

@@ -19,6 +19,7 @@ import com.fasterxml.jackson.core.Version
import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.databind.module.SimpleModule
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@@ -31,6 +32,7 @@ class Global {
private fun createObjectMapper(): ObjectMapper { private fun createObjectMapper(): ObjectMapper {
val module = SimpleModule("EmeraldDshackle", Version(1, 0, 0, null, null, null)) val module = SimpleModule("EmeraldDshackle", Version(1, 0, 0, null, null, null))
module.addSerializer(JsonRpcResponse::class.java, JsonRpcResponse.ResponseJsonSerializer())
val objectMapper = ObjectMapper() val objectMapper = ObjectMapper()
objectMapper.registerModule(module) objectMapper.registerModule(module)

View File

@@ -19,15 +19,11 @@ package io.emeraldpay.dshackle.proxy
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.api.proto.BlockchainOuterClass import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.Global import io.emeraldpay.dshackle.Global
import io.infinitape.etherjar.rpc.RpcResponseError import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.infinitape.etherjar.rpc.json.ResponseJson
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import java.lang.StringBuilder
import java.time.Duration
import java.util.function.Function import java.util.function.Function
/** /**
@@ -48,29 +44,32 @@ open class WriteRpcJson() {
open fun toJsons(call: ProxyCall): Function<Flux<BlockchainOuterClass.NativeCallReplyItem>, Flux<String>> { open fun toJsons(call: ProxyCall): Function<Flux<BlockchainOuterClass.NativeCallReplyItem>, Flux<String>> {
return Function { flux -> return Function { flux ->
flux.flatMap { response -> flux.flatMap { response ->
val json = ResponseJson<Any, Any>()
if (!call.ids.containsKey(response.id)) { if (!call.ids.containsKey(response.id)) {
log.warn("ID wasn't requested: ${response.id}") log.warn("ID wasn't requested: ${response.id}")
return@flatMap Flux.empty<String>() return@flatMap Flux.empty<String>()
} }
json.id = call.ids[response.id] val json = toJson(call, response)
if (response.succeed) { if (json == null) {
val payload = objectMapper.readValue(response.payload.toByteArray(), ResponseJson::class.java) Flux.empty<String>()
if (payload.error != null) {
json.error = payload.error
} else {
json.result = payload.result
}
} else { } else {
json.error = RpcResponseError(-32002, response.errorMessage) Flux.just(json)
} }
Flux.just(objectMapper.writeValueAsString(json))
}.onErrorContinue { t, u -> }.onErrorContinue { t, u ->
log.warn("Failed to convert to JSON", t) log.warn("Failed to convert to JSON", t)
} }
} }
} }
open fun toJson(call: ProxyCall, response: BlockchainOuterClass.NativeCallReplyItem): String? {
val id = call.ids[response.id] ?: return null;
val json = if (response.succeed) {
JsonRpcResponse.ok(response.payload.toByteArray(), JsonRpcResponse.Id.from(id))
} else {
JsonRpcResponse.error(-32002, response.errorMessage, JsonRpcResponse.Id.from(id))
}
return objectMapper.writeValueAsString(json)
}
/** /**
* Format response as JSON Array, for Batch requests * Format response as JSON Array, for Batch requests
*/ */

View File

@@ -23,9 +23,12 @@ import reactor.core.publisher.Mono
class JsonRpcResponse( class JsonRpcResponse(
private val result: ByteArray?, private val result: ByteArray?,
val error: ResponseError? val error: ResponseError?,
val id: Id
) { ) {
constructor(result: ByteArray?, error: ResponseError?) : this(result, error, IntId(0))
companion object { companion object {
private val NULL_VALUE = "null".toByteArray() private val NULL_VALUE = "null".toByteArray()
@@ -34,6 +37,11 @@ class JsonRpcResponse(
return JsonRpcResponse(value, null) return JsonRpcResponse(value, null)
} }
@JvmStatic
fun ok(value: ByteArray, id: Id): JsonRpcResponse {
return JsonRpcResponse(value, null, id);
}
@JvmStatic @JvmStatic
fun ok(value: String): JsonRpcResponse { fun ok(value: String): JsonRpcResponse {
return JsonRpcResponse(value.toByteArray(), null) return JsonRpcResponse(value.toByteArray(), null)
@@ -43,6 +51,11 @@ class JsonRpcResponse(
fun error(code: Int, msg: String): JsonRpcResponse { fun error(code: Int, msg: String): JsonRpcResponse {
return JsonRpcResponse(null, ResponseError(code, msg)) return JsonRpcResponse(null, ResponseError(code, msg))
} }
@JvmStatic
fun error(code: Int, msg: String, id: Id): JsonRpcResponse {
return JsonRpcResponse(null, ResponseError(code, msg), id)
}
} }
fun hasResult(): Boolean { fun hasResult(): Boolean {
@@ -114,11 +127,67 @@ class JsonRpcResponse(
} }
} }
/**
* JSON RPC wrapper. Makes sure that the id is either Int or String
*/
interface Id {
fun asInt(): Int
fun asString(): String
fun isInt(): Boolean
companion object {
fun from(id: Any): Id {
if (id is Int) {
return IntId(id)
}
if (id is Number) {
return IntId(id.toInt())
}
if (id is String) {
return StringId(id)
}
throw IllegalArgumentException("Id must be Int or String")
}
}
}
class IntId(val id: Int) : Id {
override fun asInt(): Int {
return id
}
override fun asString(): String {
throw IllegalStateException("Not string")
}
override fun isInt(): Boolean {
return true
}
}
class StringId(val id: String) : Id {
override fun asInt(): Int {
throw IllegalStateException("Not int")
}
override fun asString(): String {
return id
}
override fun isInt(): Boolean {
return false
}
}
class ResponseJsonSerializer : JsonSerializer<JsonRpcResponse>() { class ResponseJsonSerializer : JsonSerializer<JsonRpcResponse>() {
override fun serialize(value: JsonRpcResponse, gen: JsonGenerator, serializers: SerializerProvider) { override fun serialize(value: JsonRpcResponse, gen: JsonGenerator, serializers: SerializerProvider) {
gen.writeStartObject() gen.writeStartObject()
gen.writeStringField("jsonrpc", "2.0") gen.writeStringField("jsonrpc", "2.0")
gen.writeNumberField("id", 0) if (value.id.isInt()) {
gen.writeNumberField("id", value.id.asInt())
} else {
gen.writeStringField("id", value.id.asString())
}
if (value.error != null) { if (value.error != null) {
gen.writeObjectFieldStart("error") gen.writeObjectFieldStart("error")
gen.writeNumberField("code", value.error.code) gen.writeNumberField("code", value.error.code)
@@ -128,7 +197,9 @@ class JsonRpcResponse(
if (value.result == null) { if (value.result == null) {
throw IllegalStateException("No result set") throw IllegalStateException("No result set")
} }
gen.writeRawUTF8String(value.result, 0, value.result.size) gen.writeFieldName("result")
gen.writeRaw(":")
gen.writeRaw(String(value.result))
} }
gen.writeEndObject() gen.writeEndObject()
} }

View File

@@ -84,41 +84,18 @@ class WriteRpcJsonSpec extends Specification {
def "Convert basic to JSON"() { def "Convert basic to JSON"() {
setup: setup:
def call = new ProxyCall(ProxyCall.RpcType.SINGLE) def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
call.ids[1] = "aaa" call.ids[1] = 105
def data = [ def data = [
BlockchainOuterClass.NativeCallReplyItem.newBuilder() BlockchainOuterClass.NativeCallReplyItem.newBuilder()
.setId(1) .setId(1)
.setSucceed(true) .setSucceed(true)
.setPayload(ByteString.copyFrom('{"jsonrpc": "2.0", "id": 1, "result": "0x98dbb1"}', 'UTF-8')) .setPayload(ByteString.copyFrom('"0x98dbb1"', 'UTF-8'))
.build() .build()
] ]
when: when:
def act = Flux.fromIterable(data) def act = writer.toJson(call, data[0])
.transform(writer.toJsons(call))
.collectList()
.block(Duration.ofSeconds(1))
then: then:
act == ['{"jsonrpc":"2.0","id":"aaa","result":"0x98dbb1"}'] act == '{"jsonrpc":"2.0","id":105,"result":"0x98dbb1"}'
}
def "Convert error to JSON"() {
setup:
def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
call.ids[1] = 1
def data = [
BlockchainOuterClass.NativeCallReplyItem.newBuilder()
.setId(1)
.setSucceed(true)
.setPayload(ByteString.copyFrom('{"jsonrpc": "2.0", "id": 1, "error": {"code": -32001, "message": "oops"}}', 'UTF-8'))
.build()
]
when:
def act = Flux.fromIterable(data)
.transform(writer.toJsons(call))
.collectList()
.block(Duration.ofSeconds(1))
then:
act == ['{"jsonrpc":"2.0","id":1,"error":{"code":-32001,"message":"oops"}}']
} }
def "Convert gRPC error to JSON"() { def "Convert gRPC error to JSON"() {
@@ -133,12 +110,26 @@ class WriteRpcJsonSpec extends Specification {
.build() .build()
] ]
when: when:
def act = Flux.fromIterable(data) def act = writer.toJson(call, data[0])
.transform(writer.toJsons(call))
.collectList()
.block(Duration.ofSeconds(1))
then: then:
act == ['{"jsonrpc":"2.0","id":1,"error":{"code":-32002,"message":"Internal Error"}}'] act == '{"jsonrpc":"2.0","id":1,"error":{"code":-32002,"message":"Internal Error"}}'
}
def "Convert basic to JSON with string id"() {
setup:
def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
call.ids[1] = "aaa"
def data = [
BlockchainOuterClass.NativeCallReplyItem.newBuilder()
.setId(1)
.setSucceed(true)
.setPayload(ByteString.copyFrom('"0x98dbb1"', 'UTF-8'))
.build()
]
when:
def act = writer.toJson(call, data[0])
then:
act == '{"jsonrpc":"2.0","id":"aaa","result":"0x98dbb1"}'
} }
def "Convert few items to JSON"() { def "Convert few items to JSON"() {
@@ -151,17 +142,17 @@ class WriteRpcJsonSpec extends Specification {
BlockchainOuterClass.NativeCallReplyItem.newBuilder() BlockchainOuterClass.NativeCallReplyItem.newBuilder()
.setId(1) .setId(1)
.setSucceed(true) .setSucceed(true)
.setPayload(ByteString.copyFrom('{"jsonrpc": "2.0", "id": 1, "result": "0x98dbb1"}', 'UTF-8')) .setPayload(ByteString.copyFrom('"0x98dbb1"', 'UTF-8'))
.build(), .build(),
BlockchainOuterClass.NativeCallReplyItem.newBuilder() BlockchainOuterClass.NativeCallReplyItem.newBuilder()
.setId(2) .setId(2)
.setSucceed(true) .setSucceed(false)
.setPayload(ByteString.copyFrom('{"jsonrpc": "2.0", "id": 2, "error": {"code": -32001, "message": "oops"}}', 'UTF-8')) .setErrorMessage("oops")
.build(), .build(),
BlockchainOuterClass.NativeCallReplyItem.newBuilder() BlockchainOuterClass.NativeCallReplyItem.newBuilder()
.setId(3) .setId(3)
.setSucceed(true) .setSucceed(true)
.setPayload(ByteString.copyFrom('{"jsonrpc": "2.0", "id": 3, "result": {"hash": "0x2484f459dc"}}', 'UTF-8')) .setPayload(ByteString.copyFrom('{"hash": "0x2484f459dc"}', 'UTF-8'))
.build(), .build(),
] ]
when: when:
@@ -170,10 +161,9 @@ class WriteRpcJsonSpec extends Specification {
.collectList() .collectList()
.block(Duration.ofSeconds(1)) .block(Duration.ofSeconds(1))
then: then:
act == [ act.size() == 3
'{"jsonrpc":"2.0","id":10,"result":"0x98dbb1"}', act[0] == '{"jsonrpc":"2.0","id":10,"result":"0x98dbb1"}'
'{"jsonrpc":"2.0","id":11,"error":{"code":-32001,"message":"oops"}}', act[1] == '{"jsonrpc":"2.0","id":11,"error":{"code":-32002,"message":"oops"}}'
'{"jsonrpc":"2.0","id":15,"result":{"hash":"0x2484f459dc"}}' act[2] == '{"jsonrpc":"2.0","id":15,"result":{"hash": "0x2484f459dc"}}'
]
} }
} }

View File

@@ -15,10 +15,14 @@
*/ */
package io.emeraldpay.dshackle.upstream.rpcclient package io.emeraldpay.dshackle.upstream.rpcclient
import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.Global
import spock.lang.Specification import spock.lang.Specification
class JsonRpcResponseSpec extends Specification { class JsonRpcResponseSpec extends Specification {
ObjectMapper objectMapper = Global.objectMapper
def "Same responses are equal"() { def "Same responses are equal"() {
setup: setup:
def resp1 = new JsonRpcResponse("\"hello\"".bytes, null) def resp1 = new JsonRpcResponse("\"hello\"".bytes, null)
@@ -56,4 +60,78 @@ class JsonRpcResponseSpec extends Specification {
then: then:
act.isNull() act.isNull()
} }
def "Serialize int id and null result"() {
setup:
def json = new JsonRpcResponse("null".bytes, null, new JsonRpcResponse.IntId(1))
when:
def act = objectMapper.writeValueAsString(json)
then:
act == '{"jsonrpc":"2.0","id":1,"result":null}'
}
def "Serialize int id and string result"() {
setup:
def json = new JsonRpcResponse('"Hello World"'.bytes, null, new JsonRpcResponse.IntId(10))
when:
def act = objectMapper.writeValueAsString(json)
then:
act == '{"jsonrpc":"2.0","id":10,"result":"Hello World"}'
}
def "Serialize int id and object result"() {
setup:
def json = new JsonRpcResponse('{"foo": "Hello World", "bar": 1}'.bytes, null, new JsonRpcResponse.IntId(101))
when:
def act = objectMapper.writeValueAsString(json)
then:
act == '{"jsonrpc":"2.0","id":101,"result":{"foo": "Hello World", "bar": 1}}'
}
def "Serialize int id and error"() {
setup:
def json = new JsonRpcResponse(null, new JsonRpcResponse.ResponseError(-32041, "Oooops"), new JsonRpcResponse.IntId(101))
when:
def act = objectMapper.writeValueAsString(json)
then:
act == '{"jsonrpc":"2.0","id":101,"error":{"code":-32041,"message":"Oooops"}}'
}
def "Serialize string id and null result"() {
setup:
def json = new JsonRpcResponse("null".bytes, null, new JsonRpcResponse.StringId("asf01t1gg"))
when:
def act = objectMapper.writeValueAsString(json)
then:
act == '{"jsonrpc":"2.0","id":"asf01t1gg","result":null}'
}
def "Serialize string id and string result"() {
setup:
def json = new JsonRpcResponse('"Hello World"'.bytes, null, new JsonRpcResponse.StringId("10"))
when:
def act = objectMapper.writeValueAsString(json)
then:
act == '{"jsonrpc":"2.0","id":"10","result":"Hello World"}'
}
def "Serialize string id and object result"() {
setup:
def json = new JsonRpcResponse('{"foo": "Hello World", "bar": 1}'.bytes, null, new JsonRpcResponse.StringId("g8gk19g"))
when:
def act = objectMapper.writeValueAsString(json)
then:
act == '{"jsonrpc":"2.0","id":"g8gk19g","result":{"foo": "Hello World", "bar": 1}}'
}
def "Serialize string id and error"() {
setup:
def json = new JsonRpcResponse(null,
new JsonRpcResponse.ResponseError(-32041, "Oooops"),
new JsonRpcResponse.StringId("9kbo29gkaasf"))
when:
def act = objectMapper.writeValueAsString(json)
then:
act == '{"jsonrpc":"2.0","id":"9kbo29gkaasf","error":{"code":-32041,"message":"Oooops"}}'
}
} }