fix(jsonrpc): normalize error responses and fatal handling - #6
Open
waynercheung wants to merge 1 commit into
Open
fix(jsonrpc): normalize error responses and fatal handling#6waynercheung wants to merge 1 commit into
waynercheung wants to merge 1 commit into
Conversation
waynercheung
force-pushed
the
feat/jsonrpc-error-sanitization
branch
from
September 2, 2026 09:46
993c3dc to
65da80f
Compare
Replace jsonrpc4j's unhandled -32001 fallback with a fixed -32603 response so unmapped exception types and raw messages no longer reach clients. Keep the documented -32001 contract for net_version and eth_chainId through explicit mappings with fixed message and data. Propagate VirtualMachineError, ThreadDeath, LinkageError, and TronError through the JSON-RPC boundary. Scan complete cause chains without allocating a visited set, detecting cycles without missing fatal causes. Check exceptions escaping dispatch before logging or mapping them. Before rethrowing an Error, unwrap response decorators with a bounded walk and make a best-effort attempt to commit an empty HTTP 500 on the underlying response. Preserve the original error if cleanup fails, and abandon cleanup without delegating into cyclic or unresolved wrappers. Route single requests through handleRequest because the servlet handle API catches Throwable. Convert escaped RuntimeException and IOException instances into -32603 responses when a response is appropriate. Recover each failed batch element without discarding earlier results or skipping later requests. Share internal-error construction across serialization, dispatch, and response-parsing failures. Count only the replacement when parsing fails, retaining existing overflow rules and the current single/batch handling of requests without an id. Bound unmapped-exception logging by RPC method and exception type, and log chain-identity failures on state transitions. Log the first escaped dispatch failure in each batch at ERROR with its cause; repeats use DEBUG with only the index and exception type. Restore interrupted status for asynchronous log queries. Reject Boolean, object, and array request IDs before dispatch with -32600 and id:null. Reject non-null scalar params with -32600, preserve valid IDs, and isolate invalid batch elements so their siblings run. Missing, null, array, and object params keep their existing semantics. Remove the obsolete request replay wrapper and HTTP status provider. Add resolver, servlet, embedded-Jetty, chain-identity, asynchronous failure, dispatch-contract, and request-envelope regression coverage. Exercise the real HTTP interceptor chain and observe commitment without performing it on the servlet's behalf. Add a package-private server injection seam for servlet tests without changing production initialization.
waynercheung
force-pushed
the
feat/jsonrpc-error-sanitization
branch
from
September 7, 2026 11:51
65da80f to
fb704f5
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Replaces jsonrpc4j's unhandled-exception fallback with spec-defined responses and stops converting fatal errors into JSON-RPC replies.
Resolver (
JsonRpcErrorResolver):-32603 "Internal error"with nodata. Logging is bounded by(RPC method, exception class): the first occurrence is WARN with the Throwable; repeats are DEBUG without the Throwable or exception message.Invalid Request/Method not found/Invalid params/Internal error), somessageis nevernull.datais exception data > annotation data; jsonrpc4j'sErrorData(exceptionClass, message)default is gone.VirtualMachineError,ThreadDeath,LinkageErrorand java-tron'sTronErrorfound anywhere on the cause chain are rethrown as the actual cause instead of being answered. A shared allocation-free, cycle-safe scan has no cause-depth cutoff; servlet dispatch catches use it before ordinary logging or error mapping as well.net_version/eth_chainIdkeep their documented-32001through an explicit mapping ("Chain identity unavailable",data "{}");ethChainId()keeps the cause and logs failure-state transitions (first failure WARN, recovery INFO, no repeated WARN during one outage).ExecutionException/InterruptedExceptionon the asynchronous log query get a fixed"Internal error"message;LogBlockQuerylogs the cause and restores the interrupt flag.Servlet (
JsonRpcServlet):handleRequest(InputStream, OutputStream)instead ofhandle(request, response), whosecatch (Throwable)would swallow the rethrown fatal error. Single and batch dispatch inspect escapedRuntimeExceptionor declaredIOExceptionfor a fatal cause before ordinary error handling.-32603, retaining earlier results and continuing with later requests unless the existing response budget overflows. Each batch logs only its first escaped non-fatal dispatch failure at ERROR with the Throwable; subsequent failures use DEBUG with only the index and exception class. This state is request-local, and fatal inspection precedes logging. Malformed response bytes are discarded; only the replacement error is charged. The original pre-parse size check and strict>boundary remain: bytes exceeding the remaining budget get-32003even if malformed.doPostguard best-effort commits a zero-length HTTP 500 before rethrowing an escapedError. On this cleanup path only, it unwraps up to 16ServletResponseWrapperlayers because the productionHttpInterceptor's response wrapper does not delegateflushBuffer. Self-reference, an unresolved/deeper chain or a non-HTTP inner response abandons cleanup without calling response methods through that wrapper. Cleanup failures never replace the original fatal. The client may receive an empty 500 or a closed connection; if cleanup cannot commit, container fallback remains possible. This propagation does not itself terminate the process.application/json-rpcare set explicitly at normal JSON-RPC servlet exits; the customHttpStatusCodeProviderconfiguration and the now-unusedCachedBodyRequestWrapperare removed.-32600 "Invalid Request"withid: null. Non-null scalarparamsalso gets-32600; a valididis echoed, while a missing, null or invalididbecomesnull. In a batch only the offending element gets the error and the other elements still execute. An explicitid: nullon an otherwise valid request is not rejected by these checks; its final semantics, and whether to rejectparams: null, are decided with [Feature]Standardize JSON-RPC error handling(revert codes, LiteNode pruned-history responses, request fields validation) tronprotocol/java-tron#6676.id; whenidis absent it keeps the existing empty 200. A recoverable batch failure withoutidstill produces an error withid: null. Responses returned normally by jsonrpc4j are forwarded unchanged, including its existing error responses to some requests withoutid. Unifying notification response suppression is a separate pending decision, not part of these recovery fixes.Why are these changes required?
An exception without an
@JsonRpcErrorsmapping currently produces (Java 8):{"jsonrpc":"2.0","id":1,"error":{"code":-32001,"message":null,"data":"java.lang.NullPointerException"}}message: nullviolates JSON-RPC 2.0 section 5.1 (on Java 17 it becomes a helpful-NPE string echoing internal class and method names),dataexposes internal types, and-32001is the code the public error catalog documents for the chain identity lookup, so callers cannot tell their own bad input from a node failure. Fatal errors such asOutOfMemoryError,StackOverflowErrorandTronErrorwere converted into ordinary error responses, masking a process-level failure; after propagation, the servlet now best-effort commits a detail-free 500 rather than allowing Jetty's default error page to render the Throwable. Invalid request ID types produced HTTP 200 with an empty body for single requests. Scalarparamshad the same result after a registered method reached argument matching; an unknown method returned-32601before inspectingparams. In a batch, framework exceptions produced only-32603withid: nulland stopped further processing.-32603is the Internal error defined by the specification and matches Besu'sRpcErrorType.INTERNAL_ERRORclassification. Rejecting Boolean IDs is deliberately stricter than go-ethereum, following section 4 (String / Number / Null). Section 4.2 requires structuredparams; java-tron classifies a non-null scalar as-32600at the request-envelope layer, matching Besu's error-code classification, while geth classifies it as-32602during method-argument parsing. For this malformed shape without anid, java-tron returnsid: null, while geth sends no response. Full analysis and reproduction steps are in tronprotocol#6941.Request-envelope validation intentionally precedes method lookup: an unknown method with scalar
paramschanges from-32601to-32600, while the same method with validparams: []remains-32601. A malformed request without anidis not a valid notification. Successful notifications remain response-free; this change does not yet unify suppression of error responses across dispatch and servlet catches.This PR has been tested by:
Unit Tests
Manual Testing
JsonRpcErrorResolverTest(16 tests) - mapped code / data priority, message defaults, sanitized unmapped exceptions, bounded logging, and fatal propagation including null/ordinary/deep/cyclic cause chains andTronError.JsonRpcErrorSanitizationIntegrationTest(19 tests) - through a realJsonRpcServerandJsonRpcServlet: unmapped exceptions sanitized on the wire, four fatal categories escaping the server, servlet best-effort empty-500 handling, fixed messages forExecutionException/InterruptedException, chain identity keeping-32001with sanitized details, business messages preserved, batch isolation, transport contracts, scalar-paramsand request-ID rules, and real dispatch of supportedparamsshapes.JsonRpcDispatchContractTest(8 tests) - characterization of jsonrpc4j 1.6 dispatch (arity, null array elements, overload selection, scalarparams) so a later framework upgrade cannot change behavior silently.JsonRpcServletTest(64 tests) - request validation and pass-through; all three batch recovery points; exact-limit, replacement-overflow and missed/double-accounting guards; per-batch logging with mixed failures and reset across requests; fatal-after-failure stops dispatch without another log; preserved missing-ID behavior; direct and wrapped fatal propagation; response-wrapper cycles/depth limits and cleanup failures.LogBlockQueryFailureTest(2 tests) - cause logging and interrupt-flag restoration.TronJsonRpcImplChainIdentityTest(1 test) - one WARN per failure episode, recovery INFO, and a new WARN after recovery.JsonRpcServletJettyTest(3 tests) - filtered and unfiltered fatal paths plus a healthy filtered request. For JSON, HTML and plain-text Accept values, the production-filter test requires server-side evidence of committed status 500 and zero content length; an empty 500 or a closed connection is then accepted. The observation filter never commits the response.A package-private server setter supports servlet tests without changing production initialization.
CachedBodyRequestWrapperTestis removed together with the class. On 2026-09-07, acleanTest --no-build-cacheJDK 17 (arm64) run passed 28 test classes / 312 tests (0 failures, 0 errors, 0 skipped); the seven focused classes above contain 113 tests.checkstyleMain,checkstyleTest, andgit diff --checkalso pass. Java 8/x86 execution was not performed on this arm64 machine.Compatibility
Breaking, limited to observable failure-handling paths; no request that succeeds today starts failing.
-32001, exception message (may benull),data= class name-32603 "Internal error", nodatanet_version/eth_chainIdfailure-32001, underlying message,data= class name-32001 "Chain identity unavailable",data "{}"ExecutionException/InterruptedException-32000, causetoString()/null-32000 "Internal error"-32603/id: null, then processing stops-32600 "Invalid Request",id: null; batch siblings continueparams-32603/id: nulland stops. An unknown method returns-32601before checkingparams.-32600 "Invalid Request", nodata; valididpreserved, otherwiseid: null; batch siblings continue. Envelope validation precedes method lookup, so unknown + scalar changes to-32600, while unknown +params: []stays-32601.Error(VirtualMachineError/ThreadDeath/LinkageError/TronError)-32001/-32000handleRequestthrows non-fatalRuntimeException/IOException-32603; withoutid, the existing empty body is retainedRuntimeExceptions and response-parsing failures each return a single-32603withid: null, discarding earlier results and skipping later elements. A dispatchIOExceptionwas not caught at all and propagated to outer handlers without this JSON-RPC response guarantee.-32603for the failed element and continue within the existing overflow rules; missingidstill producesid: nullUnchanged: successful responses, HTTP 200 whenever a normal JSON-RPC response is produced, existing dispatch and method validation for missing / null / Array / Object
params,data "{}"on the 62 existing mapped errors, deliberate business messages such as"filter not found", gRPC and non-JSON-RPC HTTP APIs. Fatal and genuine transport failures are not normal JSON-RPC responses and do not carry an HTTP-200 guarantee.Before merge:
JSON_RPC_UNDERLYING_INTERNAL_ERROR,JSON_RPC_SERVLET_INTERNAL_ERROR,JSON_RPC_EXECUTION_ERROR,JSON_RPC_INTERRUPTED) need a documentation-en PR; the table is generated fromx-tron-error-modelindocs/api/openrpc.json.-32001behavior or on the previous chain-identitymessage/data, and that their error classification accounts for the new-32603responses.Follow up
jsonrpcandmethodmembers, whether to rejectparams: null, and the final semantics of an explicitid: nullon an otherwise valid request belong to [Feature]Standardize JSON-RPC error handling(revert codes, LiteNode pruned-history responses, request fields validation) tronprotocol/java-tron#6676, which overlaps this PR inJsonRpcServletand theTronJsonRpcannotation blocks (see Extra details).JsonNodeID.idis silent, and a batch catch withoutidemitsid: null. If suppression is adopted, it must cover forwarding and synthesized-error exits together and be documented as a behavior change.-32000catch sites across three methods (eth_call,eth_estimateGas,buildTransaction) that still forward the underlying exception message; changing those public business-error messages needs its own compatibility review.-32700and loses the request id.JsonRpcServlet.doPost, on a best-effort basis.Extra details
maxResponseSizecontinues to limit dispatched response accumulation. As on existing servlet-generated error paths, protocol error envelopes are still emitted and may make the final body exceed that threshold. Request-body and token limits, together with the batch-size limit when enabled, bound this behavior; redefining the threshold as a hard final-body cap is out of scope for this PR.The per-batch log policy bounds only the servlet's escaped-dispatch failure log point, not business, resolver or container logging, and is not a cross-request rate limiter. Enabling DEBUG exposes repeated failure indices and types but not their Throwable or message. Single-request non-fatal dispatch failures are logged individually at ERROR with the Throwable.
The fatal cleanup bypasses response wrappers locally; it does not modify shared HTTP wrapper semantics. The missing flush delegation in
CharResponseWrapper/ServletOutputStreamCopycan be discussed with tronprotocol#6936 independently. The embedded-Jetty regression installs the actualHttpInterceptor; an outer observer only records the underlying response's committed state, status and zero content length, and never commits it on behalf of the servlet.An object with scalar
paramsand noidis malformed rather than a valid notification, so it receives-32600withid: null. Envelope validation also intentionally precedes method lookup; clients probing method availability should use a structurally validparamsarray or object.This PR overlaps the request-envelope validation planned in tronprotocol#6676 in
JsonRpcServletand theeth_getLogs@JsonRpcErrorsblock ofTronJsonRpc. There is no dependency between the two: this PR can land first and tronprotocol#6676 can build on the pre-dispatch checks added here; if tronprotocol#6676's PR lands first, this one will be rebased.Pre-submit checklist:
handleRequestis required and why the chain-identity cause must be retainedCloses tronprotocol#6941
Refs tronprotocol#6676