Skip to content

Commit 80aeebb

Browse files
oten91claude
andcommitted
fix: Handle NEAR nested error strings as valid JSON-RPC success
NEAR's JSON-RPC returns execution errors as strings nested inside the result object: {"result":{"error":"wasm execution failed..."},"id":1} The heuristic was detecting "error" via substring match, seeing both "result" and "error" present, and flagging it as jsonrpc_both_result_and_error (malformed). This triggered retry, reputation penalty, and circuit breaking for all NEAR suppliers returning wasm execution errors. Fix: when both "result" and "error" are found, check if "error" is a top-level JSON-RPC error object ("error": {) or a nested string ("error": "..."). Only flag as malformed if it's a top-level error object. Uses whitespace-tolerant scanning consistent with emptyResultType(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 62afc4c commit 80aeebb

2 files changed

Lines changed: 55 additions & 5 deletions

File tree

qos/heuristic/analyzer_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,18 @@ func TestProtocolAnalysis_JSONRPC(t *testing.T) {
207207
expectedRetry: true,
208208
expectedReason: "jsonrpc_both_result_and_error",
209209
},
210+
{
211+
name: "NEAR result with nested error string - valid success (not malformed)",
212+
response: []byte(`{"jsonrpc":"2.0","result":{"block_hash":"kkJU2quYwNQ","block_height":192983668,"error":"wasm execution failed with error: HostError(GuestPanic { panic_msg: \"panicked at E102\" })","logs":[]},"id":20}`),
213+
expectedRetry: false,
214+
expectedReason: "jsonrpc_success",
215+
},
216+
{
217+
name: "NEAR result with nested error string - simple",
218+
response: []byte(`{"jsonrpc":"2.0","id":1,"result":{"error":"some error message","logs":[]}}`),
219+
expectedRetry: false,
220+
expectedReason: "jsonrpc_success",
221+
},
210222
{
211223
name: "Error without jsonrpc version - suspicious",
212224
response: []byte(`{"error":"Bad Gateway"}`),

qos/heuristic/protocol.go

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -340,14 +340,29 @@ func analyzeJSONRPC(prefix []byte, fullLength int, rpcType sharedtypes.RPCType,
340340
}
341341
}
342342

343-
// Case 3: Has BOTH "result" and "error" - malformed (violates JSON-RPC spec)
343+
// Case 3: Has BOTH "result" and "error"
344+
// Standard JSON-RPC spec forbids both, but some protocols (e.g., NEAR) nest an
345+
// "error" string inside the result object for execution failures:
346+
// {"jsonrpc":"2.0","result":{"error":"wasm execution failed...","logs":[]},"id":1}
347+
// This is a valid success response — NOT malformed. Only flag if the "error" is a
348+
// top-level JSON-RPC error object (i.e., "error" followed by `:` and `{`).
344349
if hasResult && hasError {
350+
if isTopLevelJSONRPCError(prefix) {
351+
return AnalysisResult{
352+
ShouldRetry: true,
353+
Confidence: 0.95,
354+
Reason: "jsonrpc_both_result_and_error",
355+
Structure: StructureValid,
356+
Details: "JSON-RPC response has both result and error (malformed)",
357+
}
358+
}
359+
// "error" is nested inside result (e.g., NEAR's result.error string) — treat as success
345360
return AnalysisResult{
346-
ShouldRetry: true,
347-
Confidence: 0.95,
348-
Reason: "jsonrpc_both_result_and_error",
361+
ShouldRetry: false,
362+
Confidence: 0.0,
363+
Reason: "jsonrpc_success",
349364
Structure: StructureValid,
350-
Details: "JSON-RPC response has both result and error (malformed)",
365+
Details: "JSON-RPC response contains result field with nested error string",
351366
}
352367
}
353368

@@ -557,6 +572,29 @@ func emptyResultType(prefix []byte) emptyResultKind {
557572
return notEmpty
558573
}
559574

575+
// isTopLevelJSONRPCError checks if the "error" field in the prefix is a top-level
576+
// JSON-RPC error object (i.e., "error": {), as opposed to a nested error string
577+
// inside the result object (e.g., NEAR's "result":{"error":"wasm execution failed..."}).
578+
// Uses whitespace-tolerant scanning, same approach as emptyResultType.
579+
func isTopLevelJSONRPCError(prefix []byte) bool {
580+
idx := bytes.Index(prefix, jsonrpcErrorField)
581+
if idx < 0 {
582+
return false
583+
}
584+
585+
// Skip past "error" and find the colon
586+
after := prefix[idx+len(jsonrpcErrorField):]
587+
after = bytes.TrimLeft(after, " \t\n\r")
588+
if len(after) == 0 || after[0] != ':' {
589+
return false
590+
}
591+
after = bytes.TrimLeft(after[1:], " \t\n\r")
592+
593+
// Top-level JSON-RPC error is an object: "error": { ... }
594+
// Nested error string is: "error": "string"
595+
return len(after) > 0 && after[0] == '{'
596+
}
597+
560598
// IsJSONRPCLikeSuccess provides a quick check for JSON-RPC success.
561599
// This is a simplified version for callers who just need a boolean.
562600
func IsJSONRPCLikeSuccess(prefix []byte) bool {

0 commit comments

Comments
 (0)