diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/stream/JsonRpcStreamParser.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/stream/JsonRpcStreamParser.kt index 21ff205f..61b665cf 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/stream/JsonRpcStreamParser.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/stream/JsonRpcStreamParser.kt @@ -6,6 +6,7 @@ import com.fasterxml.jackson.core.JsonToken import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError import io.emeraldpay.dshackle.upstream.rpcclient.ResponseRpcParser import io.emeraldpay.etherjar.rpc.RpcResponseError +import org.slf4j.LoggerFactory import reactor.core.publisher.Flux import reactor.core.publisher.Mono import reactor.netty.ByteBufFlux @@ -16,6 +17,8 @@ import java.util.concurrent.atomic.AtomicReference class JsonRpcStreamParser { companion object { + private val log = LoggerFactory.getLogger(JsonRpcStreamParser::class.java) + private val jsonFactory = JsonFactory() private val responseRpcParser = ResponseRpcParser() @@ -29,7 +32,7 @@ class JsonRpcStreamParser { fun streamParse(statusCode: Int, response: Flux): Mono { return response.switchOnFirst({ first, responseStream -> - if (first.get() == null) { + if (first.get() == null || statusCode != 200) { aggregateResponse(responseStream, statusCode) } else { val whatCount = AtomicReference() @@ -141,48 +144,72 @@ class JsonRpcStreamParser { endStream: AtomicBoolean, whatCount: AtomicReference, ): SingleResponse? { - jsonFactory.createParser(firstBytes).use { parser -> - while (true) { - parser.nextToken() - if (firstBytes.size == parser.currentLocation.byteOffset.toInt()) { - break - } - if (parser.currentName != null) { - if (parser.currentName == "result") { - val token = parser.nextToken() - val tokenStart = parser.tokenLocation.byteOffset.toInt() - return if (token.isScalarValue) { - val count = CountSlashes(AtomicInteger(1)) - whatCount.set(count) - SingleResponse(processScalarValue(parser, tokenStart, firstBytes, count, endStream), null) - } else { - when (token) { - JsonToken.START_OBJECT -> { - val count = CountObjectBrackets(AtomicInteger(1)) - whatCount.set(count) - SingleResponse( - processAndCountBrackets(tokenStart, firstBytes, count.count, endStream, OBJECT_OPEN_BRACKET, OBJECT_CLOSE_BRACKET), - null, - ) - } - JsonToken.START_ARRAY -> { - val count = CountArrayBrackets(AtomicInteger(1)) - whatCount.set(count) - SingleResponse( - processAndCountBrackets(tokenStart, firstBytes, count.count, endStream, ARRAY_OPEN_BRACKET, ARRAY_CLOSE_BRACKET), - null, - ) - } - else -> { - throw IllegalStateException("'result' not an object nor array'") + try { + jsonFactory.createParser(firstBytes).use { parser -> + while (true) { + parser.nextToken() + if (firstBytes.size == parser.currentLocation.byteOffset.toInt()) { + break + } + if (parser.currentName != null) { + if (parser.currentName == "result") { + val token = parser.nextToken() + val tokenStart = parser.tokenLocation.byteOffset.toInt() + return if (token.isScalarValue) { + val count = CountSlashes(AtomicInteger(1)) + whatCount.set(count) + SingleResponse( + processScalarValue(parser, tokenStart, firstBytes, count, endStream), + null, + ) + } else { + when (token) { + JsonToken.START_OBJECT -> { + val count = CountObjectBrackets(AtomicInteger(1)) + whatCount.set(count) + SingleResponse( + processAndCountBrackets( + tokenStart, + firstBytes, + count.count, + endStream, + OBJECT_OPEN_BRACKET, + OBJECT_CLOSE_BRACKET, + ), + null, + ) + } + + JsonToken.START_ARRAY -> { + val count = CountArrayBrackets(AtomicInteger(1)) + whatCount.set(count) + SingleResponse( + processAndCountBrackets( + tokenStart, + firstBytes, + count.count, + endStream, + ARRAY_OPEN_BRACKET, + ARRAY_CLOSE_BRACKET, + ), + null, + ) + } + + else -> { + throw IllegalStateException("'result' not an object nor array'") + } } } + } else if (parser.currentName == "error") { + return SingleResponse(null, responseRpcParser.readError(parser)) } - } else if (parser.currentName == "error") { - return SingleResponse(null, responseRpcParser.readError(parser)) } } + return null } + } catch (e: Exception) { + log.warn("Streaming parsing exception: {}", e.message) return null } } diff --git a/src/test/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/stream/JsonRpcStreamParserTest.kt b/src/test/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/stream/JsonRpcStreamParserTest.kt index 9e8ef390..4068cf66 100644 --- a/src/test/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/stream/JsonRpcStreamParserTest.kt +++ b/src/test/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/stream/JsonRpcStreamParserTest.kt @@ -1,5 +1,7 @@ package io.emeraldpay.dshackle.upstream.rpcclient.stream +import org.junit.jupiter.api.Assertions.assertArrayEquals +import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test @@ -25,6 +27,31 @@ class JsonRpcStreamParserTest { .verify(Duration.ofSeconds(1)) } + @Test + fun `if non 200 response code then aggregate response`() { + val statusCode = 403 + val bytes = "{\"strangeResponse\": 2}".toByteArray() + val stream: Flux = Flux.just(bytes) + + val response = streamParser.streamParse(statusCode, stream).block() as AggregateResponse + + assertEquals(statusCode, response.code) + assertArrayEquals(bytes, response.response) + } + + @Test + fun `if exception is thrown during parse first part then aggregate response`() { + val statusCode = 200 + val bytes = "{\"jsonrpc\":".toByteArray() + val secondBytes = "2, \"end\": 2}".toByteArray() + val stream: Flux = Flux.just(bytes, secondBytes) + + val response = streamParser.streamParse(statusCode, stream).block() as AggregateResponse + + assertEquals(statusCode, response.code) + assertArrayEquals(bytes.plus(secondBytes), response.response) + } + @ParameterizedTest @MethodSource("data") fun `if first part has result field then single response`(