fix(server): structured 405 on /metrics, allow HEAD, derive the route set - #2461
Conversation
… set Items 6-9 of #2454, all from the #2448 review. getOnly wrote a bare string via http.Error while the seven other 405 sites in this package -- including handleHealth and handleReady on the same mux -- return the structured envelope. A client parsing the error got JSON from /health and plain text from /metrics for the identical condition. It now uses WriteError, matching its siblings. It also narrowed /metrics to GET, which promhttp had not done. RFC 9110 section 9.1 makes GET and HEAD mandatory for a general-purpose server, and some probes reach /metrics with HEAD, so that was a behavior regression against a monitoring endpoint. readOnly allows both and the spec declares head: alongside get: so the contract and the server agree. The pre-existing HEAD rejection on /health, /ready and the v1/v2 endpoints is left alone and recorded in the test comment rather than widened as a side effect of a conformance test. The conformance tests derived their route set from config.Handlers plus a hand-maintained systemRoutes list, so a future route registered directly in New -- exactly where the system endpoints already live -- would have been invisible to all three of them. Server.handle now records each pattern as it registers it, and the tests read that. Verified: adding a /debug route directly in New now fails the path assertion. TestOpenAPISpecMethodsAreAccepted promised more than its oracle delivers; it only checks the response is not 405, so a handler that 500s passes. It is now TestOpenAPIDeclaredMethodsAreNotRejected and the docstring says what it does and does not cover. Items 10 and 11 of #2454 need no action until the conditions that make them real arrive. Refs #2454 Signed-off-by: Mark Chmarny <mark@chmarny.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Enterprise Run ID: 📒 Files selected for processing (3)
Included review availability: Your plan provides up to 12 included reviews per hour; 9 remain after this review. 📝 WalkthroughWalkthroughThe OpenAPI specification now declares Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk: ⚪ Minimal · up to The PR restores HEAD support and structured 405 responses for /metrics while tightening route conformance checks and updating documentation; no actionable merge-blocking risk remains beyond normal checks and review. Suggested reviewers: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Coverage Report ✅
Coverage BadgeMerging this branch will increase overall coverage
Coverage by fileChanged files (no unit tests)
Please note that the "Total", "Covered", and "Missed" counts above refer to code statements instead of lines of code. The value in brackets refers to the test coverage of that file in the old version of the code. |
Review findings on this PR. The first one invalidates a claim I made. routePaths records a pattern when it is registered via s.handle, so a raw mux.HandleFunc written directly in New is served, never recorded, and invisible to all three conformance tests -- the exact #2454 item 7 scenario I claimed to have closed. The mutation I used to verify that claim went through s.handle, so it proved the recording path worked rather than that the bypass was caught. Verified the reviewer's report directly: a raw registration passes every test. http.ServeMux exposes no way to enumerate its patterns, so the invariant cannot be checked at runtime. TestMuxRegistrationsGoThroughHandle parses server.go and asserts every mux.Handle/mux.HandleFunc call site lives inside the handle helper, naming file and line when one does not. The raw-registration mutation now fails it. The doc comment no longer says the route set cannot drift. It covers cooperating registrations, and points at the source-level test for the rest. Adding head: to the spec for /metrics left the user reference and the contributor endpoint table advertising GET alone. Both now state GET and HEAD, and that other methods get a structured 405 with Allow. Refs #2454 Signed-off-by: Mark Chmarny <mark@chmarny.com>
|
Both fixed in 1. MAJOR — you are right, and my verification was the problemI reproduced it exactly as reported: a raw The reason I missed it is that my mutation used
I also narrowed the claim rather than leaving the stronger wording next to a weaker guarantee. The doc comment now says 2. MINOR — HEAD was in the contract but not the docsCorrect, and it is the half that users actually read. Adding
|
|
🌿 Preview your docs: https://nvidia-preview-fix-server-conformance-followups.docs.buildwithfern.com/aicr |
Summary
Closes items 6–9 of #2454 — the
pkg/serverconformance findings from the #2448 review. Items 1, 2, 4 and 5 are in #2450; item 3 is #2451; items 10 and 11 need no action yet.Motivation / Context
#2454 consolidates review findings across #2436 and #2448. This PR takes the API-server group. Two of the four are defects I introduced in #2448 itself.
Fixes: N/A
Related: #2454, #2448, #2112, #2450, #2451
Type of Change
Component(s) Affected
cmd/aicrd,pkg/server)Implementation Notes
Item 6 — plain-text 405 broke the package's own convention
getOnlycalledhttp.Error(w, "method not allowed", 405), writing a bare string. The seven other 405 sites in this package — includinghandleHealthandhandleReady, registered on the same mux — return the structured envelope viaWriteError. A client parsing the error received JSON from/healthand plain text from/metricsfor the identical condition. Now usesWriteErrorwithErrCodeMethodNotAllowed, matching its siblings.Item 9 — HEAD, decided rather than left implicit
getOnlyalso narrowed/metricsto GET, whichpromhttphad not done. RFC 9110 §9.1 makes GET and HEAD mandatory for a general-purpose server, and some monitoring probes reach/metricswith HEAD — so #2448 introduced a behavior regression on a monitoring endpoint while trying to make the contract true.readOnlyallows GET and HEAD, and the spec declareshead:alongsideget:so contract and server agree.The pre-existing HEAD rejection on
/health,/ready, and the v1/v2 endpoints is left alone. It is a standing wart, but widening it as a side effect of a conformance test is the wrong way to decide it. It is now recorded in the test's doc comment so the next reader does not "fix" the assertion without realizing it encodes a deliberate deviation.Item 7 — the route set could not see a future direct-mux route
registeredPathsderived fromconfig.Handlersplus a hand-maintainedsystemRouteslist. A route added directly inNew()— exactly where/health,/readyand/metricsalready live — would appear in neither source and be invisible to all three conformance tests. A hole in a gate built to catch undocumented endpoints.http.ServeMuxexposes no way to enumerate its patterns, soServer.handlenow records each pattern as it registers it.Corrected after review: an earlier revision of this PR claimed the route set "cannot drift". It does not —
routePathsonly records registrations that go throughs.handle, so a rawmux.HandleFuncinNewis served, unrecorded, and invisible to all three tests. My mutation check useds.handle, which proved the recording path worked rather than that the bypass was caught.TestMuxRegistrationsGoThroughHandlenow parsesserver.gowithgo/astand fails, with file and line, if anymux.Handle/mux.HandleFunccall site sits outside the helper.Item 8 — the name promised more than the oracle delivered
TestOpenAPISpecMethodsAreAcceptedfails only on 405, so a declared operation whose handler 500s still passed. The narrowness is deliberate — asserting success codes would make it a fixture treadmill — so the fix is honesty, not a stricter oracle: renamed toTestOpenAPIDeclaredMethodsAreNotRejected, with the docstring stating what it does and does not cover.Not addressed
$ref-only orparameters-only path items misread as declaring no operations) — latent; all 10 spec paths use inlineget/post.go testCWD) — nogo test -cor Bazel lane exists.Both become real only under conditions that do not exist today, and guessing at the fix now would be speculative.
Testing
readOnlyis at 100% coverage;pkg/serverholds at 84.3%.docs/user/api-reference.mdanddocs/contributor/api-server.mdnow documentHEAD /metrics, which the spec declared but the prose did not.Mutation-verified, including the case the first attempt missed. Registering
/debugvias.handlefails the path assertion; registering it with a rawmux.HandleFunc— which passed before review — now fails the source-level guard:TestMetricsMethodRejectionIsStructuredpins the JSON envelope, theAllow: GET, HEADheader, and thatHEAD /metricsreturns 200 rather than 405.Risk Assessment
Rollout notes: Two behavior changes, both on
/metricsand both corrective.HEAD /metricsreturns 200 again, restoring the pre-#2448 behavior that #2448 removed. Non-GET/HEAD methods now return a JSON error envelope instead of a plain-text body, matching every other 405 in the package. Prometheus scraping is unaffected — it uses GET.Checklist
make testwith-race)make lint)git commit -S)