Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix: also skip circuit breaker for honest JSON-RPC errors on REST
Earlier commit split rest_protocol_mismatch into a separate honest-error
reason and removed it from the deceptive-pattern list, so the strike
system stops accumulating critical strikes for it. But the domain-level
circuit breaker lives on a separate path: shouldCircuitBreak only
exempts responses whose MatchedPattern is "capability_limitation", and
the new reason had an empty MatchedPattern, so the circuit breaker still
broke the domain on every retry.

Canary observation after the previous fix: rm01.kalorius.tech still
getting circuit-broken on tron with reason="...rest_protocol_mismatch_
error..." despite the strike-system change.

Tag MatchedPattern="capability_limitation" on the new reason. Mirrors
how non_json_capability_limitation (Tron lite fullnodes returning plain
text "API closed") is already handled — both branches of the existing
guard then exempt it: reputation penalty skipped, circuit breaker skipped,
ShouldRetry preserved so the request still rolls to a REST-capable peer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  • Loading branch information
oten91 and claude committed May 5, 2026
commit 16b4f3e11edfd7498bd9e830c789f8ba9b6a6cd8
32 changes: 18 additions & 14 deletions qos/heuristic/analyzer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -822,10 +822,11 @@ func TestFullAnalyzer_SolanaEmptyArrayDetection(t *testing.T) {

func TestProtocolAnalysis_REST(t *testing.T) {
tests := []struct {
name string
response []byte
expectedRetry bool
expectedReason string
name string
response []byte
expectedRetry bool
expectedReason string
expectedMatchedPattern string
}{
{
name: "REST success response",
Expand Down Expand Up @@ -879,20 +880,22 @@ func TestProtocolAnalysis_REST(t *testing.T) {
// Honest JSON-RPC error from a node that only speaks JSON-RPC: PATH routed
// a REST-shaped request to it and the node correctly replied with its
// native error format. This is a capability mismatch, NOT gaming, and
// should be classified separately so the gateway does not punish it as
// a deceptive response.
name: "REST receives JSON-RPC error — honest capability mismatch, not gaming",
response: []byte(`{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"Method not found"}}`),
expectedRetry: true,
expectedReason: "rest_protocol_mismatch_error",
// should be tagged so the gateway skips both the reputation penalty and
// the circuit breaker.
name: "REST receives JSON-RPC error — honest capability mismatch, not gaming",
response: []byte(`{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"Method not found"}}`),
expectedRetry: true,
expectedReason: "rest_protocol_mismatch_error",
expectedMatchedPattern: "capability_limitation",
},
{
// Geth/Bor/Erigon spec quirk: error response with "result":null. Should
// still be treated as an honest error, not a canned success.
name: "REST receives JSON-RPC error with null result — honest capability mismatch",
response: []byte(`{"jsonrpc":"2.0","id":1,"result":null,"error":{"code":-32601,"message":"Method not found"}}`),
expectedRetry: true,
expectedReason: "rest_protocol_mismatch_error",
name: "REST receives JSON-RPC error with null result — honest capability mismatch",
response: []byte(`{"jsonrpc":"2.0","id":1,"result":null,"error":{"code":-32601,"message":"Method not found"}}`),
expectedRetry: true,
expectedReason: "rest_protocol_mismatch_error",
expectedMatchedPattern: "capability_limitation",
},
}

Expand All @@ -903,6 +906,7 @@ func TestProtocolAnalysis_REST(t *testing.T) {

assert.Equal(t, tt.expectedRetry, result.ShouldRetry, "ShouldRetry mismatch")
assert.Equal(t, tt.expectedReason, result.Reason, "Reason mismatch")
assert.Equal(t, tt.expectedMatchedPattern, result.MatchedPattern, "MatchedPattern mismatch")
})
}
}
Expand Down
16 changes: 11 additions & 5 deletions qos/heuristic/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,7 @@
// backend speaks JSON-RPC only and replied with its native error format
// (e.g. -32601 Method not found) when PATH routed a REST-shaped request
// to it. The response is still a protocol mismatch (we should retry
// against a REST-capable peer), but it's NOT gaming — penalising it as

Check warning on line 485 in qos/heuristic/protocol.go

View workflow job for this annotation

GitHub Actions / misspell

[misspell] qos/heuristic/protocol.go#L485

"penalising" is a misspelling of "penalizing"
Raw output
./qos/heuristic/protocol.go:485:63: "penalising" is a misspelling of "penalizing"
// deceptive triggers false-positive cooldowns on operators whose nodes
// simply don't support REST. Use a distinct reason so the gateway can
// route this to a major (non-strike) signal.
Expand All @@ -497,12 +497,18 @@
hasJSONRPCResult := bytes.Contains(prefix, jsonrpcResultField) &&
!bytes.Contains(prefix, jsonrpcResultNull)
if hasJSONRPCError && !hasJSONRPCResult {
// Tag MatchedPattern as a capability limitation so existing guards
// (recordHeuristicErrorToReputation and shouldCircuitBreak) skip
// reputation penalty and circuit breaking. The supplier behaved
// correctly — its backend simply doesn't speak REST. Mirrors the
// non_json_capability_limitation treatment for Tron lite fullnodes.
return AnalysisResult{
ShouldRetry: true,
Confidence: 0.95,
Reason: "rest_protocol_mismatch_error",
Structure: StructureValid,
Details: "REST request received a JSON-RPC error response — supplier likely doesn't support REST",
ShouldRetry: true,
Confidence: 0.95,
Reason: "rest_protocol_mismatch_error",
Structure: StructureValid,
MatchedPattern: "capability_limitation",
Details: "REST request received a JSON-RPC error response — supplier likely doesn't support REST",
}
}
return AnalysisResult{
Expand Down
Loading