Add typed exception subclasses for the JSON API error envelope - #96
Open
ramonski wants to merge 2 commits into
Open
Add typed exception subclasses for the JSON API error envelope#96ramonski wants to merge 2 commits into
ramonski wants to merge 2 commits into
Conversation
Introduce six typed subclasses of APIError so route code can raise a
specific error class instead of calling api.fail(status, msg) with a
magic number:
400 BadRequestError
401 UnauthorizedError
403 ForbiddenError
404 NotFoundError
409 ConflictError
422 ValidationError
Each subclass carries its own default HTTP status; the previous
APIError(status, message) positional signature becomes
APIError(message, status=None) so typed subclasses can be raised as
raise NotFoundError("...") without repeating the status number.
Backward compatibility:
- All typed errors inherit APIError. Any existing except APIError:
handler catches every subclass.
- api.fail(status, msg) still works and still raises APIError with the
runtime status. Downstream callers do not have to change.
- APIError.setStatus(x) alias retained for the same reason.
Converts every api.fail() and raw APIError() call site inside
senaite.jsonapi itself to the typed form:
- request.get_request_data: BadRequestError (was APIError(400))
- v1/routes/content.get: NotFoundError for unknown resource
- v1/routes/content.action: BadRequestError for unknown API member
(was api.fail(500), which was misleading: the client asked for an
unknown action, that is a 4xx, not a 5xx)
- v1/routes/push: BadRequest/Unauthorized/NotFound as appropriate
(was api.fail(500) for every failure mode, mixing client errors
with server errors under one status)
- v1/routes/users.login: UnauthorizedError (was api.fail(401))
- api.check_permission: Unauthorized/ForbiddenError
Route-shape update for push.rst: the "non-registered adapter" case
now returns 404 (correct: no consumer with that name is registered),
where it previously returned 500.
Depends on senaite/senaite.core#2998 for the JSON error envelope to
actually surface the exception class name in the response body as a
'type' field. Without #2998, only the HTTP status changes are
visible; the type field is discarded by the current handle_errors
decorator.
This was referenced Jul 25, 2026
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.
Depends on senaite/senaite.core#2998
The
typefield in the JSON error envelope this PR populates only reaches the client after #2998 lands (it fixesbika.lims.jsonapi.handle_errorsto include the exception class name in the response body). Without #2998, the HTTP status changes here are visible but thetypefield is discarded by the current error handler. Merge #2998 first.Description of the issue/feature this PR addresses
Every route in
senaite.jsonapiusedapi.fail(status, msg)with a magic number for the HTTP status. That is fine when there is one call site, but with 8+ different failure modes across the routes it becomes hard to review whether the right status is used, and clients cannot distinguish "not found" from "unauthorized" from a validation error without parsing the free-formmessagetext.What this PR does
Introduces six typed subclasses of
APIErrorinsenaite.jsonapi.exceptions:BadRequestErrorUnauthorizedErrorForbiddenErrorNotFoundErrorConflictErrorValidationErrorEach subclass carries its own default HTTP status; the previous
APIError(status, message)positional signature becomesAPIError(message, status=None)so typed subclasses can be raised asraise NotFoundError("...")without repeating the status number.Converts every
api.fail()and rawAPIError()call site insidesenaite.jsonapiitself to the typed form (see commit for the full list). One tightening worth calling out:v1/routes/push.pyusedapi.fail(500, ...)for every failure mode, mixing "no data sent" (client error) with "consumer raised an exception" (server error) under one status. Now split:BadRequestErrorfor missing/malformed input,NotFoundErrorfor a missing consumer,APIError(..., status=500)only for consumer-side exceptions. Route-shape update inpush.rstreflects the 404 for the non-registered adapter case.v1/routes/content.pyaction route usedapi.fail(500, "API has no member named X")for unknown action names. That is a 4xx (caller asked for something we don't offer), nowBadRequestError.Backward compatibility
APIError. Anyexcept APIError:handler catches every subclass.api.fail(status, msg)still works and still raises plainAPIErrorwith the runtime status. Downstream callers do not have to change.APIError.setStatus(x)alias retained for the same reason.success: false, message: ...envelope; the newtypefield is additive (once #2998 lands).Coverage
New doctest
tests/doctests/typed_exceptions.rst:APIError(so existing catch-all still works)status=kwarg overrides the class defaultAPIErroracceptsstatus=for the legacyapi.failpath/registryreturns 401, non-Manager/registryreturns 403, unknown resource returns 404Existing doctests (
read,create,push,search,users,login,bearer,api_settings,api_serialization,security_fixes, …) still pass. 20 total, 0 failures.Base branch
Stacked on top of #95 (
refactor/api-serialization). Merge order: #92 → #93 → #94 → #95 → #96. Also blocks on senaite/senaite.core#2998 for thetypefield to reach the client.Verify
--
I confirm I have tested the PR thoroughly and coded it according to PEP8 standards.