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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user