Skip to content

[Feature] Standardize JSON-RPC null parameter handling #6951

Description

@waynercheung

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 fullTransactionObjects returns -32602 on endpoints where the method is available.
  • An optional DTO field explicitly set to null is 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: 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 JVMs where helpful NullPointerException messages are enabled, which is the default from JDK 15 onwards, message instead carries a diagnostic string naming internal fields and method signatures.
  • data echoes the Java exception class name, which clients should not depend on.
  • -32001 is 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 (Boolean auto-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, then fullTransactionObjects, 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_call validates its required transaction argument before the block parameter, so eth_call([null, null]) goes from -32600 to -32602.
  • The change is limited to the JSON-RPC layer of the framework module: TronJsonRpc, TronJsonRpcImpl, JsonRpcApiUtil, LogFilter, BuildArguments, CallArguments. Remove the obsolete ItemNotFoundException declaration and mapping from eth_uninstallFilter; the other filter methods retain them. Removing a throws clause 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. On TronJsonRpcImpl, uninstallFilter and getFilterChanges also declare JsonRpcInvalidParamsException, which TronJsonRpc already 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 cover code, message and data, including the absence of Java exception class names.
  • DTO fields are verified through ObjectMapper deserialization: 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 fullTransactionObjects returns -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_uninstallFilter returns false for an unknown ID and a second removal of the same ID, and true only when an installed filter is removed.
  • eth_uninstallFilter returns false for 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: null and params: [] 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 explicit null values in the argument positions and DTO fields listed above, plus the eth_uninstallFilter lookup-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 to latest; each gets its own issue.

Additional Notes

Activity

  1. lxcmyf commented on Sep 8, 2026

    @lxcmyf
    Collaborator

    One compatibility question around eth_uninstallFilter: this proposal makes a null ID return false, while an unknown but well-formed non-null ID still returns -32000 until 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 -32602 for now, or include the unknown-ID-to-false normalization 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.

  2. waynercheung commented on Sep 8, 2026

    @waynercheung
    CollaboratorAuthor

    @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:

    • A null ID, and any ID that does not identify an installed filter, return false.
    • Removing an existing filter still returns true.
    • eth_getFilterChanges and eth_getFilterLogs keep their -32000 "filter not found" for unknown IDs; only eth_uninstallFilter moves.

    false here means no filter was removed, not that removal succeeded, which is also what go-ethereum's UninstallFilter reports 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.fromHex only strips a 0x prefix and left-pads to an even length, so it is normalization rather than validation. 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. 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:

    Yes, limited to the affected null-input and filter lookup-miss responses. Successful valid calls remain unchanged.

    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?

  3. halibobo1205 commented on Sep 8, 2026

    @halibobo1205
    Collaborator

    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 false whenever eth_uninstallFilter removes 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:

    • Define how omitted parameters, explicit null, empty values, and malformed values are handled.
    • Specify defaults and distinguish invalid input from valid requests that find no matching resource.
    • Document the expected result/error shape and protocol-specific error codes, including any intentional compatibility deviations.
    • Cover these decisions with reusable wire-level contract tests, including the behavior of different node types where applicable.

    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.

  4. waynercheung commented on Sep 8, 2026

    @waynercheung
    CollaboratorAuthor

    @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:

    • Normalization is not validation. ByteArray.fromHex strips a 0x prefix 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 returns false. 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 -32602 or the not-found result.
    • Invalid input and a valid request with no matching resource are different. Here a null required object parameter is invalid input and returns -32602, while a well-formed request to uninstall an unknown filter returns false because no filter was removed. eth_getFilterChanges and eth_getFilterLogs keep -32000 for 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 required and schema first, then whether null can 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.

  5. waynercheung commented on Sep 22, 2026

    @waynercheung
    CollaboratorAuthor

    @lxcmyf @halibobo1205 The PR is #6984.

    The scope adjustment discussed above is in it: eth_uninstallFilter returns false for a null ID, an unknown ID and an ID already removed, eth_getFilterChanges / eth_getFilterLogs retain their existing -32000 "filter not found" response for unknown IDs, and the obsolete ItemNotFoundException declaration and mapping are removed. The proposal body was updated accordingly, including the breaking-change wording and the note that ByteArray.fromHex normalizes rather than validates, so an empty or non-hexadecimal ID now also returns false. 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.

  6. removed this from the GreatVoyage-v4.8.3 milestone on Sep 24, 2026
  7. waynercheung commented on Sep 26, 2026

    @waynercheung
    CollaboratorAuthor

    @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 and fullTransactionObjects, however, the earlier decoder let null reach the method as a zero value (an empty ID and false). 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 rejects null in these required parameter positions with its invalid-params errors ("Invalid filter params", "Invalid return complete transaction params").

    The updated rule: an explicit null in the following positions returns -32602 "invalid params", the message this proposal already uses for a null eth_call argument.

    • the filter ID of eth_uninstallFilter, eth_getFilterChanges and eth_getFilterLogs;
    • fullTransactionObjects of eth_getBlockByHash and eth_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_estimateGas and buildTransaction already return -32602 for null in this proposal. All 10 top-level parameter positions covered by this issue will therefore reject an explicit null with -32602 on endpoints where the methods are available. A method that is unavailable on the PBFT (or non-FullNode) endpoint keeps its current error.

    Unchanged:

    • A non-null filter ID that does not identify an installed filter: eth_uninstallFilter returns false; eth_getFilterChanges and eth_getFilterLogs return -32000 "filter not found". go-ethereum v1.17.6 and Besu behave the same way.
    • The seven optional DTO fields covered by this proposal continue to treat an explicit null as omission. For from in the eth_call object, go-ethereum (a pointer field) and Besu behave the same way.

    This narrows the 8 September adjustment: null no 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 -32602 instead of -32001. One additional behavior change is that eth_getBlockByNumber with a non-existent block and a null fullTransactionObjects flag will return -32602 instead of result: 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    • Status
      No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions