refactor(whitelist): map domain errors via central exception registry#517
refactor(whitelist): map domain errors via central exception registry#517abdulrafey1 wants to merge 4 commits into
Conversation
|
@claude-review |
|
Claude finished @abdulrafey1's task in 3m 6s —— View job Review: PR #517 — map whitelist errors via central exception registry
This is a clean, well-scoped refactor that lands exactly what the linked issue asks for. The inline What's good
Suggestions (non-blocking)
Assessment: Approve in spirit — solid refactor, good test coverage, matches the codebase's exception-handling pattern. Only the |
| try: | ||
| await session.commit() | ||
| except IntegrityError as exc: | ||
| await session.rollback() | ||
| logger.warning("Whitelist insert conflict for value %s: %s", normalized, exc) | ||
| raise WhitelistEntryAlreadyExists(f"Entry already exists: {normalized}") from exc |
There was a problem hiding this comment.
Medium — IntegrityError is translated to "already exists" unconditionally.
IntegrityError covers any integrity constraint, not just the unique-value index — e.g. an added_by_id foreign-key violation or a NOT NULL breach would also land here and be reported to the client as 409 Entry already exists, which is misleading. In practice added_by_id comes from an authenticated user so an FK failure is unlikely, and the unique index is the only realistic racing constraint, so this isn't a regression (the old inline catch did the same). But since this path is being reworked, consider narrowing it — e.g. inspect exc.orig / the constraint name and only map the unique-value violation to WhitelistEntryAlreadyExists, re-raising anything else so it surfaces as a 500 rather than a wrong 409.
Not blocking. The rollback + contextual logger.warning added here is a real improvement over the old catch that logged nothing.
f8eb778 to
aefedb8
Compare
| raise InvalidWhitelistValue(f"Invalid email format: {value}") from exc | ||
| entry_type = "email" | ||
|
|
||
| result = await session.exec(select(WhitelistedEmail).where(WhitelistedEmail.value == normalized)) |
There was a problem hiding this comment.
Now that value has unique=True and the commit below translates IntegrityError into WhitelistEntryAlreadyExists, this pre-check SELECT is a redundant round trip.
| await session.commit() | ||
| try: | ||
| await session.commit() | ||
| except IntegrityError as exc: |
There was a problem hiding this comment.
This blanket translation assumes every IntegrityError is a duplicate value, but the insert can also violate the FK on added_by_id (e.g. the user was deleted mid-request). On Postgres that failure would surface as a 409 "Entry already exists" and get logged as an insert conflict, which is misleading.
| @@ -58,7 +72,7 @@ async def remove_entry(session: AsyncSession, *, entry_id: int) -> None: | |||
| result = await session.exec(select(WhitelistedEmail).where(WhitelistedEmail.id == entry_id)) | |||
There was a problem hiding this comment.
nit: select(...).where(WhitelistedEmail.id == entry_id) plus one_or_none() is the long way to spell await session.get(WhitelistedEmail, entry_id), which is the idiomatic primary-key lookup and also uses the identity map.
9497656 to
72ef068
Compare
Introduce HTTP-agnostic whitelist domain exceptions and register their type->status mappings in the API layer, dropping the inline try/except from the route handlers. The service now owns the IntegrityError race translation (rollback + log + re-raise). Closes
aefedb8 to
9ee05c3
Compare
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
9ee05c3 to
1788215
Compare
Closes: Whitelist routes map domain errors with inline per-route try/except
What
Migrate
sparkth/api/v1/whitelistoff inlinetry/except → HTTPExceptiononto the centralized exception→HTTP registry from #493 (PR #513), so HTTP-status decisions live in one API-layer mapping instead of being duplicated (and message-string-matched) inside each route.Changes
refactor(whitelist): split the two single-file whitelist modules into packages —sparkth/api/v1/whitelist/(routes.py+__init__.py) andsparkth/services/whitelist/(service.py,exceptions.py,__init__.py) — with back-compat re-exports so existing import paths are unchanged.refactor(whitelist): add HTTP-agnostic domain exceptions (WhitelistErrorbase +InvalidWhitelistValue,WhitelistEntryAlreadyExists,WhitelistEntryNotFound); the service raises them instead ofValueError.refactor(whitelist): move theIntegrityErrorunique-index race backstop into the service (rollback + log-with-context + re-raise asWhitelistEntryAlreadyExists), fixing the old inline catch that logged nothing.refactor(api): register the three concrete exception→status mappings (409/404/422) at import in the whitelist API package viaregister_exception_handler; drop alltry/except/HTTPExceptionfrom the route handlers.test(whitelist): switch service tests to the new exception types, add anIntegrityErrorrace-path test (asserts rollback + the conflict log), assert exact API error bodies, and assert the mappings are registered.How to Test
make test.backend.pytest— full suite passes (1364 passed). New/updated coverage intests/services/test_whitelist.pyandtests/api/v1/test_whitelist.py.make mypy— clean (--strict).make lint.backendandmake lint.format.backend check=1— both clean.POST /api/v1/whitelist/with a value already present →409{"detail": "Entry already exists: <value>"}.POST /api/v1/whitelist/with an invalid value (e.g.not-valid) →422{"detail": "Invalid email format: not-valid"}.DELETE /api/v1/whitelist/{missing_id}→404{"detail": "Whitelist entry not found: <id>"}.Notes
rafey/exception-to-http-code-mapping(PR feat(api): add centralized exception-to-http mapping mechanism #513), notmain. Merge feat(api): add centralized exception-to-http mapping mechanism #513 first.404detailbody changes from the old hard-coded"Whitelist entry not found"to"Whitelist entry not found: <id>". The old delete route discarded the service message; the new path rendersstr(exc), which is consistent with the409/422bodies (they already echo the offending value). Non-breaking — status is still404and no consumer reads this body.tests/core/exceptions/test_handlers.py::test_global_registry_ships_emptyis deleted here. Its premise ("the registry ships empty — this issue delivers only the mechanism") is deliberately ended by this PR: whitelist is the registry's first consumer, registering mappings at import. The_restore_registryfixture docstring was updated to match.This description was written with the assistance of an LLM (Claude).