Skip to content

[Feature] Standardize HTTP API Error Messages #6936

Description

@halibobo1205

Summary

Some HTTP APIs return Java exception class names and unaudited Throwable.getMessage() values to clients when handling exceptions, for example:

{
  "Error": "class java.lang.NullPointerException : null"
}

This proposal standardizes client-facing error messages from standard HTTP Servlets in phase 1:

  • Existing messages are temporarily preserved for exceptions explicitly covered for compatibility.
  • All other exceptions return the stable message internal server error.
  • Java exception class prefixes and raw exception messages that are not explicitly allowed are no longer returned to clients.

Phase 1 changes only the content and format of failure responses. Successful responses, request rules, HTTP status codes, and gRPC behavior remain unchanged.

The shared HTTP rate-limit response is also sanitized for JSON-RPC endpoints, returning {"Error":"lack of computing resources"} without the Java exception class prefix. Other JSON-RPC behavior remains unchanged.

Problem

Motivation

Returning runtime exception information directly to clients has the following problems:

  • It provides little value to clients in diagnosing or handling request failures.
  • Exception messages may change with JDK or dependency versions and are unsuitable as a stable, long-term API contract.

Current State

Before this change, standard HTTP error-response paths behave inconsistently:

  • Util.processError returns a concatenation of the Java exception class name and raw message.
  • Some Servlets bypass Util.processError and directly return exception class names or Throwable.getMessage() values.
  • Two Solidity query endpoints return plain-text error bodies instead of standard JSON.

Limitations and Risks

  • Clients that depend on Java exception class names, raw exception messages, or plain-text error bodies will observe compatibility changes.
  • To limit the compatibility impact of phase 1, non-blank raw messages from three exact exception types are temporarily preserved.

Proposed Solution

Proposed Design

1. Standardize HTTP error-response paths

The standard Servlet JSON and text error-response paths governed by phase 1 are routed through Util.processError, which centrally determines the final client-facing message.

Endpoints with a specialized response format, such as validateaddress, retain that format and replace dynamic exception messages locally.

2. Preserve compatibility messages for three exact exception types

For compatibility with existing clients, exception classification preserves raw messages only for the following exact runtime types:

  • JsonFormat.ParseException
  • ContractValidateException
  • MaintenanceUnavailableException

These three cases are legacy residuals temporarily retained for compatibility with existing behavior. Subclasses and nested causes are not matched.

3. Preserve audited fixed and existing response texts

Existing client-facing validation and state messages are preserved through their audited paths:

  • The rate limiter directly returns the fixed text lack of computing resources, without constructing an exception for formatting.
  • The Scan Events deprecation text is preserved only when both the exact IllegalArgumentException type and EVENTS_DEPRECATED_MSG match.
  • Existing GetBlock validation messages and fixed address-validation responses are retained.

These fixed or pre-existing response texts do not broaden the exception-type compatibility rules above.

4. Fail closed for all other exceptions

Except for the explicitly allowed compatibility branches above, exceptions must not expose Java exception class names or raw messages to clients. This includes, but is not limited to:

  • NullPointerException
  • Array or collection bounds exceptions
  • Ordinary IllegalArgumentException
  • Other unmapped runtime or internal exceptions

They return:

{
  "Error": "internal server error"
}

Key Changes

  • The change is limited to the HTTP Servlet layer in the framework module.
  • Standard JSON and text error-response paths governed by phase 1 are routed through Util.processError.
  • Java exception class prefixes are removed from standard error responses.
  • Exception compatibility rules use exact runtime types and, for the events-deprecation exception, an exact message constant.
  • Exceptions that are not explicitly allowed return internal server error.
  • The two Solidity query endpoints change their error bodies from plain text to standard {"Error":"..."} JSON.
  • The shared rate-limit response is sanitized for all affected HTTP endpoints, including JSON-RPC endpoints.

Impact

  • Security
    • Standard generic and unclassified error-response paths no longer return Java exception class names or uncontrolled raw messages.
    • The three compatibility branches that retain raw messages remain known residuals.
  • Stability
    • Unknown exceptions use a stable message instead of depending on JDK or library exception messages.
  • Performance
    • The change adds only lightweight exception-type and fixed-message comparisons.
    • Normal request paths are unaffected.
  • Developer Experience
    • Standard HTTP error-response behavior is centralized in Util.processError.

Compatibility

Item Result
Breaking Change Yes, limited to some HTTP failure responses. Some error messages will change, including the shared rate-limit response used by JSON-RPC endpoints. The two Solidity query endpoints will return JSON instead of plain-text error bodies. These changes must be included in the release notes.
Default Behavior Change Yes. Only failure responses change; successful responses remain unchanged.
Migration Required Conditional. Clients that rely only on successful responses or documented response fields require no migration. Clients that depend on Java exception class names, raw exception messages, or plain-text Solidity error bodies must be updated.

Affected clients should:

  • Read the Error field from the standard JSON response.
  • Stop depending on Java exception class names.
  • Stop treating JDK or third-party exception messages as a stable API contract.
  • Handle unknown server errors as internal server error.

The following behavior remains unchanged:

  • Existing HTTP status codes
  • Status codes and response content for successful requests
  • Request parameters and validation rules
  • gRPC API behavior
  • JSON-RPC behavior, except for the shared HTTP rate-limit response described above

Acceptance Criteria

  • Standard Servlet error-response paths governed by phase 1 generate responses through Util.processError; endpoints with specialized response formats retain their formats and use audited messages.
  • These paths no longer concatenate Java exception class names or directly return arbitrary Throwable.getMessage() values.
  • On the exception-classification path, only the exact runtime types JsonFormat.ParseException, ContractValidateException, and MaintenanceUnavailableException may preserve non-blank raw messages.
  • For those three types, a null, empty, or whitespace-only message results in internal server error.
  • Subclasses and nested causes do not inherit compatibility exemptions.
  • The events-deprecation message is preserved only when both the exact exception type and exact message constant match.
  • Rate-limit rejections return {"Error":"lack of computing resources"}, including on JSON-RPC endpoints.
  • Existing audited GetBlock validation messages and fixed address-validation responses are preserved.
  • All other exceptions return:
{
  "Error": "internal server error"
}
  • If writing the error response to the client fails, at most one additional debug log entry is recorded.
  • The two Solidity query endpoints return standard JSON error responses.
  • HTTP status codes, successful responses, request rules, and gRPC behavior remain unchanged.
  • JSON-RPC behavior remains unchanged except for the shared HTTP rate-limit response described above.

Follow-up

Future phases will:

  • Distinguish client parameter errors from internal server errors based on the specific HTTP input source.
  • Avoid treating JDK or third-party exception messages as part of the long-term API contract.

Additional Notes

  • Do you have ideas regarding implementation? Yes. Route the in-scope standard Servlet error paths through Util.processError, classify exceptions by exact runtime type, and preserve audited fixed or existing response texts at their designated call sites.
  • Are you willing to implement this feature? Yes.

Activity

  1. halibobo1205 commented on Sep 1, 2026

    @halibobo1205
    CollaboratorAuthor

    Baseline: where today's error responses actually come from

    Before discussing the design, here is a factual inventory of how error bodies are produced on develop today.

    The main channel. Util.processError(Exception, ...) has 176 call sites across 121 servlet files in the framework/.../services/http package, all in servlet request-handling paths: 175 sit directly in doGet / doPost, and one sits in GetBlockServlet's private handle(...), which is shared by both. Of these, 172 are in catch (Exception e) blocks, while the remaining 4 are the catch (IllegalArgumentException e) events-deprecation gates in the two ScanShieldedTRC20NotesBy*Servlet classes—the only sites that also set HTTP 400. All of them funnel into printErrorMsg, which returns:

    {"Error": "class <FQCN> : <raw Throwable.getMessage()>"}

    In-scope standard error paths that bypass processError today:

    1. Six catch blocks call Util.printErrorMsg(e) inline instead of processError: GetBurnTrxServlet, GetNodeInfoServlet, GetPendingSizeServlet, and GetRewardServlet have one each, while GetTransactionInfoByBlockNumServlet has two. They produce the same class-name + raw-message body, but bypass the shared writer.
    2. GetBlockServlet builds its own {"Error": e.getMessage()} for the IllegalArgumentExceptions thrown by Wallet.getBlock, with fixed texts such as num must be non-positive number. and id must be legal block hash.
    3. GetBrokerageServlet and GetRewardServlet hand-concatenate {"Error": "INVALID address, <raw e.getMessage()>"}. The raw suffix is not JSON-escaped, so a message containing a double quote produces an invalid JSON body.
    4. ValidateAddressServlet copies e.getMessage() into the message field of its {result, message} body on the exception branch, exposing Bouncy Castle, Base64, or JDK-generated text.
    5. RateLimiterServlet writes printErrorMsg(new IllegalAccessException("lack of computing resources")), so the rejection body carries the class java.lang.IllegalAccessException : prefix on every rate-limited endpoint.
    6. solidity/GetTransactionByIdSolidityServlet and GetTransactionInfoByIdSolidityServlet print bare e.getMessage() text lines. Because the Content-Type is set centrally to application/json; charset=utf-8, these error bodies are not valid JSON, and a null message prints the literal line null.

    This list covers the standard JSON / text error paths in scope for phase 1. Other exception outlets exist and are registered separately. ValidateAddressServlet's POST parse failure logs and writes no response body. RateLimiterServlet's generic catch logs without synthesizing an error body, so the response may be empty or partially written. The trigger / estimate endpoints place exception text inside their structured business result.message envelope (see the residuals note below).

    What leaks through in practice: JDK 17 “helpful NPE” messages exposing code-level variable, expression, and method names; protobuf InvalidProtocolBufferException details; Bouncy Castle DecoderException texts; and NumberFormatException internals. These are not reliable or stable client diagnostics and can change across JDK or dependency upgrades.

    For historical context, the NullPointerException shown in the issue summary was fixed at the parsing layer by #6828, which shipped in v4.8.2. This proposal addresses the exposure pattern itself rather than one instance of it.

  2. waynercheung commented on Sep 2, 2026

    @waynercheung
    Collaborator

    @halibobo1205 Thanks for documenting the phase-1 boundary so clearly. I found one adjacent container-level case and would like your view on whether it belongs here as a later phase or in a separate linked issue.

    Phase 1 standardizes responses for exceptions caught by the HTTP servlets. Uncaught servlet/container failures take a different path: HttpService.OversizedRequestErrorHandler handles the 413 case added by #6843, but delegates other failures to Jetty's default ErrorHandler.

    I checked jetty-server 9.4.58. setShowStacks(false) protects only the HTML representation. The JSON representation still emits Throwable.toString() values from the cause chain, while the plain-text representation prints the cause stack. Disabling showStacks alone is therefore not sufficient.

    This is relevant to #6941 because its JSON-RPC servlet will stop jsonrpc4j from swallowing fatal Errors. #6941 will handle that path locally: before rethrowing a fatal Error, the servlet will make a best-effort attempt to commit a zero-length HTTP 500 response. Depending on transport state, the client may receive the empty 500 response or observe a closed connection, but Jetty should not render the Throwable when the response can be committed. Normal JSON-RPC errors remain HTTP 200 JSON-RPC responses.

    The remaining global case affects the seven HttpService implementations and is broader than JSON-RPC. A possible follow-up would sanitize exception-backed/container 5xx responses while preserving the existing 413 handling. I would leave ordinary 404/405 behavior unchanged until its compatibility impact is evaluated. The fix should be covered by embedded-Jetty tests for HTML, JSON, and plain-text responses.

    Would you prefer to track this as phase 2 of #6936, or as a separate linked hardening issue? My preference is a separate issue because it covers a different layer, affects all HTTP services, and requires a different compatibility and test scope. Either choice is fine; #6941 will remain limited to JSON-RPC behavior.

  3. halibobo1205 commented on Sep 2, 2026

    @halibobo1205
    CollaboratorAuthor

    @waynercheung I agree this belongs outside this issue because it concerns the shared container error-rendering layer rather than servlet-level exception handling. Any eventual fix should preserve #6843’s 413 behavior and avoid changing ordinary 404/405 responses unless separately evaluated.

  4. halibobo1205 commented on Sep 4, 2026

    @halibobo1205
    CollaboratorAuthor

    Design notes: why exact runtime type matching and no cause unwrapping

    The proposal classifies only the top-level exception by exact runtime class: no subtype matching
    and no inspection of the cause chain. A separate package-private processError(String, ...)
    overload is used at four audited call sites for the rate-limit text, the existing GetBlock
    validation texts, and the two INVALID address responses. ValidateAddressServlet retains its own
    fixed Invalid address body. These paths do not participate in exception classification. The
    events-deprecation text remains on the exception path and is preserved only when both the exact
    exception type and the audited message constant match. The reasoning, grounded in what develop
    does today:

    Why not unwrap causes. On develop, all 176 processError call sites pass an exception
    caught by a handler-local catch directly to processError: 172 use catch (Exception), and four
    are the events-deprecation catch (IllegalArgumentException) sites. Some callees do wrap
    lower-level exceptions, but the existing HTTP formatter serializes only the top-level exception
    class and message; it never unwraps or serializes the cause. Unwrapping would therefore expose
    information that is not part of the current response contract, and would broaden the audited
    surface from the caught exception to arbitrary nested causes.

    CreateCommonTransactionServlet is the only reflective path in this package whose reflection
    exception can propagate into processError. An InvocationTargetException reaches the formatter
    as the wrapper itself; its target exception is not serialized. Exact top-level classification
    therefore fails closed there without introducing new cause exposure.

    Why not match subclasses. ContractValidateException has two declared subclasses in the sources
    tracked on develop, and neither reaches the classification path under the default develop
    chain state: one is translated into a structured Return by the HTTP-facing broadcast paths
    before processError is involved, and the other comes from an actuator whose validation fails
    earlier, in that state, with a plain ContractValidateException. If a subtype does reach a handler, exact
    matching would deliberately fail closed rather than let it inherit the residual. JsonFormat.ParseException
    and MaintenanceUnavailableException have no declared subclasses in the tracked sources.

    Why these three compatibility residuals.

    • JsonFormat.ParseException preserves request-derived parser diagnostics. In particular, the
      fix(jsonrpc): harden RPC/HTTP parameter validation #6828 fix released in v4.8.2 reports invalid address for field ... through this path.
    • ContractValidateException preserves existing validation messages used by clients and tooling.
      Some construction sites may themselves carry lower-level text, so this remains an explicit
      compatibility residual rather than a security endorsement.
    • MaintenanceUnavailableException represents the existing maintenance-period business state.

    On the classification path, a null, empty, or whitespace-only residual message would still fail
    closed to internal server error. The events-deprecation text would be preserved only for the
    exact IllegalArgumentException type carrying the exact audited constant, so no other exception
    type can borrow that message.

    Why status codes remain unchanged in phase 1. TronWeb's default HttpProvider uses Axios
    without overriding validateStatus, and returns response.data only on the fulfilled path. Axios
    rejects non-2xx responses by default, so introducing new 4xx/5xx status codes for errors that
    currently return 2xx would also change TronWeb calls from resolving with the response body to
    rejecting with an Axios error; for an HTTP error response, the body is instead available at
    error.response.data. That is a separate
    client-compatibility change, so phase 1 limits itself to response-message sanitization and leaves
    status-code semantics to a follow-up.

  5. added a commit that references this issue on Sep 7, 2026
    8d5abd6
  6. halibobo1205 commented on Sep 8, 2026

    @halibobo1205
    CollaboratorAuthor

    Compatibility Summary

    Compatibility summary for the phase-1 change. <FQCN> is the fully-qualified Java exception
    class name and <message> is the raw exception text.

    Case Before After Note
    Standard errors from the three compatibility exception types, with non-blank messages {"Error":"class <FQCN> : <message>"} {"Error":"<message>"} Applies only to the exact runtime types JsonFormat.ParseException, ContractValidateException, and MaintenanceUnavailableException. Existing diagnostic text is retained; the class prefix is removed.
    Compatibility exceptions with null, empty, or whitespace-only messages Class-prefixed text ending in null, an empty string, or whitespace {"Error":"internal server error"} Blank messages no longer reach clients.
    Other exceptions on the standard error path {"Error":"class <FQCN> : <message>"} {"Error":"internal server error"} Includes NPEs, bounds errors, and other unclassified exceptions. Subclasses and wrapped causes do not inherit compatibility exemptions.
    Scan Events deprecation rejection {"Error":"class java.lang.IllegalArgumentException : <deprecation message>"} {"Error":"<deprecation message>"} The text 'events' field is deprecated and no longer supported and HTTP 400 are preserved. Matching requires the exact exception type and message constant.
    Shared HTTP rate-limit rejection {"Error":"class java.lang.IllegalAccessException : lack of computing resources"} {"Error":"lack of computing resources"} Also applies to FullNode, Solidity, and PBFT JSON-RPC endpoints. The existing HTTP status remains unchanged.
    Errors from the SolidityNode gettransactionbyid and gettransactioninfobyid servlets Bare exception-message text; a null message produces the literal line null Standard {"Error":"..."} JSON Applies to the standalone SolidityNode deployment, which registers these servlets. On a FullNode the same two paths are served by different servlets that already returned JSON and follow the rows above. Covers GET and POST; the message follows the same classification rules: permitted parser diagnostics remain visible, unclassified exceptions return internal server error.
    Existing GetBlock validation errors {"Error":"<existing validation message>"} Same response Existing validation messages and JSON format are preserved. Only the response-writing path is centralized.
    Address-decoding errors in getBrokerage and getReward {"Error": "INVALID address, <raw exception message>"} {"Error":"INVALID address"} Removes the dynamic suffix and its JSON-escaping risk. The body also loses the space after the colon, so clients matching the whole body literally are affected. Missing-address and default-value behavior remain unchanged.
    Exceptions during validateaddress input validation {"result":false,"message":"<raw exception message>"}; message may be absent when the exception message is null {"result":false,"message":"Invalid address"} Also covers a missing address. Existing successful format messages and Length error behavior remain unchanged.
    HTTP status codes Existing endpoint-specific status codes Unchanged No new 4xx/5xx mapping is introduced.
    Successful responses and existing not-found responses Existing response bodies Unchanged Includes existing {} responses for missing transaction records.
    Request parameters and validation rules Existing parsing, validation, and default-value behavior Unchanged This phase changes failure-response content and formatting without tightening input rules.
    gRPC and normal JSON-RPC processing Existing behavior Unchanged The only JSON-RPC change is the shared HTTP rate-limit response described above.
    Other error paths outside phase 1 Existing structured business errors, container-generated errors, and log-only failure paths Unchanged Includes Trigger/Estimate business error envelopes and validateaddress POST parsing/body-size failures; this change does not add a response body to those log-only paths.

    Operator-facing note, not a client-visible change: getburntrx, getnodeinfo, getpendingsize
    and getreward previously logged their catch-all failures at error level. Those failures are
    now logged at debug level on the shared error path, so these log entries are not emitted under
    the default log configuration.

  7. added a commit that references this issue on Sep 8, 2026
    f77b94d
  8. added a commit that references this issue on Sep 14, 2026
    0d19485
  9. removed this from the GreatVoyage-v4.8.3 milestone on Sep 24, 2026
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