fix(api): classify bulk handler store errors instead of calling them all 500 - #636
Merged
Conversation
…all 500
Bulk handlers wrapped every store error in ServerError, discarding the
classification the store had already made. A malformed JSON Pointer is
the clearest case: ApplyPatchOperations returns a *RequestError carrying
400 patch_failed, the single-threat PATCH surfaces exactly that, and the
bulk route turned it into 500 server_error.
Verified against a running server, same threat and same pointer:
PATCH .../threats/{id} -> 400 patch_failed
"unsupported patch path: /nonexistent~field"
PATCH .../threats/bulk -> 500 server_error
Seven call sites had the pattern, which is why the same run produced a
500 on repositories/bulk as well: threats (patch), assets, repositories
and documents (create and update in the upsert loops). All now route
through StoreErrorToRequestError, as the single-resource handlers
already do. It preserves an existing *RequestError, maps dberrors to
404/409/400, and still falls back to 500 for a genuine server fault.
That fallback matters: the fix must not turn every failure into a client
error. Both directions are covered by the new test, which fails on the
old code and passes on the new.
409 is added to the seven affected operations. StoreErrorToRequestError
returns ConflictError for a duplicate, and none of the bulk operations
documented 409 -- without this the fix would have traded a Zero-500
violation for a documented-status-code violation.
Version 1.6.1 -> 1.6.2.
Fixes #632
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WnrVyySuyJFqcMxB8q3pNc
This was referenced Jul 31, 2026
ericfitz
added a commit
that referenced
this pull request
Jul 31, 2026
…l 500 (#639) #636 converted the bulk PATCH and the BulkUpdate upsert loops to StoreErrorToRequestError but left the four BulkCreate (POST .../bulk) call sites wrapping every store error in ServerError. A duplicate id is the clearest case: the store classifies it as dberrors.ErrDuplicate, a client error, and the bulk create route turned it into 500 server_error. That was the only remaining 500 in CATS run 20260730T220551Z, down from 71. All four sites -- assets, documents, repositories and threats -- now route through StoreErrorToRequestError, and still fall back to 500 for a genuine server fault. Both directions are covered by a new test that fails on the old code and passes on the new. 409 added to POST /threat_models/{threat_model_id}/threats/bulk; the other three already documented it via #636. Verified end-to-end on k3s running 1.6.3: all four bulk creates return 409 on duplicate ids, a unique-id create still returns 201, and the driver error is logged without leaking SQLSTATE to the client. Version 1.6.2 -> 1.6.3. Fixes #638
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.
Fixes #632.
The bug
Bulk handlers wrapped every store error in
ServerError, discarding theclassification the store had already made:
ApplyPatchOperationsreturns a*RequestErrorcarrying 400patch_failedfor a patch document the client got wrong, and
StoreErrorToRequestErrorpreserves it via
errors.As— which is why the single-resource handlers werealways correct. The bulk handlers threw it away.
Verified against a running server, same threat and same pointer:
PATCH .../threats/{id}patch_failed— "unsupported patch path:/nonexistent~field"PATCH .../threats/bulkserver_errorScope
The issue asked whether other
*/bulkhandlers shared the cause. They do —seven call sites across four files, which is also why the same CATS run
produced a 500 on
repositories/bulk:threat_sub_resource_handlers.go— bulk patchasset_sub_resource_handlers.go— bulk create, bulk updaterepository_sub_resource_handlers.go— bulk create, bulk updatedocument_sub_resource_handlers.go— bulk create, bulk updateAll now route through
StoreErrorToRequestError, matching the single-resourcehandlers. It preserves an existing
*RequestError, mapsdberrorsto404/409/400, and still falls back to 500 for a genuine server fault.
Spec change this required
StoreErrorToRequestErrorreturnsConflictErrorfordberrors.ErrDuplicate,and none of the bulk operations documented 409. Without adding it, this fix
would have traded a Zero-500 violation for a documented-status-code violation.
409 (
#/components/responses/Conflict) added to all seven operations, specrevalidated,
api/api.goregenerated. Version 1.6.1 → 1.6.2.Test
TestBulkPatchThreats_ErrorClassificationcovers both directions, because a fixthat turned every failure into a client error would be equally wrong:
Confirmed it is a real regression test by reverting the handler to the buggy
form — it fails there and passes on the fix.
Verification
make lint0 issues ·make build-server1.6.2 ·make test-unit2448 passed,0 failed ·
make validate-openapi0 errors.Not yet exercised against the cluster; that follows with the deploy.
🤖 Generated with Claude Code
https://claude.ai/code/session_01WnrVyySuyJFqcMxB8q3pNc