Share one JSON-RPC error writer across HTTP denial paths - #6086
Merged
Conversation
JAORMX
requested review from
ChrisJBurns,
amirejaz,
blkt,
jerm-dro,
jhrozek,
rdimitrov and
tgrunnagle
as code owners
July 28, 2026 08:56
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #6086 +/- ##
==========================================
+ Coverage 72.26% 72.28% +0.02%
==========================================
Files 728 729 +1
Lines 75523 75533 +10
==========================================
+ Hits 54575 54601 +26
+ Misses 17045 17029 -16
Partials 3903 3903 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
amirejaz
previously approved these changes
Jul 28, 2026
Three HTTP denial paths each carry a private copy of the same block: encode a *jsonrpc2.Response via jsonrpc2.EncodeMessage before writing any header, fall back to a hardcoded conformant body on the unreachable encode failure, then set Content-Type, write status, write body. That block exists because of #5950 — encoding/json on a jsonrpc2 type reflects Go-capitalized keys, drops the mandatory "jsonrpc":"2.0" tag, and renders every id as {} — so the only thing preventing a fourth hand-rolled copy from reintroducing that bug has been a greppable convention. #6055's review rejected a map-building shared helper, because an any-typed id parameter is exactly how a caller reintroduces the {} id bug, but noted a narrower helper taking a jsonrpc2 message avoids the objection. Add exactly that: mcp.EncodeJSONRPCError and mcp.WriteJSONRPCError take a typed *jsonrpc2.Response, so no id can reach them untyped. Scope is deliberately three sites, not four. The fourth carrier of this block, pkg/authz/response_filter.go, is being rewritten by #6087 and again by #6088; collapsing it here would guarantee a conflict and duplicate that work, so it keeps its private copy for now and can adopt the helper once those land. Behavior is unchanged except the per-site fallback bodies, which were unreachable and now uniformly report -32603 rather than echoing a code whose integrity is unknown once encoding has failed. Each site keeps its own status and its own code via #6066's mcp.JSONRPCCodeForStatus. The 'never json.Marshal a jsonrpc2 type' convention is also recorded in .claude/rules/go-style.md. A lint gate was considered and deliberately not added: forbidigo matches call text and cannot see argument types, so it cannot distinguish marshaling a jsonrpc2 value from any other json.Marshal call, and wiring up gocritic/ruleguard for one rule is not worth the infrastructure. The helper plus the recorded rule is the enforcement. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y5WXPQr5Da9Cox2nZWhg6n
JAORMX
force-pushed
the
jsonrpc-error-contract-followups
branch
from
July 28, 2026 10:04
690adb5 to
5246673
Compare
10 tasks
amirejaz
approved these changes
Jul 28, 2026
JAORMX
added a commit
that referenced
this pull request
Jul 28, 2026
Main gained CRD-affecting changes (#6092, #6046, #6086) after the previous merge, so the Generate CRD Docs check fails against the merge result until docs/operator/crd-api.md is regenerated on top. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y5WXPQr5Da9Cox2nZWhg6n
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Why. Three HTTP denial paths — authz
handleUnauthorizedand both webhooksendErrorResponses — each carry a private copy of the same block: encode a*jsonrpc2.Responseviajsonrpc2.EncodeMessagebefore writing any header, fall back to a hardcoded conformant body on the (unreachable) encode failure, setContent-Type, write status, write body. That block exists because of #5950:encoding/jsonon ajsonrpc2type reflects Go-capitalized keys, drops the mandatory"jsonrpc":"2.0"tag, and renders every id as{}— and the only enforcement of "neverjson.Marshalajsonrpc2type" has been a greppable convention across hand-rolled copies. #6055's reviewer notes rejected a map-building shared helper (anany-typed id parameter is exactly how a caller reintroduces the{}id bug) but said a narrower helper taking a jsonrpc2 message "is worth a follow-up". This is that follow-up.What.
mcp.EncodeJSONRPCErrorandmcp.WriteJSONRPCError(pkg/mcp/jsonrpc_write.go) take a typed*jsonrpc2.Response— noany-typed id anywhere — encode before any header is written, and fall back to a hardcoded conformant-32603body on encode failure. The three sites collapse onto them; each keeps its own semantics (status, code via #6066'smcp.JSONRPCCodeForStatus, message policy). Behavior is unchanged except the per-site fallback bodies, which were unreachable and now uniformly report-32603rather than echoing a code whose integrity is unknown once encoding has failed.Scope: deliberately three sites, not four. The fourth carrier of this block,
pkg/authz/response_filter.go, is being rewritten upstream — #6087 restructures its error write path and #6088 rewrites it again — so collapsing it here would guarantee conflicts and duplicate that work. It keeps its private copy for now and can adopt the helper in a follow-up once those land; this PR touches that file zero lines. Three of four sites is still real consolidation, and the helper is the landing spot for the fourth.Honest scoping against #6066: it changed which code each site emits and scrubbed leaked text, but kept each site's private copy of the encode-fallback-write block — so the duplication is still real. Had #6066 centralized the write path, the honest answer here would have been "no longer worth it"; it didn't.
Convention + lint stance: the "never
json.Marshalajsonrpc2type" rule is recorded in.claude/rules/go-style.md(with the omit-absent-id corollary from #6038/#6068). A lint gate was considered and deliberately not added: forbidigo matches call-expression text and cannot see argument types, so it cannot distinguish marshaling ajsonrpc2value from any otherjson.Marshal; the only precise option is wiring gocritic/ruleguard, which is not worth the infrastructure for one rule. The helper plus the recorded convention is the enforcement.Part of the #5950/#6038/#6039 error-contract cleanup line; the helper follow-up named in #6055's "Special notes for reviewers".
Type of change
Test plan
pkg/mcp/jsonrpc_write_test.gopins the helper's wire properties (lowercase keys + version tag via case-sensitiveJSONEq; absent id omitted, never"id":null;id: 0survives —omitemptyon the wire struct testsIsNil, not zero-ness; status +Content-Typeon the write path). All three collapsed sites remain covered by their existing envelope-conformance tests (pkg/authz,pkg/webhook/...— all green), which is the point: a behavior-preserving refactor should need no assertion changes, and it didn't.task lint-fix) — 0 issues.task testnot used locally (known gotestfmt v2.5.0 panic); CI runs the real thing.API Compatibility
v1beta1API, OR theapi-break-allowedlabel is applied and the migration guidance is described above.Changes
pkg/mcp/jsonrpc_write.gopkg/mcp/jsonrpc_write_test.gopkg/authz/middleware.gohandleUnauthorizedcollapses ontomcp.WriteJSONRPCErrorpkg/webhook/{validating,mutating}/middleware.gosendErrorResponsecollapses onto the helper; keeps #6066'smcp.JSONRPCCodeForStatus.claude/rules/go-style.mdDoes this introduce a user-facing change?
No. Wire behavior at all three sites is unchanged; only the unreachable encode-failure fallbacks are unified.
Special notes for reviewers
any-typed ids and nojsonrpc2.ID, which is precisely the shape Emit conformant JSON-RPC on denial and error paths #6055's objection warned about; they were made conformant by Omit absent JSON-RPC ids instead of emitting null #6068 and stay as they are.pkg/authz/response_filter.gois intentionally absent (see the scope note above) — Frame SSE filter-failure errors as SSE events #6087/Parse SSE responses per event, not per line #6088 own that file's error path right now; the natural follow-up after they land is a two-line adoption ofmcp.EncodeJSONRPCErrorthere.🤖 Generated with Claude Code
https://claude.ai/code/session_01Y5WXPQr5Da9Cox2nZWhg6n