From aa40578664eab2631bae282f83081e6ed0516232 Mon Sep 17 00:00:00 2001 From: Vadim Filin Date: Tue, 21 Apr 2026 18:24:33 +0200 Subject: [PATCH] Add safe error parsing for REST HTTP responses (#812) * fix(rest): handle non-JSON error bodies on non-200 HTTP responses When a REST upstream (e.g. TON) returns a non-200 status with a plain-text body such as "Service Unavailable" but advertises application/json as the content-type, the error parser would throw JsonParseException ("Unrecognized token 'Service'"), which bubbled up as a generic -32005 ChainException and obscured the real cause. Catch JsonParseException/IOException around readError and fall back to a "HTTP Code: X" ChainCallError (CODE_UPSTREAM_INVALID_RESPONSE), matching the behavior already implemented in JsonRpcHttpReader. Also return the fallback when readError returns null. * fix(rest): close parser, log stack traces, fall back on empty errors Address PR review feedback: - Close the Jackson JsonParser via `use { ... }` to release buffers. - Switch to parameterized SLF4J logging and pass the exception so stack traces are preserved on parsing/IO failures. - Treat a parsed error with code=0 and blank message (e.g. from a bare `{}` body) as no useful error info and fall back to the generic HTTP-status error so the upstream status isn't silently dropped. * fix(rest): short-circuit HTTP 503 without JSON parsing Narrow the fix to exactly the case reported: HTTP 503 Service Unavailable responses (typically from Cloudflare) carry a plain-text or HTML body but can be served with Content-Type: application/json, so attempting to parse the body as JSON raises JsonParseException and surfaces as a confusing -32005 "Unrecognized token 'Service'" error. For status 503, skip parsing entirely and return a ChainCallError directly. All other non-200 responses keep the existing behavior. * docs(rest): align 503 branch comment with error code --------- Co-authored-by: Claude --- .../dshackle/upstream/restclient/RestHttpReader.kt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/restclient/RestHttpReader.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/restclient/RestHttpReader.kt index ebd3694f..0b4a9075 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/restclient/RestHttpReader.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/restclient/RestHttpReader.kt @@ -3,10 +3,12 @@ package io.emeraldpay.dshackle.upstream.restclient import io.emeraldpay.dshackle.Chain import io.emeraldpay.dshackle.Global import io.emeraldpay.dshackle.config.AuthConfig +import io.emeraldpay.dshackle.upstream.ChainCallError import io.emeraldpay.dshackle.upstream.ChainRequest import io.emeraldpay.dshackle.upstream.ChainResponse import io.emeraldpay.dshackle.upstream.HttpReader import io.emeraldpay.dshackle.upstream.RequestMetrics +import io.emeraldpay.dshackle.upstream.ethereum.rpc.RpcResponseError import io.emeraldpay.dshackle.upstream.generic.ChainSpecificRegistry import io.emeraldpay.dshackle.upstream.rpcclient.ResponseRpcParser import io.emeraldpay.dshackle.upstream.rpcclient.RestParams @@ -64,7 +66,15 @@ class RestHttpReader( when (it) { is StreamResponse -> sink.next(ChainResponse(it.stream, key.id, it.headers)) is AggregateResponse -> { - if (it.code != 200) { + if (it.code == 503) { + // 503 bodies (e.g. Cloudflare) are typically plain text / HTML, not JSON, + // so skip JSON parsing to avoid a misleading "Unrecognized token" error + val error = ChainCallError( + RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE, + "HTTP Code: 503", + ) + sink.next(ChainResponse(null, error, it.headers)) + } else if (it.code != 200) { val error = parser.readError(Global.objectMapper.createParser(it.response)) sink.next(ChainResponse(null, error, it.headers)) } else {