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 <noreply@anthropic.com>
This commit is contained in:
Vadim Filin
2026-04-21 18:24:33 +02:00
committed by GitHub
parent 25dbc7c388
commit aa40578664

View File

@@ -3,10 +3,12 @@ package io.emeraldpay.dshackle.upstream.restclient
import io.emeraldpay.dshackle.Chain import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.Global import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.config.AuthConfig import io.emeraldpay.dshackle.config.AuthConfig
import io.emeraldpay.dshackle.upstream.ChainCallError
import io.emeraldpay.dshackle.upstream.ChainRequest import io.emeraldpay.dshackle.upstream.ChainRequest
import io.emeraldpay.dshackle.upstream.ChainResponse import io.emeraldpay.dshackle.upstream.ChainResponse
import io.emeraldpay.dshackle.upstream.HttpReader import io.emeraldpay.dshackle.upstream.HttpReader
import io.emeraldpay.dshackle.upstream.RequestMetrics 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.generic.ChainSpecificRegistry
import io.emeraldpay.dshackle.upstream.rpcclient.ResponseRpcParser import io.emeraldpay.dshackle.upstream.rpcclient.ResponseRpcParser
import io.emeraldpay.dshackle.upstream.rpcclient.RestParams import io.emeraldpay.dshackle.upstream.rpcclient.RestParams
@@ -64,7 +66,15 @@ class RestHttpReader(
when (it) { when (it) {
is StreamResponse -> sink.next(ChainResponse(it.stream, key.id, it.headers)) is StreamResponse -> sink.next(ChainResponse(it.stream, key.id, it.headers))
is AggregateResponse -> { 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)) val error = parser.readError(Global.objectMapper.createParser(it.response))
sink.next(ChainResponse(null, error, it.headers)) sink.next(ChainResponse(null, error, it.headers))
} else { } else {