-
Notifications
You must be signed in to change notification settings - Fork 0
feat(jsonrpc): standardize JSON-RPC error handling #18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package org.tron.core.exception.jsonrpc; | ||
|
|
||
| public class JsonRpcExecutionRevertedException extends JsonRpcInternalException { | ||
|
|
||
| public JsonRpcExecutionRevertedException(String message) { | ||
| super(message); | ||
| } | ||
|
|
||
| public JsonRpcExecutionRevertedException(String message, Object data) { | ||
| super(message, data); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| package org.tron.core.exception.jsonrpc; | ||
|
|
||
| /** | ||
| * Thrown when a request targets historical state that a LiteNode has pruned. | ||
| * Maps to JSON-RPC error code 4444 "Pruned history unavailable", as standardized | ||
| * by the Ethereum Execution API (EIP-4444). | ||
| */ | ||
| public class JsonRpcPrunedHistoryException extends JsonRpcInternalException { | ||
|
|
||
| public JsonRpcPrunedHistoryException(String message) { | ||
| super(message); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -24,6 +24,7 @@ | |||||||||||||||||||||||||||||
| import org.tron.common.utils.StringUtil; | ||||||||||||||||||||||||||||||
| import org.tron.core.Wallet; | ||||||||||||||||||||||||||||||
| import org.tron.core.exception.jsonrpc.JsonRpcInvalidParamsException; | ||||||||||||||||||||||||||||||
| import org.tron.core.exception.jsonrpc.JsonRpcPrunedHistoryException; | ||||||||||||||||||||||||||||||
| import org.tron.protos.Protocol.Block; | ||||||||||||||||||||||||||||||
| import org.tron.protos.Protocol.Transaction; | ||||||||||||||||||||||||||||||
| import org.tron.protos.Protocol.Transaction.Contract.ContractType; | ||||||||||||||||||||||||||||||
|
|
@@ -62,6 +63,7 @@ public class JsonRpcApiUtil { | |||||||||||||||||||||||||||||
| public static final String TAG_PENDING_SUPPORT_ERROR = "TAG pending not supported"; | ||||||||||||||||||||||||||||||
| public static final String TAG_SAFE_SUPPORT_ERROR = "TAG safe not supported"; | ||||||||||||||||||||||||||||||
| public static final String BLOCK_NUM_ERROR = "invalid block number"; | ||||||||||||||||||||||||||||||
| public static final String PRUNED_HISTORY_ERROR = "Pruned history unavailable"; | ||||||||||||||||||||||||||||||
| public static final String TX_INDEX_ERROR = "invalid index value"; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| private static final SecureRandom random = new SecureRandom(); | ||||||||||||||||||||||||||||||
|
|
@@ -636,7 +638,13 @@ public static long parseBlockTag(String tag, Wallet wallet) | |||||||||||||||||||||||||||||
| return wallet.getHeadBlockNum(); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| if (EARLIEST_STR.equalsIgnoreCase(tag)) { | ||||||||||||||||||||||||||||||
| return 0; | ||||||||||||||||||||||||||||||
| if (!wallet.isLiteNode()) { | ||||||||||||||||||||||||||||||
| return 0; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| // "earliest" anchors to the receipt floor (first block with complete data); with | ||||||||||||||||||||||||||||||
| // receipt persistence off no such block exists — fall back to the body floor | ||||||||||||||||||||||||||||||
| long receiptFloor = wallet.getLowestReceiptBlockNum(); | ||||||||||||||||||||||||||||||
| return receiptFloor == Long.MAX_VALUE ? wallet.getLowestBlockNum() : receiptFloor; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| if (FINALIZED_STR.equalsIgnoreCase(tag)) { | ||||||||||||||||||||||||||||||
| return wallet.getSolidBlockNum(); | ||||||||||||||||||||||||||||||
|
|
@@ -700,6 +708,42 @@ public static long parseBlockNumber(String blockNumOrTag, Wallet wallet) | |||||||||||||||||||||||||||||
| return parseBlockNumber(blockNumOrTag); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||
| * Rejects a query for a block below the LiteNode pruning cutoff with error code 4444. | ||||||||||||||||||||||||||||||
| * Raw primitive — no genesis exemption; callers own that semantics. | ||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||
| public static void checkPrunedHistory(long blockNum, Wallet wallet) | ||||||||||||||||||||||||||||||
| throws JsonRpcPrunedHistoryException { | ||||||||||||||||||||||||||||||
| if (wallet.isLiteNode() && blockNum < wallet.getLowestBlockNum()) { | ||||||||||||||||||||||||||||||
| throw new JsonRpcPrunedHistoryException(prunedMessage(wallet.getLowestBlockNum())); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||
| * Receipt form of {@link #checkPrunedHistory(long, Wallet)} for endpoints that read | ||||||||||||||||||||||||||||||
| * receipts or logs; their floor is the first block with receipts. Same raw-primitive | ||||||||||||||||||||||||||||||
| * contract. | ||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||
| public static void checkPrunedReceiptHistory(long blockNum, Wallet wallet) | ||||||||||||||||||||||||||||||
| throws JsonRpcPrunedHistoryException { | ||||||||||||||||||||||||||||||
| if (!wallet.isLiteNode()) { | ||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| long receiptFloor = wallet.getLowestReceiptBlockNum(); | ||||||||||||||||||||||||||||||
| if (receiptFloor == Long.MAX_VALUE) { | ||||||||||||||||||||||||||||||
| throw new JsonRpcPrunedHistoryException( | ||||||||||||||||||||||||||||||
| PRUNED_HISTORY_ERROR + ": transaction history is not persisted on this node"); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| if (blockNum < receiptFloor) { | ||||||||||||||||||||||||||||||
| throw new JsonRpcPrunedHistoryException(prunedMessage(receiptFloor)); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
Comment on lines
+732
to
+739
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Handle disabled receipt persistence before comparing the floor. When receipt persistence is disabled, Proposed fix long receiptFloor = wallet.getLowestReceiptBlockNum();
- if (wallet.isLiteNode() && blockNum < receiptFloor) {
- throw new JsonRpcPrunedHistoryException(receiptFloor == Long.MAX_VALUE
- ? PRUNED_HISTORY_ERROR + ": transaction history is not persisted on this node"
- : prunedMessage(receiptFloor));
+ if (wallet.isLiteNode() && receiptFloor == Long.MAX_VALUE) {
+ throw new JsonRpcPrunedHistoryException(
+ PRUNED_HISTORY_ERROR + ": transaction history is not persisted on this node");
+ }
+ if (wallet.isLiteNode() && blockNum < receiptFloor) {
+ throw new JsonRpcPrunedHistoryException(prunedMessage(receiptFloor));
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| private static String prunedMessage(long earliestAvailable) { | ||||||||||||||||||||||||||||||
| return PRUNED_HISTORY_ERROR + ": earliest available block is 0x" | ||||||||||||||||||||||||||||||
| + Long.toHexString(earliestAvailable); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||
| * Max hex digits of a 32-bit int (0x7FFFFFFF). A transaction index fits a signed int, so the | ||||||||||||||||||||||||||||||
| * longest valid input is "0x" + 8 hex digits; the +2 in the guard covers the prefix. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -133,6 +133,12 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I | |
| writeJsonRpcError(resp, JsonRpcError.INVALID_REQUEST, "Invalid Request", null, false); | ||
| return; | ||
| } | ||
| if (!isBatch && violatesStrictCompliance(rootNode)) { | ||
| JsonNode id = rootNode.get("id"); | ||
| writeJsonRpcError(resp, JsonRpcError.INVALID_REQUEST, "Invalid Request", | ||
| id != null && (id.isTextual() || id.isNumber()) ? id : null, false); | ||
| return; | ||
| } | ||
| int batchSize = parameter.getJsonRpcMaxBatchSize(); | ||
| if (isBatch && batchSize > 0 && rootNode.size() > batchSize) { | ||
| writeJsonRpcError(resp, JsonRpcError.EXCEED_LIMIT, | ||
|
|
@@ -193,8 +199,10 @@ private void handleBatch(HttpServletResponse resp, JsonNode rootNode, int maxRes | |
| continue; | ||
| } | ||
|
|
||
| if (!subRequest.isObject()) { | ||
| ObjectNode errNode = buildErrorNode(JsonRpcError.INVALID_REQUEST, "Invalid Request", null); | ||
| if (!subRequest.isObject() || violatesStrictCompliance(subRequest)) { | ||
| JsonNode subId = subRequest.get("id"); | ||
| ObjectNode errNode = buildErrorNode(JsonRpcError.INVALID_REQUEST, "Invalid Request", | ||
| subId != null && (subId.isTextual() || subId.isNumber()) ? subId : null); | ||
| byte[] errBytes = MAPPER.writeValueAsBytes(errNode); | ||
| int addition = errBytes.length + (!batchResult.isEmpty() ? 1 : 0); | ||
| if (maxResponseSize > 0 && accumulatedSize + addition > maxResponseSize) { | ||
|
|
@@ -265,6 +273,36 @@ private void handleBatch(HttpServletResponse resp, JsonNode rootNode, int maxRes | |
| resp.getOutputStream().flush(); | ||
| } | ||
|
|
||
| /** | ||
| * Whether a request object breaks JSON-RPC 2.0 (https://www.jsonrpc.org/specification), | ||
| * checked only when {@code node.jsonrpc.strictComplianceMode} is on. jsonrpc4j is tolerant | ||
| * by default: it accepts a missing or wrong {@code jsonrpc} field and any {@code id} type. | ||
| */ | ||
| private static boolean violatesStrictCompliance(JsonNode node) { | ||
| if (!CommonParameter.getInstance().isJsonRpcStrictComplianceMode()) { | ||
| return false; | ||
| } | ||
| JsonNode version = node.get("jsonrpc"); | ||
| if (version == null || !version.isTextual() || !"2.0".equals(version.asText())) { | ||
| return true; | ||
| } | ||
| // A malformed request object is -32600, not the -32601 jsonrpc4j reports for a missing | ||
| // or non-string method. | ||
| JsonNode method = node.get("method"); | ||
| if (method == null || !method.isTextual()) { | ||
| return true; | ||
| } | ||
| // params may be omitted; a present one must be an Array or Object per JSON-RPC 2.0 §4.2 | ||
| JsonNode params = node.get("params"); | ||
| if (params != null && !params.isArray() && !params.isObject()) { | ||
| return true; | ||
| } | ||
| // Any non-scalar id is rejected. Spec deviation: null is a valid id per JSON-RPC 2.0, | ||
| // but jsonrpc4j treats it as a notification and answers nothing, so -32600 beats silence. | ||
| JsonNode id = node.get("id"); | ||
| return id != null && !id.isTextual() && !id.isNumber(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: Strict compliance mode rejects an explicit JSON Prompt for AI agents
Comment on lines
+300
to
+303
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift Accept an explicit null JSON-RPC ID.
Preserve the explicit-null request through jsonrpc4j without converting it to a notification. Return a normal response with 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| private byte[] readBody(InputStream in) throws IOException { | ||
| ByteArrayOutputStream buffer = new ByteArrayOutputStream(); | ||
| byte[] tmp = new byte[4096]; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Resolve
"earliest"from the block floor.For a LiteNode, this code returns
getLowestReceiptBlockNum(). The PR contract requires"earliest"to resolve togetLowestBlockNum(). When receipt retention starts later than block retention, block-number methods skip retained blocks.Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents