problem: single invalid response from upstream breaks whole batch

solution: handle http and json errors and convert to RPC errors
This commit is contained in:
Igor Artamonov
2020-05-25 18:30:55 -04:00
parent d7bb121ef0
commit 5584858e44
4 changed files with 97 additions and 44 deletions

View File

@@ -15,9 +15,10 @@
*/ */
package io.emeraldpay.dshackle.upstream.rpcclient package io.emeraldpay.dshackle.upstream.rpcclient
import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.config.AuthConfig import io.emeraldpay.dshackle.config.AuthConfig
import io.emeraldpay.dshackle.reader.Reader import io.emeraldpay.dshackle.reader.Reader
import io.infinitape.etherjar.rpc.RpcException
import io.infinitape.etherjar.rpc.RpcResponseError
import io.netty.buffer.Unpooled import io.netty.buffer.Unpooled
import io.netty.handler.codec.http.HttpHeaderNames import io.netty.handler.codec.http.HttpHeaderNames
import io.netty.handler.codec.http.HttpHeaders import io.netty.handler.codec.http.HttpHeaders
@@ -25,7 +26,6 @@ import io.netty.handler.ssl.SslContextBuilder
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import reactor.netty.http.client.HttpClient import reactor.netty.http.client.HttpClient
import reactor.netty.tcp.SslProvider
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.security.KeyStore import java.security.KeyStore
import java.security.cert.CertificateFactory import java.security.cert.CertificateFactory
@@ -86,9 +86,13 @@ class JsonRpcHttpClient(
.uri(target) .uri(target)
.send(Mono.just(request).map { Unpooled.wrappedBuffer(it) }) .send(Mono.just(request).map { Unpooled.wrappedBuffer(it) })
return response.responseContent() return response.response { header, bytes ->
.aggregate() if (header.status().code() != 200) {
.asByteArray() Mono.error(RpcException(RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE, "HTTP Code: ${header.status().code()}"))
} else {
bytes.aggregate().asByteArray()
}
}.single()
} }
override fun read(key: JsonRpcRequest): Mono<JsonRpcResponse> { override fun read(key: JsonRpcRequest): Mono<JsonRpcResponse> {
@@ -96,5 +100,13 @@ class JsonRpcHttpClient(
.map(JsonRpcRequest::toJson) .map(JsonRpcRequest::toJson)
.flatMap(this@JsonRpcHttpClient::execute) .flatMap(this@JsonRpcHttpClient::execute)
.map(parser::parse) .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)
}
Mono.just(err)
}
} }
} }

View File

@@ -16,6 +16,7 @@
package io.emeraldpay.dshackle.upstream.rpcclient package io.emeraldpay.dshackle.upstream.rpcclient
import com.fasterxml.jackson.core.JsonFactory import com.fasterxml.jackson.core.JsonFactory
import com.fasterxml.jackson.core.JsonParseException
import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.JsonToken import com.fasterxml.jackson.core.JsonToken
import io.infinitape.etherjar.rpc.RpcResponseError import io.infinitape.etherjar.rpc.RpcResponseError
@@ -30,48 +31,52 @@ class JsonRpcParser() {
private val jsonFactory = JsonFactory() private val jsonFactory = JsonFactory()
fun parse(json: ByteArray): JsonRpcResponse { fun parse(json: ByteArray): JsonRpcResponse {
val parser: JsonParser = jsonFactory.createParser(json) try {
parser.nextToken() val parser: JsonParser = jsonFactory.createParser(json)
if (parser.currentToken != JsonToken.START_OBJECT) { parser.nextToken()
return JsonRpcResponse(null, JsonRpcResponse.ResponseError(RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE, "Invalid JSON")) if (parser.currentToken != JsonToken.START_OBJECT) {
} return JsonRpcResponse(null, JsonRpcResponse.ResponseError(RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE, "Invalid JSON"))
var nullResponse: JsonRpcResponse? = null }
while (parser.nextToken() != JsonToken.END_OBJECT) { var nullResponse: JsonRpcResponse? = null
val field = parser.currentName while (parser.nextToken() != JsonToken.END_OBJECT) {
if (field == "jsonrpc" || field == "id") { val field = parser.currentName
if (!parser.nextToken().isScalarValue) { if (field == "jsonrpc" || field == "id") {
return JsonRpcResponse(null, JsonRpcResponse.ResponseError(RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE, "Invalid JSON (id/type)")) if (!parser.nextToken().isScalarValue) {
} return JsonRpcResponse(null, JsonRpcResponse.ResponseError(RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE, "Invalid JSON (id/type)"))
// just skip the field }
} else if (field == "result") { // just skip the field
val value = parser.nextToken() } else if (field == "result") {
val start = parser.tokenLocation val value = parser.nextToken()
if (value.isScalarValue) { val start = parser.tokenLocation
val text = parser.text if (value.isScalarValue) {
if (value == JsonToken.VALUE_STRING) { val text = parser.text
return JsonRpcResponse(("\"" + text + "\"").toByteArray(), null) if (value == JsonToken.VALUE_STRING) {
} else if (value == JsonToken.VALUE_NULL) { return JsonRpcResponse(("\"" + text + "\"").toByteArray(), null)
//if null we should check if error is present } else if (value == JsonToken.VALUE_NULL) {
nullResponse = JsonRpcResponse(text.toByteArray(), null) //if null we should check if error is present
} else { nullResponse = JsonRpcResponse(text.toByteArray(), null)
return JsonRpcResponse(text.toByteArray(), null) } else {
return JsonRpcResponse(text.toByteArray(), null)
}
} else if (value == JsonToken.START_OBJECT || value == JsonToken.START_ARRAY) {
parser.skipChildren()
val end = parser.currentLocation.byteOffset.toInt()
val copy = ByteArray((end - start.byteOffset).toInt())
System.arraycopy(json, start.byteOffset.toInt(), copy, 0, copy.size)
return JsonRpcResponse(copy, null)
}
} else if (field == "error") {
val err = readError(parser)
if (err != null) {
return JsonRpcResponse(null, err)
} }
} else if (value == JsonToken.START_OBJECT || value == JsonToken.START_ARRAY) {
parser.skipChildren()
val end = parser.currentLocation.byteOffset.toInt()
val copy = ByteArray((end - start.byteOffset).toInt())
System.arraycopy(json, start.byteOffset.toInt(), copy, 0, copy.size)
return JsonRpcResponse(copy, null)
}
} else if (field == "error") {
val err = readError(parser)
if (err != null) {
return JsonRpcResponse(null, err)
} }
} }
} if (nullResponse != null) {
if (nullResponse != null) { return nullResponse
return nullResponse }
} 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, JsonRpcResponse.ResponseError(RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE, "Invalid JSON structure"))
} }

View File

@@ -17,6 +17,8 @@ package io.emeraldpay.dshackle.upstream.rpcclient
import io.emeraldpay.dshackle.config.AuthConfig import io.emeraldpay.dshackle.config.AuthConfig
import io.emeraldpay.dshackle.test.TestingCommons import io.emeraldpay.dshackle.test.TestingCommons
import io.infinitape.etherjar.rpc.RpcException
import io.infinitape.etherjar.rpc.RpcResponseError
import org.mockserver.integration.ClientAndServer import org.mockserver.integration.ClientAndServer
import org.mockserver.model.HttpRequest import org.mockserver.model.HttpRequest
import org.mockserver.model.HttpResponse import org.mockserver.model.HttpResponse
@@ -88,4 +90,25 @@ class JsonRpcHttpClientSpec extends Specification {
) )
} }
def "Produces RPC Exception on error status code"() {
setup:
def client = new JsonRpcHttpClient("localhost:18332", null, null)
mockServer.when(
HttpRequest.request()
).respond(
HttpResponse.response()
.withStatusCode(500)
.withBody("pong")
)
when:
def act = client.execute("ping".bytes).map { new String(it) }
then:
StepVerifier.create(act)
.expectErrorMatches { t ->
t instanceof RpcException && t.code == RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE
}
.verify(Duration.ofSeconds(1))
}
} }

View File

@@ -15,6 +15,7 @@
*/ */
package io.emeraldpay.dshackle.upstream.rpcclient package io.emeraldpay.dshackle.upstream.rpcclient
import io.infinitape.etherjar.rpc.RpcResponseError
import spock.lang.Specification import spock.lang.Specification
class JsonRpcParserSpec extends Specification { class JsonRpcParserSpec extends Specification {
@@ -177,4 +178,16 @@ class JsonRpcParserSpec extends Specification {
!act.hasResult() !act.hasResult()
} }
def "Handle non-json with producing an error response"() {
setup:
def json = 'NOT JSON'
when:
def act = parser.parse(json.getBytes())
then:
act.error != null
act.error.code == RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE
act.hasError()
!act.hasResult()
}
} }