Repository navigation
[Feature] Standardize JSON-RPC error mapping and exception boundaries #6941
Description
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
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. - An invalid request ID type or non-null scalar
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_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: 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.- A single request with an invalid request ID gets HTTP 200 with an empty body. Scalar
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
- Unmapped exceptions: the resolver returns
nulland jsonrpc4j falls back toERROR_NOT_HANDLED(-32001).net_version/eth_chainIddeclareJsonRpcInternalExceptionwithout an annotation and take exactly this path. - The asynchronous query in
eth_getLogs/eth_getFilterLogs:ExecutionExceptionleaks the cause's type throughmessage;InterruptedExceptionyieldsmessage: nulland leaves the interrupt flag cleared. - Fatal errors: jsonrpc4j catches
Throwableboth at the method invocation layer and inhandle(...). - Protocol level: a Boolean / object / array request ID throws
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. - 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/datawill observe a change; thenet_version/eth_chainIdresponses are registered in the public error catalog and need a synchronized update. - When a fatal
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. - 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; logged at DEBUG with the stack |
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 |
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. 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; they are logged at DEBUG with the Throwable, because clients can trigger them repeatedly;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. - mapping annotations: add an explicit
-32001mapping fornet_version/eth_chainId; giveExecutionException/InterruptedExceptiona fixedmessage; log the cause at DEBUG and restore the interrupt flag atFuture.get();ethChainId()logs a lookup failure at DEBUG. - 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 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. - The change is limited to the JSON-RPC layer of the
frameworkmodule: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; unmapped exceptions are logged only at DEBUG, so repeated client-triggered failures do not produce WARN stacks.
- 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. The 62 refers to the release_v4.8.3 base before #6985; removing the eth_uninstallFilter lookup-miss mapping is handled separately by #6984 (for #6951).
Originally planned before merge; now tracked as release follow-ups outside this issue: 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 catch-all -32001 or on the previous chain-identity message / data, and ensure they classify the new -32603 responses correctly.
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/datafor every row of the table are pinned by tests against a realJsonRpcServer/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/IOExceptionfollows the documented ID/notification behavior. - A non-fatal
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. - Unmapped exceptions,
ethChainId()lookup failures andFuture.get()failures are logged at DEBUG;Future.get()logs the cause and restores the interrupt flag. -
code/dataof the 62 existing mappings on therelease_v4.8.3base are unchanged by fix(jsonrpc): normalize error responses and fatal handling #6985. - 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
paramsreturns-32600 "Invalid Request"for single and batch requests; a valididis preserved and other batch elements continue. - Request-envelope validation precedes method lookup: unknown method + scalar
paramsreturns-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:
- Whether to reject
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. - Notification normalization is deferred to [Feature]Standardize JSON-RPC error handling(revert codes, LiteNode pruned-history responses, request fields validation) #6676. This issue preserves response-suppression rules, not the old error contents: unknown-method, arity and unmapped-exception errors without
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. - 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
-32000catch-alls ineth_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, merged into
release_v4.8.3. - Are you willing to implement this feature? Yes.
- [Feature]Standardize JSON-RPC error handling(revert codes, LiteNode pruned-history responses, request fields validation) #6676 also touches
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.
Metadata
Metadata
Assignees
Type
Projects
- StatusShow more project fieldsDone
Activity
waynercheung commentedon Sep 2, 2026
Updated the proposal body after review; the scope is unchanged. What changed:
TronErroralongside the three JVM categories, and states what the servlet does before rethrowing: a best-effort attempt to commit an empty HTTP 500 so the container does not render exception details. A client may instead observe a closed connection; propagation does not itself terminate the process.IOExceptionescapinghandleRequest(-32603, same ID rules) and the matching acceptance items.halibobo1205 commentedon Sep 4, 2026
Thanks for the detailed proposal. The overall direction looks good. A few edge cases may be worth clarifying or covering in tests:
Interaction with the production response wrapper
FullNodeJsonRpcHttpServiceinstallsHttpInterceptor, which passes aCharResponseWrapperto the servlet. ItsflushBuffer()may not commit the underlying response when no wrapper-local writer or stream has been created. In a Jetty test with the production filter enabled, the intended empty HTTP 500 resulted in a non-empty default error response. A Jetty +HttpInterceptorintegration test could help confirm the expected behavior.Per-element batch handling
If
handleRequest()throwsRuntimeExceptionorIOException, the batch path should preserve completed results and continue processing subsequent elements where possible. A[success, throws, success]test would be useful.Wrapped fatal errors
It may be safer to apply the same cause-chain classification at the servlet catch boundaries as well as in the resolver, so both paths follow the same exception policy.
Notifications
It would be helpful to verify that notifications remain response-free when method resolution or invocation produces an error.
These look like implementation and test-coverage details rather than changes to the main proposal.
waynercheung commentedon Sep 4, 2026
@halibobo1205 Thanks, these are good catches. I checked all four against the current branch. Three are implementation and test gaps; the fourth requires an explicit compatibility decision, because the spec-compliant behavior changes existing wire responses.
1. Production response wrapper. Confirmed. The embedded-Jetty test registers the servlet directly and never installs
HttpInterceptor, and the servlet unit test uses a mock response whoseflushBuffer()does commit, so both missed this. In the observed production filter path the guard does not commit the underlying response:CharResponseWrapper.flushBuffer()does not callsuper.flushBuffer(), andServletOutputStreamCopydoes not overrideflush(), so neither wrapper path reliably propagates a flush downwards. TheErrorthen arrives at Jetty with an uncommitted response and the default error page is rendered.I will add an integration test that wires the production filter chain, and have the guard resolve the underlying response through
ServletResponseWrapper.getResponse()before committing, so the change stays inside JSON-RPC. The unwrapping will be used only on the fatal cleanup path and will handle nested wrappers.On the guarantee: when the bare response commits successfully, the client observes an empty HTTP 500 with no exception details. If the cleanup itself fails under a fatal condition, the result stays best effort and the client may see a closed connection or the container's own handling. I will state it that way rather than promising that details can never be rendered.
Neither wrapper path reliably propagates a flush for any caller, not only this one. Rather than widen this change, I would like to raise that on #6936 first and let you decide whether it belongs there or in a separate issue.
2. Per-element batch handling. Agreed. The proposal records losing the accumulated batch only as the fatal
Errorboundary, so recoverable failures were meant to stay element-scoped, but the implementation does not do that: aRuntimeExceptionorIOExceptionfrom one element discards the accumulated results and returns a single-element array. Two sibling paths do the same and will change with it: sub-request serialization failure and sub-response parse failure. All three will append an element-scoped-32603, preserve completed responses, and continue, reusing the existing response-size accounting. Fatal Errors still abort the batch.Tests:
[success, RuntimeException, success]and[success, IOException, success], since the change covers both; a malformed sub-response followed by a successful element; a wrapped fatal aborting both the single and batch paths.3. Wrapped fatal errors. Agreed. I will centralize the cycle-safe cause-chain classification and apply it before the servlet catch paths translate
RuntimeExceptionorIOExceptioninto-32603, so a wrapped fatal Error is treated consistently whether it reaches the resolver or eitherhandleRequestcatch boundary.4. Notifications. Agreed on the target, with a correction to the baseline and one classification question for you.
Today a request without
idis response-free only when the call succeeds. Measured with jsonrpc4j 1.6 and the resolver currently ondevelop:iddevelop-32601-32601-32602-32602-32001, raw message and exception class name-32603The
method throws, unmappedrow matters for this discussion: today a client that asked for no response still receives the exception message and class name. So making notifications response-free on failure paths is a change rather than a preservation, and I will list it under breaking changes with these rows.The classification question: deciding what to suppress requires deciding what counts as a notification, and this issue deliberately does not validate
jsonrpcormethod. Suppressing every response for an object without anidwould swallow genuinely malformed requests. Measured on the same setup, all of these return-32601today and have noid:I see two ways forward and would rather you pick:
id,jsonrpcexactly"2.0", a String-valuedmethod, andparamseither absent, an Array, or an Object. This does not reject or otherwise validate malformed envelopes; it only suppresses dispatcher output for requests that are unambiguously valid JSON-RPC 2.0 notifications. Missing or invalidjsonrpcormethod,params: null, and explicitid: nullare not classified as notifications by this narrow helper; their behavior remains governed by the existing dispatch rules and the separate envelope decisions in [Feature]Standardize JSON-RPC error handling(revert codes, LiteNode pruned-history responses, request fields validation) #6676.Suppression under the first option applies to whatever the dispatcher produced, so the rows above are representative rather than exhaustive: mapped method errors and parameter-conversion failures for otherwise valid notifications follow the same rule.
Either way, a malformed object without an
id, for example one carrying scalarparams, stays an Invalid Request and receives-32600withid: null. If we take the first option, a valid notification must not receive an appended error node in a batch either.I will update the acceptance criteria and the PR tests accordingly. Thanks in particular for exercising the production filter path.
waynercheung commentedon Sep 7, 2026
@halibobo1205 Implementation status for the four points above.
Items 1 to 3 are implemented and verified. The latest JDK 17 arm64 run passed 312 tests across 28 classes, with no failures, errors or skipped tests. Main and test Checkstyle also passed.
flushBuffer. The walk is bounded and allocation-free, and a self-reference, an unresolved chain or a non-HTTP inner response abandons cleanup rather than calling response methods through that wrapper. A regression test installs the realHttpInterceptorand requires server-side evidence that the underlying response was committed with status 500 and zero content length; the observing filter never commits it on the servlet's behalf.OutOfMemoryError, and it has no depth cutoff, so a fatal cause deep in a chain is still found while cyclic chains still terminate.Two notes that came out of implementing this.
developthe dispatch call is wrapped only incatch (RuntimeException), so a dispatchIOExceptionpropagated to outer handlers instead of producing a JSON-RPC error, while the three caught paths returned a single-32603withid: nullregardless of the element's own id. I will state this split in the compatibility section above.Notification normalization (item 4) has not been implemented. The current behavior remains: errors returned by jsonrpc4j are forwarded, a single-request servlet catch without an
idis silent, and a recoverable batch failure without anidproduces an error withid: null.Would you prefer the narrow notification classifier in this issue, or defer normalization to #6676? I will update the implementation notes for items 1 to 3 separately, and finalize the notification-related compatibility statement and acceptance criteria once we agree on that choice.
lxcmyf commentedon Sep 7, 2026
For notification handling, I would defer normalization to #6676. A narrow classifier here creates a second, temporary definition of a valid notification while the semantics of
jsonrpc/method,params: null, andid: nullare still undecided. That makes behavior dependent on landing order. Keeping characterization tests in this PR, then reusing one canonical envelope/notification predicate from #6676 across both forwarded and synthesized errors, seems safer.One boundary question: the proposal says only
VirtualMachineError,ThreadDeath,LinkageError, andTronErrorpropagate, but the currentdoPostguard catches and rethrows everyError.handleSingleandexecuteBatchRequestonly catchRuntimeException | IOException, so anAssertionErrorfrompreHandleJson(outside jsonrpc4j invocation catch) or another non-classifiedErrorwould bypass the resolver and become an empty HTTP 500. Is that intentional? If not, could we add single/batch tests for a non-fatalErrorat that boundary and translate it to-32603per element, while rethrowing only whenfindFatalCausereturns a match? Otherwise the compatibility section should explicitly state that allErrorsubclasses escaping dispatch are propagated.waynercheung commentedon Sep 8, 2026
@lxcmyf Both points accepted. The second one is a real gap.
Notification. Deferring to #6676 is the better call, for the reason you give: a narrow classifier here would be a second definition of a valid envelope while
jsonrpc,method,params: nullandid: nullare still open there, and it would make the result depend on landing order. This issue will preserve the existing response-suppression rules and pin them with characterization tests; the error-mapping and recovery changes still apply under those rules. I will also drop the blanket "valid notifications remain response-free" wording, because three shapes still differ: errors returned by jsonrpc4j are forwarded, a single-request servlet catch without anidis silent, and a recoverable batch failure without anidproduces an error withid: null. Deferring means recording those, not claiming they are already unified. @halibobo1205, you raised this item, so say if you would rather have it here. @0xbigapple, does taking that normalization into #6676 fit the scope you planned?The
Errorboundary. Not intentional. I checked the jsonrpc4j 1.6 bytecode:JsonRpcBasicServer.handleRequest(InputStream, OutputStream)has an exception table covering onlyJsonParseExceptionandJsonMappingException, andpreHandleJsonis invoked inside that range, so anErrorraised there leaves jsonrpc4j uncaught and also escapes both servlet dispatch catches.Errorraised during method invocation is caught byhandleObject'scatch (Throwable)and reaches the resolver, wherefindFatalCausereturns null for something like anAssertionError, so the response is-32603.The same
AssertionErrortherefore produces-32603from one layer and reaches the best-effort empty-500 guard from the other, and the existingAssertionErrortest only covers the method-invocation path. ARuntimeExceptionfrom the same hook is already mapped to-32603, so the inconsistency is specific toErrorsubclasses. This is an exception-handling boundary that is inconsistent with the stated policy;MetricInterceptor.preHandleJsonis empty in production, so I am not claiming a client-triggerable failure.Planned fix, in the direction you suggest:
Error, and applyfindFatalCausebefore any logging or response generation. A matching fatal cause is rethrown unchanged; anything else uses the existing sanitized-32603path with the current ID policy and per-element batch recovery. The helper and the per-batch log record acceptThrowableaccordingly.doPostguard stays a last-resort, best-effort cleanup. It also covers errors raised outside the conversion path, in body reading, envelope parsing and response writing, where there may be no usable request ID and no safe way to still generate an error response, so it should not be turned into a general JSON-RPC translator.preHandleJson: a single request with an ID maps to HTTP 200 with-32603, the ID preserved and no internal detail;[success, AssertionError, success]keeps three results in order; a direct and a wrapped fatal at the same boundary are rethrown unchanged and stop later batch elements.For the compatibility table I will keep comparing against
develop, whereJsonRpcServer.handleswallows thisThrowableand logs it, so a single request currently ends as HTTP 200 with an empty body rather than a 500. For this non-fatal dispatchError, the empty-500 behavior is an intermediate state of the branch under development, not a change this issue makes relative todevelop. The best-effort empty 500 remains the intended handling for the four fatal categories.halibobo1205 commentedon Sep 8, 2026
@waynercheung Thanks for addressing items 1–3 and for clarifying the existing notification behavior.
I would prefer to defer notification normalization to #6676. Notification classification is closely tied to the complete envelope-validation rules for
jsonrpc,method,params, andid, so handling it there should avoid maintaining a second, narrower definition in this issue.For #6941, it would be enough to document that error responses for notifications remain unchanged for now, and qualify any acceptance criterion that currently implies all valid notifications are response-free. #6676 can then implement and test notification suppression consistently across all dispatch and validation paths.
waynercheung commentedon Sep 8, 2026
@halibobo1205 Thanks, that aligns with @lxcmyf's suggestion. Notification normalization will stay outside #6941.
The proposal and the acceptance criteria have already been updated to record the existing paths rather than imply they are unified: errors returned by jsonrpc4j are forwarded, a single-request servlet catch without an
idstays silent, and a recoverable batch failure without anidproduces an error withid: null.One distinction worth being explicit about: what this issue preserves is the response-suppression rule, not the old error payloads. An unmapped invocation failure on a request without an
idstill changes from-32001with the exception class name to the sanitized-32603. So notifications are not untouched by this issue; only the decision of whether a response is produced is left alone.I will make sure the PR's characterization tests cover each of those shapes before merge, including the ones that are not yet pinned: an unknown method, an argument count mismatch and an unmapped exception on a request without an
id, and the servlet catch with an explicitid: null, which is a different branch from a missingid. That gives #6676 an enumerated starting point without adding a classifier here.@0xbigapple, notification normalization remains proposed follow-up work for #6676, subject to confirmation there. It is not a prerequisite for #6941.
The separate non-fatal
Errordispatch-boundary fix is still pending implementation and verification, as recorded in the proposal.waynercheung commentedon Sep 22, 2026
@lxcmyf @halibobo1205 The PR is #6985. The two outstanding implementation and test items from this thread are now addressed in it.
Non-fatal
Errorat the dispatch boundary. Both dispatch catches now takeRuntimeException | IOException | Error, withfindFatalCauseapplied before any logging or response generation; a fatal cause is rethrown unchanged, anything else follows the sanitized-32603path with the existing ID policy and per-element batch recovery. The outerdoPostguard is unchanged as a last-resort cleanup. Through a realJsonRpcServer:testPreHandleJsonAssertionErrorEscapesRealServerpins the gap itself (anAssertionErrorfrompreHandleJsonleaves jsonrpc4j uncaught),testServletSingleRecoversInterceptorAssertionErrorandtestServletBatchRecoversInterceptorAssertionErrorshow the servlet now answering-32603with the ID preserved and[ok, error, ok]kept in order, andtestNonFatalErrorIsSanitizedByRealServercovers the method-invocation layer, so both layers agree.Notification characterization. The shapes that were not yet pinned are now:
testServletForwardsUnknownMethodNotificationError,testServletForwardsArityNotificationErrorandtestServletForwardsUnmappedNotificationErrorfor forwarded jsonrpc4j errors without anid, plussingleAssertionErrorWithoutId_keepsEmptyResponse,singleAssertionErrorWithNullId_keepsErrorResponseandbatchAssertionErrorWithoutId_keepsErrorNodeAndContinuesfor the servlet catches, where a missingidand an explicitid: nullare separate branches. No classifier was added; #6676 has been asked to confirm taking normalization (#6676 (comment)).The remaining documentation and compatibility checks are listed in the PR: updating the four public error-catalog entries in documentation-en, checking gateway / SDK / monitoring dependencies on the old catch-all
-32001and on the previous chain-identitymessage/data, and ensuring their error classification handles the new-32603responses.0xbigapple commentedon Oct 12, 2026
@waynercheung Yes, that fits. Notification normalization,
params: null, explicitid: nulland the remaining request-envelope validation will be handled in #6676.waynercheung commentedon Oct 15, 2026
@0xbigapple Thanks, recorded. Notification normalization,
params: null, explicitid: nulland the remaining request-envelope validation are left to #6676.#6985 has been merged into
release_v4.8.3, alongside the related null-parameter fix #6984. The issue body and acceptance criteria have been updated to match the merged implementation. I am closing this issue as completed.The jsonrpc4j characterization test caught the change in parameter-conversion error responses in jsonrpc4j 1.7; the upgrade and the updated test are merged in #7030.
Two release items remain outside this issue:
documentation-en(docs/api/openrpc.jsonand the rendered table);-32001or on the previous chain-identitymessage/data, and ensuring they classify the new-32603responses correctly.