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.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 {