Repository navigation
[Feature] Standardize JSON-RPC null parameter handling #6951
Description
Summary
Some JSON-RPC methods dereference a null parameter without a null check, throw NullPointerException, and answer the client with jsonrpc4j's fallback error. On Java 8 the response is:
{"jsonrpc":"2.0","id":1,"error":{"code":-32001,"message":null,"data":"java.lang.NullPointerException"}}This issue defines the null handling of 10 parameter positions and 7 optional DTO fields:
- A null required object parameter, filter ID or
fullTransactionObjectsreturns-32602on endpoints where the method is available. - An optional DTO field explicitly set to
nullis treated as omitted.
This issue handles the listed null inputs and aligns eth_uninstallFilter lookup-miss results for non-null IDs with go-ethereum. Other behavior of non-null inputs, HTTP status codes, gRPC and HTTP API behavior remain unchanged.
This issue covers how these methods themselves check and answer a null, together with the eth_uninstallFilter lookup-miss result; it does not touch the framework layer. The shape of the fallback response for unmapped exceptions (-32001, echoed exception class name) is a separate problem, tracked in #6941.
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 JVMs where helpful NullPointerException messages are enabled, which is the default from JDK 15 onwards,messageinstead carries a diagnostic string naming internal fields and method signatures.dataechoes the Java exception class name, which clients should not depend on.-32001is registered in the public error catalog as a server-side internal error, so clients cannot tell that they passed a bad parameter.- The same class of input gets different results across methods (an error, a success, or a different code), so clients cannot handle it uniformly.
Current State
10 parameter positions have execution paths that can dereference a null argument:
- null object parameter:
eth_getLogs,eth_newFilter,eth_estimateGas,eth_call,buildTransaction - null filter ID:
eth_uninstallFilter,eth_getFilterChanges,eth_getFilterLogs - null
fullTransactionObjects(Booleanauto-unboxing):eth_getBlockByHash,eth_getBlockByNumber
7 optional DTO fields behave differently when explicitly null than when omitted: tokenId / tokenValue on BuildArguments (return -32001), consumeUserResourcePercent / originEnergyLimit / permissionId / extraData (wrapped into -32000 by the builder's catch-all), and CallArguments.from (returns -32602, whereas omitting it continues with the zero address).
Audit scope: the null handling of all 52 methods on TronJsonRpc was checked one by one, with the result below. This issue only covers positions where a null can lead to an unhandled exception, or DTO fields whose explicit null differs from omission. These positions are covered as a whole, including the existing successful eth_getBlockByNumber branch for a non-existent block and a null fullTransactionObjects; positions that already reject null are left alone; positions where null already has a definite but debatable result are behavior changes that need their own discussion. Following the discussion below, one lookup-miss behavior is additionally taken into this issue, eth_uninstallFilter returning false instead of an error, avoiding a separate intermediate policy for uninstall lookup misses. The other behavior changes stay out of scope.
| Category | Count | Handling |
|---|---|---|
| Methods without parameters | 16 methods | not applicable |
Unimplemented methods whose body only throws -32601, so the parameters never reach the business logic |
11 methods | not applicable |
hash / address / block number / storage key parameters that already return -32602 on null (most of these null checks were added by #6828) |
10 positions | already correct, unchanged |
Positions with paths that can throw NullPointerException on null |
10 positions | this issue |
| Optional DTO fields whose explicit null differs from omission | 7 fields | this issue |
Null has a definite but debatable result: web3_sha3(null) returns the hash of empty input; whether the transaction index is validated depends on whether the block exists; the 4 uncle methods validate nothing; an optional block parameter that is null returns an error (-32600 for eth_call) instead of being treated as latest |
several | behavior changes, separate issues |
On the baseline verified here, the node logs nothing for these calls, because JsonRpcServlet sets setShouldLogInvocationErrors(false) and the fallback path has no log point of its own. develop @ 4a21592 and GreatVoyage-v4.8.2.1 are both affected; verified on Java 8 and Java 17. Reproduce:
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}'Limitations and Risks
- Clients matching the old codes (
-32001/-32000) on these paths will observe a change. - Consensus, chain state and funds are not involved; the current response may carry an exception class name and, on JVMs with helpful NullPointerException messages, JVM-generated diagnostic text; the responses shown here contain no stack trace, path or configuration.
Proposed Solution
Proposed Design
Basis, in priority order: the Execution API required / schema; whether null can reasonably be taken as the zero value; go-ethereum (v1.17.6) / Besu (26.9.0) behavior as a reference.
The null-input results below apply on endpoints where the methods are available. Existing request-source checks retain precedence; unavailable methods keep their current errors.
| Input | Handling | Basis |
|---|---|---|
| Null required object parameter (5 methods) | -32602, message invalid filter request (filter methods) / invalid params |
JSON-RPC 2.0 / Execution API required. go-ethereum v1.17.6 and Besu 26.9.0 also reject it with -32602; go-ethereum v1.17.5 and earlier decoded it as a zero value. The comparison applies to the four eth_ methods; the TRON-specific buildTransaction follows the same required-object policy. |
Null filter ID in eth_uninstallFilter / eth_getFilterChanges / eth_getFilterLogs |
-32602 "invalid params" |
required ID. go-ethereum v1.17.6 (ethereum/go-ethereum#35576) and Besu 26.9.0 reject null here with -32602; go-ethereum v1.17.5 and earlier decoded it as an empty ID |
eth_uninstallFilter with a non-null ID that is unknown or already removed |
returns false |
aligned with go-ethereum, whose UninstallFilter reports whether a filter was found; removing an installed filter still returns true |
eth_getFilterChanges / eth_getFilterLogs with a non-null unknown ID |
-32000 "filter not found", unchanged |
same as go-ethereum and Besu |
Null fullTransactionObjects |
-32602 "invalid params". The block hash or selector is validated first, and the flag is checked before any block lookup, so the result does not depend on whether the block exists |
required boolean. go-ethereum v1.17.6 and Besu 26.9.0 reject null here with -32602; go-ethereum v1.17.5 and earlier decoded it as false |
| Optional DTO field explicitly null (7 fields) | same as omitting the field | the zero value is the field default; for CallArguments.from, go-ethereum (a pointer field) and Besu behave the same way |
Error responses keep the existing annotation mapping: data is "{}" and the id is echoed.
Key Changes
- Null-check the 10 positions after the request-source check and before the business logic. For
eth_getBlockByHash/eth_getBlockByNumber, the block hash or selector is validated first, thenfullTransactionObjects, before any block lookup; a null flag is no longer unboxed. - Add
@JsonSetter(nulls = Nulls.SKIP)to the 7 DTO fields; all 7 already declare non-null default initializers (0L/0/""/ the zero address), so skipping the setter on an explicit null lands exactly on the omitted-field semantics. eth_callvalidates its required transaction argument before the block parameter, soeth_call([null, null])goes from-32600to-32602.- The change is limited to the JSON-RPC layer of the
frameworkmodule:TronJsonRpc,TronJsonRpcImpl,JsonRpcApiUtil,LogFilter,BuildArguments,CallArguments. Remove the obsoleteItemNotFoundExceptiondeclaration and mapping frometh_uninstallFilter; the other filter methods retain them. Removing athrowsclause keeps existing Java binaries compatible, but source callers that specifically catch that checked exception, or implementations that still declare it, may need adjustment when recompiled. OnTronJsonRpcImpl,uninstallFilterandgetFilterChangesalso declareJsonRpcInvalidParamsException, whichTronJsonRpcalready declares for them.
Impact
- Security: removes Java exception types and diagnostic text from these error responses; no impact on consensus or transaction execution has been identified.
- Stability: null inputs no longer reach the NPE fallback path.
- Performance: null handling adds small local checks; uninstall reuses the existing maps and avoids a separate presence lookup before removal, without introducing application-level locks or full-map scans.
- Developer Experience: error codes become interpretable against the specification; clients no longer need to parse Java class names.
Compatibility
| Item | Result |
|---|---|
| Breaking Change | Yes, limited to the affected null-input and filter lookup-miss responses. Successful valid calls remain unchanged. |
| Default Behavior Change | Yes. Null object parameters, null filter IDs and null fullTransactionObjects -32001 -> -32602; eth_getBlockByNumber with a non-existent block and a null fullTransactionObjects goes from result: null to -32602; eth_uninstallFilter returns false for any non-null ID that does not identify an installed filter, replacing the previous -32000; explicit null DTO fields equal omission; eth_call([null, null]) goes from -32600 to -32602. |
| Migration Required | Conditional. Clients matching the old codes on these paths need to adjust, as do clients that branch on whether eth_uninstallFilter answers with an error or with a result. |
ByteArray.fromHex only strips a 0x prefix and left-pads to an even length, so it normalizes rather than validates. Once a lookup miss returns false, an empty string or a non-hexadecimal string also returns false instead of -32000, because they simply fail the lookup. This issue does not add format validation as a side effect.
Every -32001 above is jsonrpc4j's fallback for an exception without an @JsonRpcErrors mapping. If #6941 lands first, that fallback becomes -32603 "Internal error" without data, so only the observed "before" side of these rows changes; the target behavior defined by this issue is the same either way.
The following remain unchanged: results of valid non-null requests other than the eth_uninstallFilter lookup misses above, HTTP status codes, the request-source check, wildcard semantics of nulls inside a filter object, gRPC and non-JSON-RPC HTTP API behavior.
Acceptance Criteria
- Each row's result or error object is verified through a real
JsonRpcServer; error assertions covercode,messageanddata, including the absence of Java exception class names. - DTO fields are verified through
ObjectMapperdeserialization: an explicit null and an omitted field give the same result. - The request-source check on a SolidityNode / under PBFT is unchanged.
- A null filter ID returns
-32602 "invalid params"from all three filter-ID methods on supported endpoints; PBFT retains-32601. - A null
fullTransactionObjectsreturns-32602 "invalid params"for existing and non-existent blocks; an invalid block hash or selector keeps its own error, and a null flag is rejected before any block is read. -
eth_uninstallFilterreturnsfalsefor an unknown ID and a second removal of the same ID, andtrueonly when an installed filter is removed. -
eth_uninstallFilterreturnsfalsefor an empty string and for a non-hexadecimal string, and no format validation is introduced. - Wildcard semantics of nulls inside a filter object are unchanged.
Follow-up
Outside the scope of this issue and not blocking its closure:
- Missing
params,params: nullandparams: []retain existing dispatch and arity behavior and are not universally rejected: whether they are accepted depends on how many parameters the method declares. This issue covers explicitnullvalues in the argument positions and DTO fields listed above, plus theeth_uninstallFilterlookup-miss result. - The remaining "behavior changes" row of the audit table:
web3_sha3(null), unconditional transaction index validation, parameter validation for the uncle methods, and normalizing an omitted / null optional block parameter tolatest; each gets its own issue.
Additional Notes
- Do you have ideas regarding implementation? Yes. fix(jsonrpc): handle null parameters consistently #6984 implements the null handling defined above, including the regression tests; the update is in dccf4b3.
- Are you willing to implement this feature? Yes.
- PR fix(jsonrpc): harden RPC/HTTP parameter validation #6828 (merged into develop) added null checks for hash / address / block number / storage key parameters; this issue builds on top of it, has no pending prerequisite, and adds wire regressions for 4 of the hash-parameter methods.
- The change is confined to the JSON-RPC layer of the
frameworkmodule (TronJsonRpc,TronJsonRpcImpland the related DTOs) and does not touch consensus, transaction execution or other core logic. [Feature]Standardize JSON-RPC error handling(revert codes, LiteNode pruned-history responses, request fields validation) #6676 also touchesJsonRpcApiUtilandTronJsonRpcImpl, and [Feature] Standardize JSON-RPC error mapping and exception boundaries #6941 also modifiesTronJsonRpcandTronJsonRpcImpl. This issue does not require [Feature]Standardize JSON-RPC error handling(revert codes, LiteNode pruned-history responses, request fields validation) #6676 or [Feature] Standardize JSON-RPC error mapping and exception boundaries #6941 to land first; whichever lands second rebases and re-runs the related regression tests.
Metadata
Metadata
Assignees
Type
Projects
- StatusShow more project fieldsNo status
Activity
lxcmyf commentedon Sep 8, 2026
One compatibility question around
eth_uninstallFilter: this proposal makes a null ID returnfalse, while an unknown but well-formed non-null ID still returns-32000until a follow-up. That leaves the malformed input looking successful while the validly shaped missing filter errors, and clients may have to absorb two wire changes to the same method. Would it be cleaner to either treat null as-32602for now, or include the unknown-ID-to-falsenormalization here and document it as one compatibility change? This also seems more accurately described as breaking for affected callers, since an error becomes a successful result.waynercheung commentedon Sep 8, 2026
@lxcmyf Thanks. I agree with your second option: handle null and unknown filter IDs together, rather than introduce another intermediate uninstall behavior.
Concretely, in this issue:
false.true.eth_getFilterChangesandeth_getFilterLogskeep their-32000 "filter not found"for unknown IDs; onlyeth_uninstallFiltermoves.falsehere means no filter was removed, not that removal succeeded, which is also what go-ethereum'sUninstallFilterreports on a lookup miss. This moves one method's behavior out of the follow-up list without pulling in the other parameter-validation items and without creating a dependency on #6941.One consequence worth stating in the issue rather than discovering later:
ByteArray.fromHexonly strips a0xprefix and left-pads to an even length, so it is normalization rather than validation. Once a lookup miss returnsfalse, an empty string or a non-hexadecimal string also returnsfalseinstead of-32000, because they simply fail the lookup. I will cover that in the tests and the compatibility notes, and I will not add format validation as a side effect of this change.You are also right about the classification. "No successful request starts failing" is too narrow a basis, since callers can branch on the error code or on error-versus-result. It is also inconsistent with #6941, which labels the same class of change as breaking. I will change it to:
I will update the summary, the audit scope, the design table, the compatibility section, the acceptance criteria and the follow-up list, and replace the tests that currently pin the old unknown-ID error, adding coverage for repeated removal and for the string-ID normalization above. Does that bounded scope adjustment address your concern?
halibobo1205 commentedon Sep 8, 2026
Thanks for the detailed investigation and the clear scope. The distinction between method-level null handling here and framework-level exception mapping in #6941 makes sense. Returning
falsewhenevereth_uninstallFilterremoves no filter also gives clients a consistent contract.This issue highlights an opportunity to capture a lightweight set of conventions for future API additions and changes. For example:
null, empty values, and malformed values are handled.Having these expectations in the API review checklist would help catch ambiguities before release and reduce repeated endpoint-by-endpoint fixes.
I would support this as a separate follow-up, adopted incrementally for new or modified APIs. It should not block the focused changes proposed here.
waynercheung commentedon Sep 8, 2026
@halibobo1205 Thanks. I agree that a lightweight API review checklist would be useful, applied incrementally to new or modified APIs rather than requiring a retrofit of existing endpoints.
This issue can supply a few worked examples: distinguishing omitted, explicit
null, empty and malformed inputs; documenting defaults and lookup-miss results; and checking exact wire responses together with node-specific behavior across FullNode, Solidity and PBFT.Two points from this work that a checklist would benefit from stating explicitly:
ByteArray.fromHexstrips a0xprefix and left-pads to an even length, so an empty or non-hexadecimal filter ID is not rejected; it simply fails the lookup and now returnsfalse. That is a consequence of preserving the existing ID handling, not a statement that those inputs conform to the schema. A convention should say, per parameter, which of the two applies, because that choice decides whether a malformed value yields-32602or the not-found result.-32602, while a well-formed request to uninstall an unknown filter returnsfalsebecause no filter was removed.eth_getFilterChangesandeth_getFilterLogskeep-32000for the same lookup miss, because there the caller asked for data that does not exist. Why those three differ is not obvious from the method names.The ordering I used to decide the JSON-RPC cases here was the Execution API
requiredand schema first, then whethernullcan reasonably be read as the zero value, then go-ethereum and Besu as a reference. That is a JSON-RPC example rather than a rule for the HTTP and gRPC surfaces, which have their own conventions.For JSON-RPC specifically, I would keep envelope validation and notification classification, discussed in #6676, separate from method-level argument handling, so the checklist references those decisions instead of becoming a second place that defines competing rules.
A short checklist with a few examples and references to reusable test patterns seems like a useful first step. I can contribute the material from this work. It would not block or expand #6951; the uninstall adjustment is implemented and regression-tested on the branch.
waynercheung commentedon Sep 22, 2026
@lxcmyf @halibobo1205 The PR is #6984.
The scope adjustment discussed above is in it:
eth_uninstallFilterreturnsfalsefor a null ID, an unknown ID and an ID already removed,eth_getFilterChanges/eth_getFilterLogsretain their existing-32000 "filter not found"response for unknown IDs, and the obsoleteItemNotFoundExceptiondeclaration and mapping are removed. The proposal body was updated accordingly, including the breaking-change wording and the note thatByteArray.fromHexnormalizes rather than validates, so an empty or non-hexadecimal ID now also returnsfalse. Eight lookup-miss regressions cover repeated removal and that string-ID path. Request-source restrictions, including PBFT rejection, remain unchanged.The API review checklist stays a separate follow-up; the worked examples from this issue are ready whenever that is opened.
waynercheung commentedon Sep 26, 2026
@lxcmyf @halibobo1205 I have updated the null handling in this proposal after a recent go-ethereum change; the issue description above now reflects it.
go-ethereum v1.17.6, released on 23 September, includes ethereum/go-ethereum#35576 ("rpc: reject null for required arguments"). Parameter types with their own JSON decoding, such as hashes, addresses and quantities, already rejected
null. For the filter ID andfullTransactionObjects, however, the earlier decoder letnullreach the method as a zero value (an empty ID andfalse). Since v1.17.6 these fail parameter decoding with-32602("missing value for required argument N"). The go-ethereum alignment this proposal cited for these null inputs was based on the earlier behavior. Besu 26.9.0 also rejectsnullin these required parameter positions with its invalid-params errors ("Invalid filter params", "Invalid return complete transaction params").The updated rule: an explicit
nullin the following positions returns-32602 "invalid params", the message this proposal already uses for a nulleth_callargument.eth_uninstallFilter,eth_getFilterChangesandeth_getFilterLogs;fullTransactionObjectsofeth_getBlockByHashandeth_getBlockByNumber. The block hash or selector is validated first, following go-ethereum's argument order, and the flag is checked before any block lookup, so the result does not depend on whether the block exists.The required object parameters of
eth_getLogs,eth_newFilter,eth_call,eth_estimateGasandbuildTransactionalready return-32602fornullin this proposal. All 10 top-level parameter positions covered by this issue will therefore reject an explicitnullwith-32602on endpoints where the methods are available. A method that is unavailable on the PBFT (or non-FullNode) endpoint keeps its current error.Unchanged:
eth_uninstallFilterreturnsfalse;eth_getFilterChangesandeth_getFilterLogsreturn-32000 "filter not found". go-ethereum v1.17.6 and Besu behave the same way.nullas omission. Forfromin theeth_callobject, go-ethereum (a pointer field) and Besu behave the same way.This narrows the 8 September adjustment:
nullno longer shares the lookup-miss result but is treated as invalid input, which is the first option @lxcmyf suggested and the distinction @halibobo1205 described between invalid input and a valid request that finds no matching resource.Compared with v4.8.2, the existing NPE paths will return
-32602instead of-32001. One additional behavior change is thateth_getBlockByNumberwith a non-existent block and a nullfullTransactionObjectsflag will return-32602instead ofresult: null.#6984 will be updated to this behavior shortly. Reviews of the updated PR are welcome once it is pushed; any concerns can be discussed here.