problem: doesn't pass error details from upstream

fix: #42
This commit is contained in:
Igor Artamonov
2020-08-10 22:18:54 -04:00
parent 2abe3023b6
commit b5daceeff5
30 changed files with 325 additions and 103 deletions

View File

@@ -117,6 +117,7 @@ dependencies {
testImplementation "io.projectreactor:reactor-test:$reactorVersion"
testImplementation 'org.objenesis:objenesis:3.1'
testImplementation 'org.mock-server:mockserver-netty:5.10'
testImplementation "nl.jqno.equalsverifier:equalsverifier:3.3"
}
compileKotlin {

View File

@@ -86,7 +86,7 @@ class ProxyServer(
.addAllItems(call.items)
.build()
val jsons = nativeCall
.nativeCall(Mono.just(request))
.nativeCallResult(Mono.just(request))
.transform(writeRpcJson.toJsons(call))
return if (call.type == ProxyCall.RpcType.SINGLE) {
jsons.next()

View File

@@ -42,7 +42,7 @@ open class WriteRpcJson() {
/**
* Convert Dshackle protobuf based responses to JSON RPC formatted as strings
*/
open fun toJsons(call: ProxyCall): Function<Flux<BlockchainOuterClass.NativeCallReplyItem>, Flux<String>> {
open fun toJsons(call: ProxyCall): Function<Flux<NativeCall.CallResult>, Flux<String>> {
return Function { flux ->
flux
.flatMap { response ->
@@ -70,19 +70,24 @@ open class WriteRpcJson() {
}
}
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))
open fun toJson(call: ProxyCall, response: NativeCall.CallResult): String? {
val id = call.ids[response.id]?.let {
JsonRpcResponse.Id.from(it)
} ?: return null;
val json = if (response.isError()) {
val error = response.error!!
error.upstreamError?.let { upstreamError ->
JsonRpcResponse.error(upstreamError, id)
} ?: JsonRpcResponse.error(-32002, error.message, id)
} else {
JsonRpcResponse.error(-32002, response.errorMessage, JsonRpcResponse.Id.from(id))
JsonRpcResponse.ok(response.result!!, id)
}
return objectMapper.writeValueAsString(json)
}
fun toJson(call: ProxyCall, error: NativeCall.CallFailure): String? {
val id = call.ids[error.id] ?: return null;
val json = JsonRpcResponse.error(-32002, error.reason.message ?: "", JsonRpcResponse.Id.from(id))
val json = JsonRpcResponse.error(-32003, error.reason.message ?: "", JsonRpcResponse.Id.from(id))
return objectMapper.writeValueAsString(json)
}

View File

@@ -18,12 +18,15 @@ package io.emeraldpay.dshackle.quorum
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
import io.infinitape.etherjar.rpc.RpcException
open class AlwaysQuorum: CallQuorum {
private var resolved = false
private var result: ByteArray? = null
private var rpcError: JsonRpcError? = null
override fun init(head: Head) {
}
@@ -42,10 +45,15 @@ open class AlwaysQuorum: CallQuorum {
return true
}
override fun record(error: RpcException, upstream: Upstream) {
override fun record(error: JsonRpcException, upstream: Upstream) {
this.rpcError = error.error
}
override fun getResult(): ByteArray? {
return result
}
override fun getError(): JsonRpcError? {
return rpcError
}
}

View File

@@ -18,6 +18,8 @@ package io.emeraldpay.dshackle.quorum
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
import io.infinitape.etherjar.domain.TransactionId
import io.infinitape.etherjar.rpc.RpcException
import io.infinitape.etherjar.rpc.json.BlockJson
@@ -34,8 +36,9 @@ interface CallQuorum {
fun isFailed(): Boolean
fun record(response: ByteArray, upstream: Upstream): Boolean
fun record(error: RpcException, upstream: Upstream)
fun record(error: JsonRpcException, upstream: Upstream)
fun getResult(): ByteArray?
fun getError(): JsonRpcError?
companion object {
fun untilResolved(cq: CallQuorum): Predicate<Any> {

View File

@@ -19,6 +19,7 @@ package io.emeraldpay.dshackle.quorum
import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
import io.infinitape.etherjar.rpc.JacksonRpcConverter
import io.infinitape.etherjar.rpc.RpcException
@@ -55,7 +56,7 @@ open class NonEmptyQuorum(
tries++
}
override fun record(error: RpcException, upstream: Upstream) {
override fun record(error: JsonRpcException, upstream: Upstream) {
tries++
}

View File

@@ -18,6 +18,8 @@ package io.emeraldpay.dshackle.quorum
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
import io.infinitape.etherjar.rpc.RpcException
import java.util.concurrent.atomic.AtomicReference
@@ -25,6 +27,7 @@ class NotLaggingQuorum(val maxLag: Long = 0): CallQuorum {
private val result: AtomicReference<ByteArray> = AtomicReference()
private val failed = AtomicReference(false)
private var rpcError: JsonRpcError? = null
override fun init(head: Head) {
}
@@ -46,7 +49,8 @@ class NotLaggingQuorum(val maxLag: Long = 0): CallQuorum {
return false
}
override fun record(error: RpcException, upstream: Upstream) {
override fun record(error: JsonRpcException, upstream: Upstream) {
this.rpcError = error.error
val lagging = upstream.getLag() > maxLag
if (!lagging && result.get() == null) {
failed.set(true)
@@ -56,4 +60,8 @@ class NotLaggingQuorum(val maxLag: Long = 0): CallQuorum {
override fun getResult(): ByteArray {
return result.get()
}
override fun getError(): JsonRpcError? {
return rpcError
}
}

View File

@@ -17,6 +17,7 @@ package io.emeraldpay.dshackle.quorum
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.ApiSource
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.infinitape.etherjar.rpc.RpcException
@@ -56,8 +57,10 @@ class QuorumRpcReader(
val defaultResult: Mono<Result> = Mono.just(quorum).flatMap { q ->
if (q.isFailed()) {
//TODO record and return actual error details
Mono.error<Result>(RpcException(-32000, "Upstream error"))
Mono.error<Result>(
q.getError()?.asException(JsonRpcResponse.IntId(1))
?: RpcException(-32000, "Unknown Upstream error")
)
} else {
log.warn("Empty result for ${key.method} as ${q}")
Mono.empty<Result>()
@@ -74,9 +77,14 @@ class QuorumRpcReader(
.flatMap { response ->
response.requireResult()
.onErrorResume { err ->
if (err is RpcException) {
if (err is RpcException || err is JsonRpcException) {
// on error notify quorum, it may use error message or other details
quorum.record(err, api)
val cleanErr: JsonRpcException = when (err) {
is RpcException -> JsonRpcException.from(err)
is JsonRpcException -> err
else -> throw IllegalStateException("Cannot convert from exception", err)
}
quorum.record(cleanErr, api)
// it it's failed after that, then we don't need more calls, stop api source
if (quorum.isFailed()) {
apis.resolve()

View File

@@ -19,6 +19,8 @@ package io.emeraldpay.dshackle.quorum
import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
import io.infinitape.etherjar.rpc.JacksonRpcConverter
import io.infinitape.etherjar.rpc.RpcException
import org.slf4j.LoggerFactory
@@ -28,6 +30,7 @@ abstract class ValueAwareQuorum<T>(
): CallQuorum {
private val log = LoggerFactory.getLogger(ValueAwareQuorum::class.java)
private var rpcError: JsonRpcError? = null
fun extractValue(response: ByteArray, clazz: Class<T>): T? {
return Global.objectMapper.readValue(response.inputStream(), clazz)
@@ -45,12 +48,16 @@ abstract class ValueAwareQuorum<T>(
return isResolved();
}
override fun record(error: RpcException, upstream: Upstream) {
recordError(null, error.rpcMessage, upstream)
override fun record(error: JsonRpcException, upstream: Upstream) {
this.rpcError = error.error
recordError(null, error.error.message, upstream)
}
abstract fun recordValue(response: ByteArray, responseValue: T?, upstream: Upstream)
abstract fun recordError(response: ByteArray?, errorMessage: String?, upstream: Upstream)
override fun getError(): JsonRpcError? {
return rpcError
}
}

View File

@@ -25,6 +25,8 @@ import io.emeraldpay.dshackle.upstream.*
import io.emeraldpay.dshackle.quorum.AlwaysQuorum
import io.emeraldpay.dshackle.quorum.CallQuorum
import io.emeraldpay.dshackle.quorum.QuorumReaderFactory
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.grpc.Chain
@@ -48,6 +50,12 @@ open class NativeCall(
var quorumReaderFactory: QuorumReaderFactory = QuorumReaderFactory.default()
open fun nativeCall(requestMono: Mono<BlockchainOuterClass.NativeCallRequest>): Flux<BlockchainOuterClass.NativeCallReplyItem> {
return nativeCallResult(requestMono)
.map(this::buildResponse)
.onErrorResume(this::processException)
}
open fun nativeCallResult(requestMono: Mono<BlockchainOuterClass.NativeCallRequest>): Flux<CallResult> {
return requestMono.flatMapMany(this::prepareCall)
.map(this::parseParams)
.parallel()
@@ -56,8 +64,6 @@ open class NativeCall(
.doOnError { e -> log.warn("Error during native call: ${e.message}") }
}
.sequential()
.map(this::buildResponse)
.onErrorResume(this::processException)
}
fun parseParams(it: CallContext<RawCallDetails>): CallContext<ParsedCallDetails> {
@@ -201,13 +207,14 @@ open class NativeCall(
open class CallFailure(val id: Int, val reason: Throwable) : Exception("Failed to call $id: ${reason.message}")
open class CallError(val id: Int, val message: String) {
open class CallError(val id: Int, val message: String, val upstreamError: JsonRpcError?) {
companion object {
fun from(t: Throwable): CallError {
return when (t) {
is RpcException -> CallError(t.code, t.rpcMessage)
is CallFailure -> CallError(t.id, t.reason.message ?: "Upstream Error")
else -> CallError(1, t.message ?: "Upstream Error")
is JsonRpcException -> CallError(t.id.asInt(), t.error.message, t.error)
is RpcException -> CallError(t.code, t.rpcMessage, null)
is CallFailure -> CallError(t.id, t.reason.message ?: "Upstream Error", null)
else -> CallError(1, t.message ?: "Upstream Error", null)
}
}
}
@@ -220,7 +227,7 @@ open class NativeCall(
}
fun fail(id: Int, errorCore: Int, errorMessage: String): CallResult {
return CallResult(id, null, CallError(errorCore, errorMessage))
return CallResult(id, null, CallError(errorCore, errorMessage, null))
}
fun fail(id: Int, error: Throwable): CallResult {

View File

@@ -56,7 +56,7 @@ class EthereumRpcHead(
.timeout(Defaults.timeout, Mono.error(Exception("Block number not received")))
.flatMap {
if (it.error != null) {
Mono.error(it.error.asException())
Mono.error(it.error.asException(null))
} else {
val value = it.getResultAsProcessedString()
Mono.just(HexQuantity.from(value))

View File

@@ -0,0 +1,56 @@
/**
* Copyright (c) 2020 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream.rpcclient
import io.infinitape.etherjar.rpc.RpcException
class JsonRpcError(val code: Int, val message: String, val details: Any?) {
constructor(code: Int, message: String) : this(code, message, null)
companion object {
@JvmStatic
fun from(err: RpcException): JsonRpcError {
return JsonRpcError(
err.code, err.rpcMessage, err.details
)
}
}
fun asException(id: JsonRpcResponse.Id?): JsonRpcException {
return JsonRpcException(id ?: JsonRpcResponse.IntId(-1), this)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is JsonRpcError) return false
if (code != other.code) return false
if (message != other.message) return false
if (details != other.details) return false
return true
}
override fun hashCode(): Int {
var result = code
result = 31 * result + message.hashCode()
result = 31 * result + (details?.hashCode() ?: 0)
return result
}
}

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2020 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream.rpcclient
import io.infinitape.etherjar.rpc.RpcException
class JsonRpcException(
val id: JsonRpcResponse.Id,
val error: JsonRpcError
) : Exception(error.message) {
constructor(id: Int, message: String) : this(JsonRpcResponse.IntId(id), JsonRpcError(-32005, message))
companion object {
fun from(err: RpcException): JsonRpcException {
val id = err.details?.let {
if (it is JsonRpcResponse.Id) {
it
} else {
JsonRpcResponse.IntId(-3)
}
} ?: JsonRpcResponse.IntId(-4)
return JsonRpcException(
id, JsonRpcError.from(err)
)
}
}
}

View File

@@ -88,7 +88,9 @@ class JsonRpcHttpClient(
return response.response { header, bytes ->
if (header.status().code() != 200) {
Mono.error(RpcException(RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE, "HTTP Code: ${header.status().code()}"))
Mono.error(JsonRpcException(JsonRpcResponse.IntId(-2),
JsonRpcError(RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE, "HTTP Code: ${header.status().code()}"))
)
} else {
bytes.aggregate().asByteArray()
}
@@ -101,10 +103,10 @@ class JsonRpcHttpClient(
.flatMap(this@JsonRpcHttpClient::execute)
.map(parser::parse)
.onErrorResume { t ->
val err = if (t is RpcException) {
JsonRpcResponse.error(t.code, t.rpcMessage)
} else {
JsonRpcResponse.error(1, t.message ?: t.javaClass.name)
val err = when (t) {
is RpcException -> JsonRpcResponse.error(t.code, t.rpcMessage)
is JsonRpcException -> JsonRpcResponse.error(t.error, JsonRpcResponse.IntId(1))
else -> JsonRpcResponse.error(1, t.message ?: t.javaClass.name)
}
Mono.just(err)
}

View File

@@ -19,6 +19,7 @@ import com.fasterxml.jackson.core.JsonFactory
import com.fasterxml.jackson.core.JsonParseException
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.JsonToken
import io.emeraldpay.dshackle.Global
import io.infinitape.etherjar.rpc.RpcResponseError
import org.slf4j.LoggerFactory
@@ -35,14 +36,14 @@ class JsonRpcParser() {
val parser: JsonParser = jsonFactory.createParser(json)
parser.nextToken()
if (parser.currentToken != JsonToken.START_OBJECT) {
return JsonRpcResponse(null, JsonRpcResponse.ResponseError(RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE, "Invalid JSON"))
return JsonRpcResponse(null, JsonRpcError(RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE, "Invalid JSON"))
}
var nullResponse: JsonRpcResponse? = null
while (parser.nextToken() != JsonToken.END_OBJECT) {
val field = parser.currentName
if (field == "jsonrpc" || field == "id") {
if (!parser.nextToken().isScalarValue) {
return JsonRpcResponse(null, JsonRpcResponse.ResponseError(RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE, "Invalid JSON (id/type)"))
return JsonRpcResponse(null, JsonRpcError(RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE, "Invalid JSON (id or jsonrpc value)"))
}
// just skip the field
} else if (field == "result") {
@@ -78,12 +79,13 @@ class JsonRpcParser() {
} catch (e: JsonParseException) {
log.warn("Failed to parse JSON from upstream: ${e.message}")
}
return JsonRpcResponse(null, JsonRpcResponse.ResponseError(RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE, "Invalid JSON structure"))
return JsonRpcResponse(null, JsonRpcError(RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE, "Invalid JSON structure"))
}
fun readError(parser: JsonParser): JsonRpcResponse.ResponseError? {
fun readError(parser: JsonParser): JsonRpcError? {
var code = 0
var message = ""
var details: Any? = null
while (parser.nextToken() != JsonToken.END_OBJECT) {
if (parser.currentToken() == JsonToken.VALUE_NULL) {
@@ -95,9 +97,15 @@ class JsonRpcParser() {
code = parser.intValue
} else if (field == "message" && parser.currentToken == JsonToken.VALUE_STRING) {
message = parser.valueAsString
} else if (field == "data") {
when (val value = parser.nextToken()) {
JsonToken.VALUE_NULL -> details = null
JsonToken.VALUE_STRING -> details = parser.valueAsString
JsonToken.START_OBJECT -> details = Global.objectMapper.readValue(parser, java.util.Map::class.java)
else -> log.warn("Unsupported error data type $value")
}
}
}
return JsonRpcResponse.ResponseError(code, message)
return JsonRpcError(code, message, details)
}
}

View File

@@ -18,16 +18,15 @@ package io.emeraldpay.dshackle.upstream.rpcclient
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.SerializerProvider
import io.infinitape.etherjar.rpc.RpcException
import reactor.core.publisher.Mono
class JsonRpcResponse(
private val result: ByteArray?,
val error: ResponseError?,
val error: JsonRpcError?,
val id: Id
) {
constructor(result: ByteArray?, error: ResponseError?) : this(result, error, IntId(0))
constructor(result: ByteArray?, error: JsonRpcError?) : this(result, error, IntId(0))
companion object {
private val NULL_VALUE = "null".toByteArray()
@@ -49,12 +48,17 @@ class JsonRpcResponse(
@JvmStatic
fun error(code: Int, msg: String): JsonRpcResponse {
return JsonRpcResponse(null, ResponseError(code, msg))
return JsonRpcResponse(null, JsonRpcError(code, msg))
}
@JvmStatic
fun error(error: JsonRpcError, id: Id): JsonRpcResponse {
return JsonRpcResponse(null, error, id)
}
@JvmStatic
fun error(code: Int, msg: String, id: Id): JsonRpcResponse {
return JsonRpcResponse(null, ResponseError(code, msg), id)
return JsonRpcResponse(null, JsonRpcError(code, msg), id)
}
}
@@ -88,7 +92,7 @@ class JsonRpcResponse(
fun requireResult(): Mono<ByteArray> {
return if (error != null) {
Mono.error(error.asException())
Mono.error(error.asException(id))
} else {
Mono.just(getResult())
}
@@ -96,7 +100,7 @@ class JsonRpcResponse(
fun requireStringResult(): Mono<String> {
return if (error != null) {
Mono.error(error.asException())
Mono.error(error.asException(id))
} else {
Mono.just(getResultAsProcessedString())
}
@@ -121,12 +125,6 @@ class JsonRpcResponse(
return result1
}
class ResponseError(val code: Int, val message: String) {
fun asException(): RpcException {
return RpcException(code, message)
}
}
/**
* JSON RPC wrapper. Makes sure that the id is either Int or String
*/
@@ -220,6 +218,13 @@ class JsonRpcResponse(
gen.writeObjectFieldStart("error")
gen.writeNumberField("code", value.error.code)
gen.writeStringField("message", value.error.message)
value.error.details?.let { details ->
when (details) {
is String -> gen.writeStringField("data", details)
is Number -> gen.writeNumberField("data", details.toInt())
else -> gen.writeObjectField("data", details)
}
}
gen.writeEndObject()
} else {
if (value.result == null) {

View File

@@ -62,7 +62,7 @@ class ProxyServerSpec extends Specification {
def act = server.execute(Common.ChainRef.CHAIN_ETHEREUM, call)
then:
1 * nativeCall.nativeCall(_) >> Flux.just(BlockchainOuterClass.NativeCallReplyItem.newBuilder().build())
1 * nativeCall.nativeCallResult(_) >> Flux.just(new NativeCall.CallResult(1, "".bytes, null))
StepVerifier.create(act)
.expectNext("hello")
.expectComplete()

View File

@@ -18,6 +18,7 @@ package io.emeraldpay.dshackle.proxy
import com.google.protobuf.ByteString
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.rpc.NativeCall
import io.emeraldpay.dshackle.test.TestingCommons
import reactor.core.publisher.Flux
import spock.lang.Specification
@@ -86,11 +87,7 @@ class WriteRpcJsonSpec extends Specification {
def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
call.ids[1] = 105
def data = [
BlockchainOuterClass.NativeCallReplyItem.newBuilder()
.setId(1)
.setSucceed(true)
.setPayload(ByteString.copyFrom('"0x98dbb1"', 'UTF-8'))
.build()
new NativeCall.CallResult(1, '"0x98dbb1"'.bytes, null)
]
when:
def act = writer.toJson(call, data[0])
@@ -103,11 +100,7 @@ class WriteRpcJsonSpec extends Specification {
def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
call.ids[1] = 1
def data = [
BlockchainOuterClass.NativeCallReplyItem.newBuilder()
.setId(1)
.setSucceed(false)
.setErrorMessage("Internal Error")
.build()
new NativeCall.CallResult(1, null, new NativeCall.CallError(1, "Internal Error", null))
]
when:
def act = writer.toJson(call, data[0])
@@ -120,11 +113,7 @@ class WriteRpcJsonSpec extends Specification {
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()
new NativeCall.CallResult(1, '"0x98dbb1"'.bytes, null)
]
when:
def act = writer.toJson(call, data[0])
@@ -139,21 +128,9 @@ class WriteRpcJsonSpec extends Specification {
call.ids[2] = 11
call.ids[3] = 15
def data = [
BlockchainOuterClass.NativeCallReplyItem.newBuilder()
.setId(1)
.setSucceed(true)
.setPayload(ByteString.copyFrom('"0x98dbb1"', 'UTF-8'))
.build(),
BlockchainOuterClass.NativeCallReplyItem.newBuilder()
.setId(2)
.setSucceed(false)
.setErrorMessage("oops")
.build(),
BlockchainOuterClass.NativeCallReplyItem.newBuilder()
.setId(3)
.setSucceed(true)
.setPayload(ByteString.copyFrom('{"hash": "0x2484f459dc"}', 'UTF-8'))
.build(),
new NativeCall.CallResult(1, '"0x98dbb1"'.bytes, null),
new NativeCall.CallResult(2, null, new NativeCall.CallError(2, "oops", null)),
new NativeCall.CallResult(3, '{"hash": "0x2484f459dc"}'.bytes, null),
]
when:
def act = Flux.fromIterable(data)

View File

@@ -22,6 +22,7 @@ import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.quorum.BroadcastQuorum
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
import io.infinitape.etherjar.rpc.RpcException
import spock.lang.Specification
@@ -54,7 +55,7 @@ class BroadcastQuorumSpec extends Specification {
1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _)
when:
q.record(new RpcException(1, "Nonce too low"), upstream3)
q.record(new JsonRpcException(1, "Nonce too low"), upstream3)
then:
1 * q.recordError(_, _, _)
q.isResolved()
@@ -74,7 +75,7 @@ class BroadcastQuorumSpec extends Specification {
!q.isResolved()
when:
q.record(new RpcException(1, "Internal error"), upstream1)
q.record(new JsonRpcException(1, "Internal error"), upstream1)
then:
!q.isResolved()
1 * q.recordError(_, _, _)
@@ -86,7 +87,7 @@ class BroadcastQuorumSpec extends Specification {
1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _)
when:
q.record(new RpcException(1, "Nonce too low"), upstream3)
q.record(new JsonRpcException(1, "Nonce too low"), upstream3)
then:
1 * q.recordError(_, _, _)
q.isResolved()

View File

@@ -18,6 +18,7 @@ package io.emeraldpay.dshackle.quorum
import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
import io.infinitape.etherjar.rpc.RpcException
import spock.lang.Specification
@@ -37,19 +38,19 @@ class NonEmptyQuorumSpec extends Specification {
!q.isFailed()
when:
q.record(new RpcException(1, "Internal"), upstream1)
q.record(new JsonRpcException(1, "Internal"), upstream1)
then:
!q.isResolved()
!q.isFailed()
when:
q.record(new RpcException(1, "Internal"), upstream2)
q.record(new JsonRpcException(1, "Internal"), upstream2)
then:
!q.isResolved()
!q.isFailed()
when:
q.record(new RpcException(1, "Internal"), upstream3)
q.record(new JsonRpcException(1, "Internal"), upstream3)
then:
q.isFailed()
!q.isResolved()
@@ -89,7 +90,7 @@ class NonEmptyQuorumSpec extends Specification {
!q.isFailed()
when:
q.record(new RpcException(1, "Internal"), upstream1)
q.record(new JsonRpcException(1, "Internal"), upstream1)
then:
!q.isFailed()
!q.isResolved()

View File

@@ -22,6 +22,7 @@ import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.quorum.NonceQuorum
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
import io.infinitape.etherjar.rpc.RpcException
import spock.lang.Specification
@@ -74,7 +75,7 @@ class NonceQuorumSpec extends Specification {
!q.isResolved()
when:
q.record(new RpcException(1, "Internal"), upstream1)
q.record(new JsonRpcException(1, "Internal"), upstream1)
then:
!q.isResolved()
1 * q.recordError(_, _, _)
@@ -113,21 +114,23 @@ class NonceQuorumSpec extends Specification {
!q.isFailed()
when:
q.record(new RpcException(1, "Internal"), upstream1)
q.record(new JsonRpcException(1, "Internal"), upstream1)
then:
!q.isResolved()
!q.isFailed()
when:
q.record(new RpcException(1, "Internal"), upstream2)
q.record(new JsonRpcException(1, "Internal"), upstream2)
then:
!q.isResolved()
!q.isFailed()
when:
q.record(new RpcException(1, "Internal"), upstream3)
q.record(new JsonRpcException(1, "Internal"), upstream3)
then:
q.isFailed()
!q.isResolved()
q.getError() != null
q.getError().message == "Internal"
}
}

View File

@@ -18,6 +18,7 @@ package io.emeraldpay.dshackle.quorum
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.quorum.NotLaggingQuorum
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
import io.infinitape.etherjar.rpc.RpcException
import spock.lang.Specification
@@ -74,7 +75,7 @@ class NotLaggingQuorumSpec extends Specification {
def quorum = new NotLaggingQuorum(1)
when:
quorum.record(new RpcException(-100, "test error"), up)
quorum.record(new JsonRpcException(-100, "test error"), up)
then:
1 * up.getLag() >> 1
!quorum.isResolved()

View File

@@ -21,6 +21,7 @@ import com.google.protobuf.ByteString
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.grpc.stub.StreamObserver
@@ -60,7 +61,7 @@ class EthereumApiMock implements Reader<JsonRpcRequest, JsonRpcResponse> {
Callable<JsonRpcResponse> call = {
def predefined = predefined.find { it.isSame(request.method, request.params) }
byte[] result = null
JsonRpcResponse.ResponseError error = null
JsonRpcError error = null
if (predefined != null) {
if (predefined.exception != null) {
predefined.onCalled()
@@ -69,7 +70,7 @@ class EthereumApiMock implements Reader<JsonRpcRequest, JsonRpcResponse> {
}
if (predefined.result instanceof RpcResponseError) {
((RpcResponseError) predefined.result).with { err ->
error = new JsonRpcResponse.ResponseError(err.code, err.message)
error = new JsonRpcError(err.code, err.message)
}
} else {
// ResponseJson json = new ResponseJson<Object, Integer>(id: 1, result: predefined.result)
@@ -79,7 +80,7 @@ class EthereumApiMock implements Reader<JsonRpcRequest, JsonRpcResponse> {
predefined.print()
} else {
log.error("Method ${request.method} with ${request.params} is not mocked")
error = new JsonRpcResponse.ResponseError(-32601, "Method ${request.method} with ${request.params} is not mocked")
error = new JsonRpcError(-32601, "Method ${request.method} with ${request.params} is not mocked")
}
return new JsonRpcResponse(result, error)
} as Callable<JsonRpcResponse>

View File

@@ -0,0 +1,34 @@
package io.emeraldpay.dshackle.upstream.rpcclient
import io.infinitape.etherjar.rpc.RpcException
import nl.jqno.equalsverifier.EqualsVerifier
import nl.jqno.equalsverifier.Warning
import spock.lang.Specification
class JsonRpcErrorSpec extends Specification {
def "Build from RpcException"() {
when:
def act = JsonRpcError.from(new RpcException(-32123, "test test"))
then:
act.code == -32123
act.message == "test test"
act.details == null
}
def "Build from RpcException with details"() {
when:
def act = JsonRpcError.from(new RpcException(-32123, "test test", "foo bar"))
then:
act.code == -32123
act.message == "test test"
act.details == "foo bar"
}
def "Equals"() {
when:
def v = EqualsVerifier.forClass(JsonRpcError)
then:
v.verify()
}
}

View File

@@ -106,7 +106,7 @@ class JsonRpcHttpClientSpec extends Specification {
then:
StepVerifier.create(act)
.expectErrorMatches { t ->
t instanceof RpcException && t.code == RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE
t instanceof JsonRpcException && t.error.code == RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE
}
.verify(Duration.ofSeconds(1))
}

View File

@@ -178,6 +178,36 @@ class JsonRpcParserSpec extends Specification {
!act.hasResult()
}
def "Parse error with data"() {
setup:
// 0 8 16 32
def json = '{"jsonrpc": "2.0", "id": 1, "result": null, "error": {"code": -1111, "message": "test", "data": "just data"}}'
when:
def act = parser.parse(json.getBytes())
then:
act.error != null
act.error.code == -1111
act.error.message == "test"
act.error.details == "just data"
act.hasError()
!act.hasResult()
}
def "Parse error with data struct"() {
setup:
// 0 8 16 32
def json = '{"jsonrpc": "2.0", "id": 1, "result": null, "error": {"code": -1111, "message": "test", "data": {"foo": "just data", "bar": 1}}}'
when:
def act = parser.parse(json.getBytes())
then:
act.error != null
act.error.code == -1111
act.error.message == "test"
act.error.details == [foo: "just data", bar: 1]
act.hasError()
!act.hasResult()
}
def "Handle non-json with producing an error response"() {
setup:
def json = 'NOT JSON'

View File

@@ -90,7 +90,7 @@ class JsonRpcResponseSpec extends Specification {
def "Serialize int id and error"() {
setup:
def json = new JsonRpcResponse(null, new JsonRpcResponse.ResponseError(-32041, "Oooops"), new JsonRpcResponse.IntId(101))
def json = new JsonRpcResponse(null, new JsonRpcError(-32041, "Oooops"), new JsonRpcResponse.IntId(101))
when:
def act = objectMapper.writeValueAsString(json)
then:
@@ -127,7 +127,7 @@ class JsonRpcResponseSpec extends Specification {
def "Serialize string id and error"() {
setup:
def json = new JsonRpcResponse(null,
new JsonRpcResponse.ResponseError(-32041, "Oooops"),
new JsonRpcError(-32041, "Oooops"),
new JsonRpcResponse.StringId("9kbo29gkaasf"))
when:
def act = objectMapper.writeValueAsString(json)

View File

@@ -8,19 +8,21 @@ interface CallHandler {
private Object result
private Integer errorCode
private String errorMessage
private Object errorDetails
Result(Object result, Integer errorCode, String errorMessage) {
Result(Object result, Integer errorCode, String errorMessage, Object errorDetails) {
this.result = result
this.errorCode = errorCode
this.errorMessage = errorMessage
this.errorDetails = errorDetails
}
static Result ok(Object result) {
return new Result(result, null, null)
return new Result(result, null, null, null)
}
static Result error(int errorCode, String errorMessage) {
return new Result(null, errorCode, errorMessage)
static Result error(int errorCode, String errorMessage, Object details = null) {
return new Result(null, errorCode, errorMessage, details)
}
boolean isResult() {
@@ -38,5 +40,9 @@ interface CallHandler {
String getErrorMessage() {
return errorMessage
}
Object getErrorDetails() {
return errorDetails
}
}
}

View File

@@ -44,6 +44,9 @@ class SimpleUpstream {
code : result.getErrorCode(),
message: result.getErrorMessage()
]
if (result.getErrorDetails() != null) {
resultJson["error"]["data"] = result.getErrorDetails()
}
}
resp.status(200)
resp.header("content-type", "application/json")

View File

@@ -19,6 +19,11 @@ class TestcaseHandler implements CallHandler {
&& params[0].to?.toLowerCase() == "0x542156d51D10Db5acCB99f9Db7e7C91B74E80a2c".toLowerCase()) {
return Result.error(-32015, "VM execution error.")
}
// https://github.com/emeraldpay/dshackle/issues/35 (second)
if (method == "eth_call"
&& params[0].to?.toLowerCase() == "0x8ee2a5aca4f88cb8c757b8593d0734855dcc0eba".toLowerCase()) {
return Result.error(-32015, "VM execution error.", "revert: SafeMath: division by zero")
}
// https://github.com/emeraldpay/dshackle/issues/43
if (method == "debug_traceTransaction"
&& params[0].toLowerCase() == "0xd949bc0fe1a5d16f4522bc47933554dcc4ada0493ff71ee1973b2410257af9fe".toLowerCase()) {