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 a9b6eff4..157391e2 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 @@ -37,7 +37,10 @@ class JsonRpcStreamParser( private const val QUOTE: Byte = '"'.code.toByte() } - fun streamParse(statusCode: Int, response: Flux): Mono { + fun streamParse( + statusCode: Int, + response: Flux, + ): Mono { val firstPartSize = AtomicInteger() return response.bufferUntil { if (firstPartSize.get() > firstChunkMaxSize) { @@ -107,7 +110,10 @@ class JsonRpcStreamParser( } } - private fun aggregateResponse(response: Flux, statusCode: Int): Mono { + private fun aggregateResponse( + response: Flux, + statusCode: Int, + ): Mono { return ByteBufFlux.fromInbound(response).aggregate().asByteArray() .map { AggregateResponse(it, statusCode) } } @@ -161,10 +167,10 @@ class JsonRpcStreamParser( endStream: AtomicBoolean, whatCount: AtomicReference, ): SingleResponse? { + var response: SingleResponse? = null try { jsonFactory.createParser(firstBytes).use { parser -> - while (true) { - parser.nextToken() + while (parser.nextToken() != null) { if (firstBytes.size == parser.currentLocation.byteOffset.toInt()) { break } @@ -172,51 +178,56 @@ class JsonRpcStreamParser( if (parser.currentName == "result") { val token = parser.nextToken() val tokenStart = parser.tokenLocation.byteOffset.toInt() - return if (token.isScalarValue) { + if (token.isScalarValue) { val count = CountQuotesAndSlashes(AtomicInteger(1)) whatCount.set(count) - SingleResponse( - processScalarValue(parser, tokenStart, firstBytes, count, endStream), - null, - ) + response = + SingleResponse( + processScalarValue(parser, tokenStart, firstBytes, count, endStream), + null, + ).merge(response) } else { when (token) { JsonToken.START_OBJECT -> { - val count = CountObjectBrackets( - AtomicInteger(1), - CountQuotesAndSlashes(AtomicInteger(0)), - ) + val count = + CountObjectBrackets( + AtomicInteger(1), + CountQuotesAndSlashes(AtomicInteger(0)), + ) whatCount.set(count) - SingleResponse( - processAndCountBrackets( - tokenStart, - firstBytes, - count, - endStream, - OBJECT_OPEN_BRACKET, - OBJECT_CLOSE_BRACKET, - ), - null, - ) + response = + SingleResponse( + processAndCountBrackets( + tokenStart, + firstBytes, + count, + endStream, + OBJECT_OPEN_BRACKET, + OBJECT_CLOSE_BRACKET, + ), + null, + ).merge(response) } JsonToken.START_ARRAY -> { - val count = CountArrayBrackets( - AtomicInteger(1), - CountQuotesAndSlashes(AtomicInteger(0)), - ) + val count = + CountArrayBrackets( + AtomicInteger(1), + CountQuotesAndSlashes(AtomicInteger(0)), + ) whatCount.set(count) - SingleResponse( - processAndCountBrackets( - tokenStart, - firstBytes, - count, - endStream, - ARRAY_OPEN_BRACKET, - ARRAY_CLOSE_BRACKET, - ), - null, - ) + response = + SingleResponse( + processAndCountBrackets( + tokenStart, + firstBytes, + count, + endStream, + ARRAY_OPEN_BRACKET, + ARRAY_CLOSE_BRACKET, + ), + null, + ).merge(response) } else -> { @@ -225,15 +236,15 @@ class JsonRpcStreamParser( } } } else if (parser.currentName == "error") { - return SingleResponse(null, responseRpcParser.readError(parser)) + return SingleResponse(null, responseRpcParser.readError(parser)).merge(response) } } } - return null + return response } } catch (e: Exception) { log.warn("Streaming parsing exception: {}", e.message) - return null + return response } } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/stream/Responses.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/stream/Responses.kt index 1b2e7a4f..c9903da9 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/stream/Responses.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/stream/Responses.kt @@ -13,6 +13,21 @@ data class SingleResponse( fun noResponse() = result == null && error == null + fun merge(other: SingleResponse?): SingleResponse { + if (other == null) { + return this + } + var newResult: ByteArray? = result + var newError: ChainCallError? = error + if (newResult == null && other.result != null) { + newResult = other.result + } + if (newError == null && other.error != null) { + newError = other.error + } + return SingleResponse(newResult, newError) + } + override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is SingleResponse) return false 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 6c2c61b7..a5941c72 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,6 @@ package io.emeraldpay.dshackle.upstream.rpcclient.stream +import io.emeraldpay.dshackle.upstream.ChainCallError import io.emeraldpay.dshackle.upstream.stream.AggregateResponse import io.emeraldpay.dshackle.upstream.stream.Chunk import io.emeraldpay.dshackle.upstream.stream.SingleResponse @@ -71,6 +72,28 @@ class JsonRpcStreamParserTest { .verify(Duration.ofSeconds(1)) } + @Test + fun `if first part has result field and error then single response`() { + val statusCode = 200 + val response = + @Suppress("ktlint:standard:max-line-length") + "{\"id\": 2,\"result\": null, \"error\":{\"code\":-32000,\"message\":\"tracing failed: fee cap less than block base fee\"}}".toByteArray() + val stream: Flux = Flux.just(response) + + StepVerifier.create(streamParser.streamParse(statusCode, stream)) + .expectNext( + SingleResponse( + "null".toByteArray(), + ChainCallError( + -32000, + "tracing failed: fee cap less than block base fee", + ), + ), + ) + .expectComplete() + .verify(Duration.ofSeconds(1)) + } + @ParameterizedTest @MethodSource("dataStream") fun `if big result then stream response`(