Repository navigation
[Feature] Standardize HTTP API Error Messages #6936
Description
Activity
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
developtoday.The main channel.
Util.processError(Exception, ...)has 176 call sites across 121 servlet files in theframework/.../services/httppackage, all in servlet request-handling paths: 175 sit directly indoGet/doPost, and one sits inGetBlockServlet's privatehandle(...), which is shared by both. Of these, 172 are incatch (Exception e)blocks, while the remaining 4 are thecatch (IllegalArgumentException e)events-deprecation gates in the twoScanShieldedTRC20NotesBy*Servletclasses—the only sites that also set HTTP 400. All of them funnel intoprintErrorMsg, which returns:{"Error": "class <FQCN> : <raw Throwable.getMessage()>"}In-scope standard error paths that bypass
processErrortoday:- Six catch blocks call
Util.printErrorMsg(e)inline instead ofprocessError:GetBurnTrxServlet,GetNodeInfoServlet,GetPendingSizeServlet, andGetRewardServlethave one each, whileGetTransactionInfoByBlockNumServlethas two. They produce the same class-name + raw-message body, but bypass the shared writer. GetBlockServletbuilds its own{"Error": e.getMessage()}for theIllegalArgumentExceptions thrown byWallet.getBlock, with fixed texts such asnum must be non-positive number.andid must be legal block hash.GetBrokerageServletandGetRewardServlethand-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.ValidateAddressServletcopiese.getMessage()into themessagefield of its{result, message}body on the exception branch, exposing Bouncy Castle, Base64, or JDK-generated text.RateLimiterServletwritesprintErrorMsg(new IllegalAccessException("lack of computing resources")), so the rejection body carries theclass java.lang.IllegalAccessException :prefix on every rate-limited endpoint.solidity/GetTransactionByIdSolidityServletandGetTransactionInfoByIdSolidityServletprint baree.getMessage()text lines. Because the Content-Type is set centrally toapplication/json; charset=utf-8, these error bodies are not valid JSON, and a null message prints the literal linenull.
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 businessresult.messageenvelope (see the residuals note below).What leaks through in practice: JDK 17 “helpful NPE” messages exposing code-level variable, expression, and method names; protobuf
InvalidProtocolBufferExceptiondetails; Bouncy CastleDecoderExceptiontexts; andNumberFormatExceptioninternals. These are not reliable or stable client diagnostics and can change across JDK or dependency upgrades.For historical context, the
NullPointerExceptionshown 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.- Six catch blocks call
@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.OversizedRequestErrorHandlerhandles the 413 case added by #6843, but delegates other failures to Jetty's defaultErrorHandler.I checked
jetty-server9.4.58.setShowStacks(false)protects only the HTML representation. The JSON representation still emitsThrowable.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
HttpServiceimplementations 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.
@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.
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-privateprocessError(String, ...)
overload is used at four audited call sites for the rate-limit text, the existing GetBlock
validation texts, and the twoINVALID addressresponses.ValidateAddressServletretains its own
fixedInvalid addressbody. 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 whatdevelop
does today:Why not unwrap causes. On
develop, all 176processErrorcall sites pass an exception
caught by a handler-local catch directly toprocessError: 172 usecatch (Exception), and four
are the events-deprecationcatch (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.CreateCommonTransactionServletis the only reflective path in this package whose reflection
exception can propagate intoprocessError. AnInvocationTargetExceptionreaches 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.
ContractValidateExceptionhas two declared subclasses in the sources
tracked ondevelop, and neither reaches the classification path under the defaultdevelop
chain state: one is translated into a structuredReturnby the HTTP-facing broadcast paths
beforeprocessErroris involved, and the other comes from an actuator whose validation fails
earlier, in that state, with a plainContractValidateException. If a subtype does reach a handler, exact
matching would deliberately fail closed rather than let it inherit the residual.JsonFormat.ParseException
andMaintenanceUnavailableExceptionhave no declared subclasses in the tracked sources.Why these three compatibility residuals.
JsonFormat.ParseExceptionpreserves request-derived parser diagnostics. In particular, the
fix(jsonrpc): harden RPC/HTTP parameter validation #6828 fix released in v4.8.2 reportsinvalid address for field ...through this path.ContractValidateExceptionpreserves 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.MaintenanceUnavailableExceptionrepresents the existing maintenance-period business state.
On the classification path, a null, empty, or whitespace-only residual message would still fail
closed tointernal server error. The events-deprecation text would be preserved only for the
exactIllegalArgumentExceptiontype 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
HttpProvideruses Axios
without overridingvalidateStatus, and returnsresponse.dataonly 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.- added a commit that references this issue
on Sep 7, 2026 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, andMaintenanceUnavailableException. 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 supportedand 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 gettransactionbyidandgettransactioninfobyidservletsBare exception-message text; a null message produces the literal line nullStandard {"Error":"..."}JSONApplies 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 getBrokerageandgetReward{"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 validateaddressinput validation{"result":false,"message":"<raw exception message>"};messagemay be absent when the exception message is null{"result":false,"message":"Invalid address"}Also covers a missing address. Existing successful format messages andLength errorbehavior 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 validateaddressPOST 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
andgetrewardpreviously logged their catch-all failures aterrorlevel. Those failures are
now logged atdebuglevel on the shared error path, so these log entries are not emitted under
the default log configuration.- added a commit that references this issue
on Sep 8, 2026 - added a parent issue
on Sep 9, 2026 - added a commit that references this issue
on Sep 10, 2026 - added a commit that references this issue
on Sep 14, 2026
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsNo status
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:
internal server error.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:
Current State
Before this change, standard HTTP error-response paths behave inconsistently:
Util.processErrorreturns a concatenation of the Java exception class name and raw message.Util.processErrorand directly return exception class names orThrowable.getMessage()values.Limitations and Risks
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.ParseExceptionContractValidateExceptionMaintenanceUnavailableExceptionThese 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:
lack of computing resources, without constructing an exception for formatting.IllegalArgumentExceptiontype andEVENTS_DEPRECATED_MSGmatch.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:
NullPointerExceptionIllegalArgumentExceptionThey return:
{ "Error": "internal server error" }Key Changes
frameworkmodule.Util.processError.internal server error.{"Error":"..."}JSON.Impact
Util.processError.Compatibility
Affected clients should:
Errorfield from the standard JSON response.internal server error.The following behavior remains unchanged:
Acceptance Criteria
Util.processError; endpoints with specialized response formats retain their formats and use audited messages.Throwable.getMessage()values.JsonFormat.ParseException,ContractValidateException, andMaintenanceUnavailableExceptionmay preserve non-blank raw messages.internal server error.{"Error":"lack of computing resources"}, including on JSON-RPC endpoints.{ "Error": "internal server error" }debuglog entry is recorded.Follow-up
Future phases will:
Additional Notes
Util.processError, classify exceptions by exact runtime type, and preserve audited fixed or existing response texts at their designated call sites.