fix: answer a rejected id as a client error, not a server fault - #216
Conversation
Every controller carries @validated and bounds its id path variables with @min(1). A request that violated one of those bounds was not answered as the client error it is: ConstraintViolationException, which Spring's method-validation proxy raises for a method-parameter constraint, had no handler, so it fell through to handleGenericException and was answered 500 with the message masked to "An unexpected error occurred". The caller could not tell its own malformed request from a server fault, and the violation was logged at ERROR with a stack trace. The two validation handlers already present cover different mechanisms and never see this one: MethodArgumentNotValidException is raised for a @Valid @RequestBody, and HandlerMethodValidationException only where the controller is not proxied by @validated. A handler of its own now answers 400 and names the rejected parameter, in the manner body-field violations already are named. Only the last node of the violation's property path is quoted back, since the validator reports it as method.parameter and the caller has no business knowing which method received its request. docs/openapi/viron-api.json documents the 400 on all 27 routes that can now produce one, through a shared ValidationError response, and expresses the @min bound itself as a minimum on the parameter schemas. Doing so surfaced four location routes whose parameters were referenced from components under a name that does not match their path template: /locations/{id} declared locationId, and the environment- and grid-scoped listings declared id. A client generated from the spec would have built the wrong URL for each. They are declared inline now, as the equivalent grid route already was. OpenApiSpecDriftTest gains the guard that found them. The existing check compares springdoc's routes, which say nothing about what a route answers to a rejected input; the new ones walk the controllers' own annotations, so neither the 400 nor the bound can go undocumented again. Closes #214 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Self-review rubric, scored against the diff and command output rather than impression.
Judgment calls left for a reviewer
Observed outside the diff, not acted on
This review was drafted during a Gardener session (https://github.com/Stephenson-Software/gardener). drafted by Claude on behalf of Daniel Stephenson |
|
Autonomous merge is being held on this PR, and the reason is a path match rather than anything about the
Everything else the merge gate asks for is done:
No further work is queued against this branch. Merging it needs only a codeowner's word. This comment was drafted during a Gardener session (https://github.com/Stephenson-Software/gardener). drafted by Claude on behalf of Daniel Stephenson |
Summary
@Min(1)bound its controller declares is now answered400with an
ErrorResponsenaming the rejected parameter, rather than500with the message masked to"An unexpected error occurred".
jakarta.validation.ConstraintViolationException— the type Spring'smethod-validation proxy raises for a method-parameter constraint, because every controller carries
@Validated— had no handler inGlobalExceptionHandlerand reachedhandleGenericException.WARNas a client error, in place of anERRORwith a stack trace.method.parameter(getEntityById.id), and which handler method received the request is not thecaller's concern.
docs/openapi/viron-api.jsondocuments the400on the 27 routes that can now produce one, througha shared
#/components/responses/ValidationError, and expresses the@Minbound itself asminimum: 1on the parameter schemas.OpenApiSpecDriftTestgains two checks that read the controllers' annotations directly, so neitherthe
400nor the bound can fall out of the contract again.A spec bug the new guard found
Four location routes referenced a shared parameter component whose
namedoes not match their pathtemplate, which the route-level comparison in the existing drift test cannot see:
GET /api/v1/locations/{id}locationId{id}GET /api/v1/locations/environment/{environmentId}id{environmentId}GET /api/v1/locations/grid/{gridId}id{gridId}GET /api/v1/locations/grid/{gridId}/unoccupiedid{gridId}A client generated from the spec would have built the wrong URL for each. They are declared inline
now, matching how
GET /api/v1/grids/environment/{environmentId}already declared its own. ThePostman collection was checked and was already correct, so it is unchanged.
Test plan
mvn -B test— 454 tests, 0 failures (443 before this change, 11 added)PathVariableValidationTestreverted against the unfixed handler: all 6 original cases fail withexpected:<400> but was:<500>, and pass with the handler in placeboundedPathVariablesDeclareTheirBoundInTheSpecfailed on the four mismatched parameters abovebefore they were corrected, and passes now
python3 -m pytest(pytest 7.1.3) andpytest(pytest 9.0.3) — 108 tests each, 0 failuresdocs/openapi/viron-api.jsonparses, and the spec's route set is unchangedNotes on scope
and none of its methods branch on
500as distinct from any other non-2xxanswer. Its suite wasrun regardless and is green.
documented answers rather than new ones.
GET /api/v1/environments/name/{name}are untouched: neither validates aninput, so neither can raise the exception this handler answers.
Deferred this cycle
The open backlog was reviewed and left alone, with reasons:
body that a product decision is required before any implementation is correct, so neither is work
this loop should pick.
expected concurrency that is not recorded anywhere in the repository; it also wants a considered
pair of settings rather than the single handler this PR adds.
added here covers a further slice of its second gap, a spec-drift check in CI, without wiring a
generator.
the implemented surface. It is unrelated to this fix and was kept out to avoid unrelated edits in a
bug-fix PR.
Closes #214
This PR description was drafted during a Gardener session (https://github.com/Stephenson-Software/gardener).
drafted by Claude on behalf of Daniel Stephenson