You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
For an exception without an @JsonRpcErrors mapping, JSON-RPC falls back to jsonrpc4j's -32001 and returns the Java exception class name and the raw exception message to the client, for example:
This issue standardizes JSON-RPC error mapping and the exception boundaries:
Unmapped non-fatal exceptions return -32603 "Internal error"; Java exception class names and raw messages are no longer echoed.
Four fatal Error categories, including java-tron's TronError, propagate instead of being disguised as JSON-RPC error responses. That classification governs the resolver and the two dispatch boundaries; the outer doPost guard is a last-resort cleanup that covers any Error raised outside the conversion path, so it is not a guarantee that only these four categories can escape every stage of the servlet. Before rethrowing, the servlet makes a best-effort attempt to commit an empty HTTP 500 so the container does not render exception details.
An invalid request ID type or non-null scalar params returns -32600 "Invalid Request" instead of being swallowed on the single-request path or aborting the whole batch.
Only failure-path responses change. Whenever a normal JSON-RPC response is produced, its HTTP status remains 200; before propagating a fatal Error, the servlet best-effort commits an empty HTTP 500, while a failed attempt may leave a closed connection. Successful responses, gRPC and non-JSON-RPC HTTP API behavior remain unchanged.
This issue only covers the framework-level fallback and exception boundaries; it does not change how any method validates its own parameters. The null parameter of eth_getLogs in the example is a separate problem; even once it gets a null check, any other unmapped exception still takes this path.
Problem
Motivation
message: null violates JSON-RPC 2.0 section 5.1, which defines message as "A String providing a short description of the error" (null is not a String); on Java 17 it becomes a diagnostic string containing internal method signatures; data echoes the Java exception class name. None of these should be depended on by clients.
-32001 is registered in the public error catalog as a server-side internal error, yet the fallback files every unmapped exception there, including client input errors, so clients cannot tell them apart.
OutOfMemoryError / StackOverflowError are converted into ordinary error responses, masking an unrecoverable process state.
A single request with an invalid request ID gets HTTP 200 with an empty body. Scalar params has the same result after a registered method reaches argument matching; an unknown method is rejected earlier as -32601. In a batch, the servlet catch-all returns only a -32603 / id: null response for the framework exception and stops early, discarding prior results and skipping later elements.
Current State
Unmapped exceptions: the resolver returns null and jsonrpc4j falls back to ERROR_NOT_HANDLED (-32001). net_version / eth_chainId declare JsonRpcInternalException without an annotation and take exactly this path.
The asynchronous query in eth_getLogs / eth_getFilterLogs: ExecutionException leaks the cause's type through message; InterruptedException yields message: null and leaves the interrupt flag cleared.
Fatal errors: jsonrpc4j catches Throwable both at the method invocation layer and in handle(...).
Protocol level: a Boolean / object / array request ID throws IllegalArgumentException. Scalar params also throws after a registered method reaches argument matching; an unknown method returns -32601 before that check. Single-request handle(...) swallows the exception, while the batch servlet catch-all returns -32603 and stops early.
Unmapped exceptions at the method invocation stage leave no trace on the node (setShouldLogInvocationErrors(false)).
develop @ 4a21592 and GreatVoyage-v4.8.2.1 are both affected; verified on Java 8 and Java 17. Reproduce:
# unmapped exception (any unmapped exception gives the same shape)
curl -s -X POST http://127.0.0.1:8545/jsonrpc -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"eth_getLogs","params":[null],"id":1}'# invalid request ID type -> HTTP 200 with an empty body
curl -i -s -X POST http://127.0.0.1:8545/jsonrpc -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"web3_clientVersion","params":[],"id":true}'# scalar params -> HTTP 200 with an empty body
curl -i -s -X POST http://127.0.0.1:8545/jsonrpc -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"web3_clientVersion","params":5,"id":2}'
Limitations and Risks
Clients matching the old message / data will observe a change; the net_version / eth_chainId responses are registered in the public error catalog and need a synchronized update.
When a fatal Error propagates, the client receives a best-effort empty HTTP 500 or a closed connection; the current batch loses accumulated results. This is an intentional boundary.
Consensus, chain state and funds are not involved.
Proposed Solution
Proposed Design
Case
HTTP status
JSON-RPC code
message
data / notes
Unmapped non-fatal exception
200
-32603
"Internal error"
no data; first (method, exception type) occurrence is WARN with the stack, repeats are DEBUG without stack/message
net_version / eth_chainId failure
200
-32001
"Chain identity unavailable"
"{}" (explicit mapping; keeps the code registered in the public catalog)
ExecutionException / InterruptedException
200
-32000
"Internal error"
"{}"; InterruptedException restores the interrupt flag
Non-fatal Error escaping dispatch (for example an AssertionError from a JsonRpcInterceptor hook, which jsonrpc4j does not catch)
200
-32603
"Internal error"
no data; the fatal cause is classified before logging or response generation; the same ID rules apply, a single request without an id keeps its empty body, and in a batch only that element is affected
servlet best-effort commits an empty HTTP 500 and rethrows the same Error; resource exhaustion may close the connection instead; propagation does not itself terminate the process
handleRequest throws IOException
200
-32603 when an ID is present
"Internal error"
no data; a no-ID single request remains response-free
Boolean / object / array request ID
200
-32600
"Invalid Request"
id: null (2.0 section 4); in a batch only that element is affected
Non-null scalar params (single request or batch element)
200
-32600
"Invalid Request"
no data; echo a valid id, otherwise use id: null; only the offending batch element is affected
Mapped errors
200
unchanged
deliberate business messages such as "filter not found" unchanged
"{}" unchanged
-32603 is the Internal error defined by JSON-RPC 2.0; its error-code classification matches Besu's RpcErrorType.INTERNAL_ERROR. Rejecting Boolean IDs is stricter than go-ethereum, following 2.0 section 4 (an ID is a String, Number or Null). Section 4.2 requires params to be structured. java-tron classifies non-null scalar params at the request-envelope layer as -32600, matching Besu's error-code classification (its HTTP status handling differs); geth classifies the same shape as -32602 at method-argument parsing. This is a difference in layering and error classification, not a claim that geth violates the specification.
Request-envelope validation takes precedence over method lookup: an unknown method with scalar params changes from -32601 to -32600, while the same unknown method with valid params: [] remains -32601. An object with scalar params and no id is not a valid Notification, because a Notification must first be a valid Request Object under sections 4 and 4.1; it therefore receives -32600 with id: null. This issue does not unify notification handling: errors returned by jsonrpc4j are forwarded, a single-request servlet catch without an id stays silent, and a recoverable batch failure without an id produces an error with id: null. Normalizing those into one rule is proposed for #6676, subject to confirmation there.
Key Changes
resolver: unmapped non-fatal exceptions become -32603; logging is bounded by (method, exception type), with the first occurrence at WARN carrying the Throwable and repeats at DEBUG without the Throwable/message; message precedence is annotation > exception > per-code default; walk the cause chain for four fatal Error categories and rethrow the actual cause. The scan allocates nothing, because a fatal cause may itself be an OutOfMemoryError, detects cycles with two pointers rather than a visited set, and applies no depth cutoff.
mapping annotations: add an explicit -32001 mapping for net_version / eth_chainId; give ExecutionException / InterruptedException a fixed message; log the cause and restore the interrupt flag at Future.get().
servlet: recoverable batch failures in request serialization, dispatch and response parsing produce an element-scoped -32603, keep earlier results and continue with later elements within the existing response-size rules; only the replacement error is charged when a malformed response is replaced. Each batch logs its first escaped non-fatal dispatch failure at ERROR with the Throwable and later ones at DEBUG with only the index and exception class, with request-local state. Dispatch single requests through handleRequest(InputStream, OutputStream) (handle(...) swallows fatal errors); catch RuntimeException, IOException and Error at both dispatch boundaries, rethrowing a classified fatal cause before logging and otherwise using the existing sanitized error path; separately, the outer guard best-effort commits an empty 500 before rethrowing an escaped Error, without allowing cleanup failures to replace it; validate request-ID and non-null params container types before dispatch and isolate batch elements.
The change is limited to the JSON-RPC layer of the framework module: JsonRpcErrorResolver, JsonRpcServlet, TronJsonRpc, TronJsonRpcImpl, LogBlockQuery.
Impact
Security: error responses no longer return Java exception class names or unaudited exception messages; propagating fatal errors stops masking an existing process-level failure signal.
Stability: unmapped exceptions get a stable fallback; a future method that misses a null check will not echo internal types.
Performance: the normal path is unaffected; full WARN stacks for unmapped exceptions are limited to the first occurrence of each method/type pair.
Developer Experience: errors become interpretable against the specification; unmapped exceptions at the method invocation stage start appearing in the node log.
Compatibility
Item
Result
Breaking Change
Yes, limited to failure and exception handling paths. Unmapped exceptions -32001 + class name -> -32603; net_version / eth_chainId keep the code but get a fixed message / data; ExecutionException / InterruptedException get a fixed message; invalid IDs, and scalar params after a registered method is selected, go from an empty body to an error response for single requests and from one -32603 plus early batch termination to an isolated -32600 for batch elements; an unknown method with scalar params changes from -32601 to -32600, while valid params: [] still returns -32601; four fatal categories go from an error response to an empty HTTP 500 or closed connection. Difference from geth: geth returns -32602 for scalar params and sends no response when such a malformed request has no id, whereas java-tron returns -32600 with id: null. Must be included in the release notes.
Default Behavior Change
Yes. Only failure responses and request ID / non-null params container validation change; successful responses remain unchanged.
Migration Required
Conditional. Clients matching the old message / data need to adjust.
The following remain unchanged: successful responses, HTTP 200 whenever a normal JSON-RPC response is produced, existing dispatch and method validation for missing / null / Array / Object params, code / data of the 62 existing mappings (4 asynchronous-exception mappings only gain a message; 2 chain identity mappings are added), gRPC and non-JSON-RPC HTTP API behavior.
Before merge: update the public error catalog (docs/api/openrpc.json in documentation-en; four entries: JSON_RPC_UNDERLYING_INTERNAL_ERROR, JSON_RPC_SERVLET_INTERNAL_ERROR, JSON_RPC_EXECUTION_ERROR, JSON_RPC_INTERRUPTED); check whether gateways / SDKs / monitoring depend on the old -32001 behavior.
The comparisons above are against develop. For a non-fatal Error escaping dispatch, the old single-request JsonRpcServer.handle catches and logs the Throwable without guaranteeing a complete JSON-RPC error response: a hook that fails before output produces an empty HTTP 200, while a later failure may leave partial output. A batch escapes to outer/container handling. The new dispatch catches return -32603 under the existing ID rules, discard partial output, and recover per batch element within the existing response budget. The best-effort empty HTTP 500 remains the intended handling for the four fatal categories; it was only an intermediate branch behavior for non-fatal dispatch Errors.
Acceptance Criteria
code / message / data for every row of the table are pinned by tests against a real JsonRpcServer / JsonRpcServlet.
All four fatal categories (including wrapped causes) escape the servlet; the best-effort empty 500 never echoes the fatal marker, and cleanup failure never replaces the original Error.
Escaped RuntimeException / IOException follows the documented ID/notification behavior.
A non-fatal Error escaping dispatch is classified before logging, then answered with -32603 under the same ID and batch rules; a classified fatal cause at the same boundary is still rethrown unchanged.
Resolver logging emits one WARN per method/type pair; ethChainId() logs failure/recovery transitions; Future.get() logs the cause and restores the interrupt flag.
code / data of the 62 existing mappings are unchanged.
Whenever a normal JSON-RPC response is produced, HTTP 200, content type and the batch and response size limits are unchanged; the three notification shapes above keep their current behavior and are pinned by characterization tests.
Non-null scalar params returns -32600 "Invalid Request" for single and batch requests; a valid id is preserved and other batch elements continue.
Request-envelope validation precedes method lookup: unknown method + scalar params returns -32600, while the same unknown method + params: [] remains -32601.
A characterization test pins jsonrpc4j dispatch behavior so a later upgrade cannot drift silently.
Follow-up
Outside the scope of this issue and not blocking its closure:
jsonrpc4j's loss of precision when round-tripping large integer or high-precision numeric request IDs is handled separately.
Unifying the wording of the 5 -32000 catch-alls in eth_call / eth_estimateGas / buildTransaction.
Additional Notes
Do you have ideas regarding implementation? Yes. The implementation and the adjustments discussed below have been completed and verified locally on JDK 17 (arm64): 330 tests across 28 classes, with no failures, errors or skipped tests; main and test Checkstyle passed. Notification normalization remains out of scope; characterization tests record its current behavior. The PR is fix(jsonrpc): normalize error responses and fatal handling #6985, awaiting upstream review.
Summary
For an exception without an
@JsonRpcErrorsmapping, JSON-RPC falls back to jsonrpc4j's-32001and returns the Java exception class name and the raw exception message to the client, for example:{"jsonrpc":"2.0","id":1,"error":{"code":-32001,"message":null,"data":"java.lang.NullPointerException"}}This issue standardizes JSON-RPC error mapping and the exception boundaries:
-32603 "Internal error"; Java exception class names and raw messages are no longer echoed.Errorcategories, including java-tron'sTronError, propagate instead of being disguised as JSON-RPC error responses. That classification governs the resolver and the two dispatch boundaries; the outerdoPostguard is a last-resort cleanup that covers anyErrorraised outside the conversion path, so it is not a guarantee that only these four categories can escape every stage of the servlet. Before rethrowing, the servlet makes a best-effort attempt to commit an empty HTTP 500 so the container does not render exception details.paramsreturns-32600 "Invalid Request"instead of being swallowed on the single-request path or aborting the whole batch.Only failure-path responses change. Whenever a normal JSON-RPC response is produced, its HTTP status remains 200; before propagating a fatal
Error, the servlet best-effort commits an empty HTTP 500, while a failed attempt may leave a closed connection. Successful responses, gRPC and non-JSON-RPC HTTP API behavior remain unchanged.This issue only covers the framework-level fallback and exception boundaries; it does not change how any method validates its own parameters. The null parameter of
eth_getLogsin the example is a separate problem; even once it gets a null check, any other unmapped exception still takes this path.Problem
Motivation
message: nullviolates JSON-RPC 2.0 section 5.1, which definesmessageas "A String providing a short description of the error" (nullis not a String); on Java 17 it becomes a diagnostic string containing internal method signatures;dataechoes the Java exception class name. None of these should be depended on by clients.-32001is registered in the public error catalog as a server-side internal error, yet the fallback files every unmapped exception there, including client input errors, so clients cannot tell them apart.OutOfMemoryError/StackOverflowErrorare converted into ordinary error responses, masking an unrecoverable process state.paramshas the same result after a registered method reaches argument matching; an unknown method is rejected earlier as-32601. In a batch, the servlet catch-all returns only a-32603/id: nullresponse for the framework exception and stops early, discarding prior results and skipping later elements.Current State
nulland jsonrpc4j falls back toERROR_NOT_HANDLED(-32001).net_version/eth_chainIddeclareJsonRpcInternalExceptionwithout an annotation and take exactly this path.eth_getLogs/eth_getFilterLogs:ExecutionExceptionleaks the cause's type throughmessage;InterruptedExceptionyieldsmessage: nulland leaves the interrupt flag cleared.Throwableboth at the method invocation layer and inhandle(...).IllegalArgumentException. Scalarparamsalso throws after a registered method reaches argument matching; an unknown method returns-32601before that check. Single-requesthandle(...)swallows the exception, while the batch servlet catch-all returns-32603and stops early.setShouldLogInvocationErrors(false)).develop @ 4a21592 and GreatVoyage-v4.8.2.1 are both affected; verified on Java 8 and Java 17. Reproduce:
Limitations and Risks
message/datawill observe a change; thenet_version/eth_chainIdresponses are registered in the public error catalog and need a synchronized update.Errorpropagates, the client receives a best-effort empty HTTP 500 or a closed connection; the current batch loses accumulated results. This is an intentional boundary.Proposed Solution
Proposed Design
-32603"Internal error"data; first(method, exception type)occurrence is WARN with the stack, repeats are DEBUG without stack/messagenet_version/eth_chainIdfailure-32001"Chain identity unavailable""{}"(explicit mapping; keeps the code registered in the public catalog)ExecutionException/InterruptedException-32000"Internal error""{}";InterruptedExceptionrestores the interrupt flagErrorescaping dispatch (for example anAssertionErrorfrom aJsonRpcInterceptorhook, which jsonrpc4j does not catch)-32603"Internal error"data; the fatal cause is classified before logging or response generation; the same ID rules apply, a single request without anidkeeps its empty body, and in a batch only that element is affectedError(VirtualMachineError/ThreadDeath/LinkageError/TronError, including wrapped ones)handleRequestthrowsIOException-32603when an ID is present"Internal error"data; a no-ID single request remains response-free-32600"Invalid Request"id: null(2.0 section 4); in a batch only that element is affectedparams(single request or batch element)-32600"Invalid Request"data; echo a validid, otherwise useid: null; only the offending batch element is affected"filter not found"unchanged"{}"unchanged-32603is the Internal error defined by JSON-RPC 2.0; its error-code classification matches Besu'sRpcErrorType.INTERNAL_ERROR. Rejecting Boolean IDs is stricter than go-ethereum, following 2.0 section 4 (an ID is a String, Number or Null). Section 4.2 requiresparamsto be structured. java-tron classifies non-null scalarparamsat the request-envelope layer as-32600, matching Besu's error-code classification (its HTTP status handling differs); geth classifies the same shape as-32602at method-argument parsing. This is a difference in layering and error classification, not a claim that geth violates the specification.Request-envelope validation takes precedence over method lookup: an unknown method with scalar
paramschanges from-32601to-32600, while the same unknown method with validparams: []remains-32601. An object with scalarparamsand noidis not a valid Notification, because a Notification must first be a valid Request Object under sections 4 and 4.1; it therefore receives-32600withid: null. This issue does not unify notification handling: errors returned by jsonrpc4j are forwarded, a single-request servlet catch without anidstays silent, and a recoverable batch failure without anidproduces an error withid: null. Normalizing those into one rule is proposed for #6676, subject to confirmation there.Key Changes
-32603; logging is bounded by(method, exception type), with the first occurrence at WARN carrying the Throwable and repeats at DEBUG without the Throwable/message;messageprecedence is annotation > exception > per-code default; walk the cause chain for four fatalErrorcategories and rethrow the actual cause. The scan allocates nothing, because a fatal cause may itself be anOutOfMemoryError, detects cycles with two pointers rather than a visited set, and applies no depth cutoff.-32001mapping fornet_version/eth_chainId; giveExecutionException/InterruptedExceptiona fixedmessage; log the cause and restore the interrupt flag atFuture.get().-32603, keep earlier results and continue with later elements within the existing response-size rules; only the replacement error is charged when a malformed response is replaced. Each batch logs its first escaped non-fatal dispatch failure at ERROR with the Throwable and later ones at DEBUG with only the index and exception class, with request-local state. Dispatch single requests throughhandleRequest(InputStream, OutputStream)(handle(...)swallows fatal errors); catchRuntimeException,IOExceptionandErrorat both dispatch boundaries, rethrowing a classified fatal cause before logging and otherwise using the existing sanitized error path; separately, the outer guard best-effort commits an empty 500 before rethrowing an escapedError, without allowing cleanup failures to replace it; validate request-ID and non-nullparamscontainer types before dispatch and isolate batch elements.frameworkmodule:JsonRpcErrorResolver,JsonRpcServlet,TronJsonRpc,TronJsonRpcImpl,LogBlockQuery.Impact
Compatibility
-32001+ class name ->-32603;net_version/eth_chainIdkeep the code but get a fixedmessage/data;ExecutionException/InterruptedExceptionget a fixedmessage; invalid IDs, and scalarparamsafter a registered method is selected, go from an empty body to an error response for single requests and from one-32603plus early batch termination to an isolated-32600for batch elements; an unknown method with scalarparamschanges from-32601to-32600, while validparams: []still returns-32601; four fatal categories go from an error response to an empty HTTP 500 or closed connection. Difference from geth: geth returns-32602for scalarparamsand sends no response when such a malformed request has noid, whereas java-tron returns-32600withid: null. Must be included in the release notes.paramscontainer validation change; successful responses remain unchanged.message/dataneed to adjust.The following remain unchanged: successful responses, HTTP 200 whenever a normal JSON-RPC response is produced, existing dispatch and method validation for missing / null / Array / Object
params,code/dataof the 62 existing mappings (4 asynchronous-exception mappings only gain amessage; 2 chain identity mappings are added), gRPC and non-JSON-RPC HTTP API behavior.Before merge: update the public error catalog (
docs/api/openrpc.jsonindocumentation-en; four entries:JSON_RPC_UNDERLYING_INTERNAL_ERROR,JSON_RPC_SERVLET_INTERNAL_ERROR,JSON_RPC_EXECUTION_ERROR,JSON_RPC_INTERRUPTED); check whether gateways / SDKs / monitoring depend on the old-32001behavior.The comparisons above are against
develop. For a non-fatalErrorescaping dispatch, the old single-requestJsonRpcServer.handlecatches and logs theThrowablewithout guaranteeing a complete JSON-RPC error response: a hook that fails before output produces an empty HTTP 200, while a later failure may leave partial output. A batch escapes to outer/container handling. The new dispatch catches return-32603under the existing ID rules, discard partial output, and recover per batch element within the existing response budget. The best-effort empty HTTP 500 remains the intended handling for the four fatal categories; it was only an intermediate branch behavior for non-fatal dispatch Errors.Acceptance Criteria
code/message/datafor every row of the table are pinned by tests against a realJsonRpcServer/JsonRpcServlet.RuntimeException/IOExceptionfollows the documented ID/notification behavior.Errorescaping dispatch is classified before logging, then answered with-32603under the same ID and batch rules; a classified fatal cause at the same boundary is still rethrown unchanged.ethChainId()logs failure/recovery transitions;Future.get()logs the cause and restores the interrupt flag.code/dataof the 62 existing mappings are unchanged.paramsreturns-32600 "Invalid Request"for single and batch requests; a valididis preserved and other batch elements continue.paramsreturns-32600, while the same unknown method +params: []remains-32601.Follow-up
Outside the scope of this issue and not blocking its closure:
params: 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) #6676.idare still forwarded, while a single servlet catch withoutidstays silent and explicitid: nullgets an error. Tests characterize these paths without choosing a future normalization policy.-32000catch-alls ineth_call/eth_estimateGas/buildTransaction.Additional Notes
JsonRpcServletand theTronJsonRpcannotation blocks. There is no dependency: this PR can land first and [Feature]Standardize JSON-RPC error handling(revert codes, LiteNode pruned-history responses, request fields validation) #6676 can build on the request validation it adds; if [Feature]Standardize JSON-RPC error handling(revert codes, LiteNode pruned-history responses, request fields validation) #6676 lands first, this PR will be rebased.