Summary
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:
{"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:
- 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. 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 |
Fatal Error (VirtualMachineError / ThreadDeath / LinkageError / TronError, including wrapped ones) |
no JSON-RPC response |
N/A |
N/A |
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. Valid notifications still produce no response.
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 (identity-set cycle protection).
- 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: dispatch single requests through
handleRequest(InputStream, OutputStream) (handle(...) swallows fatal errors); map escaped RuntimeException | IOException; best-effort commit 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 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.
Acceptance Criteria
Follow-up
Outside the scope of this issue and not blocking its closure:
Additional Notes
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. 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 flagError(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. Valid notifications still produce no response.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 (identity-set cycle protection).-32001mapping fornet_version/eth_chainId; giveExecutionException/InterruptedExceptiona fixedmessage; log the cause and restore the interrupt flag atFuture.get().handleRequest(InputStream, OutputStream)(handle(...)swallows fatal errors); map escapedRuntimeException | IOException; best-effort commit an empty 500 before rethrowing an escaped Error 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 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.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.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.-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.