Skip to content

Implement API response contract and custom exception handling#35

Merged
saudzahirr merged 7 commits intomainfrom
feature/api-response-contract
Feb 24, 2026
Merged

Implement API response contract and custom exception handling#35
saudzahirr merged 7 commits intomainfrom
feature/api-response-contract

Conversation

@saudzahirr
Copy link
Copy Markdown
Member

@saudzahirr saudzahirr commented Feb 20, 2026

Summary by CodeRabbit

  • New Features

    • Request ID propagation for all API requests to improve tracing.
    • Global, standardized error responses (structured envelope, validation and generic handlers, consistent error codes and HTTP statuses).
  • Bug Fixes

    • Missing database pool now returns service-unavailable (503) for clearer signaling.
  • Chores

    • Broadened ignore rules to exclude build/dist, egg-info, and additional Python bytecode variants.

@saudzahirr saudzahirr self-assigned this Feb 20, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 20, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c223bb7 and c297cca.

📒 Files selected for processing (1)
  • backend/app/api/middlewares.py

📝 Walkthrough

Walkthrough

Adds request-ID middleware, a hierarchical API exception hierarchy and handlers that return structured JSON error envelopes (including request_id), new response schemas, registers middleware and handlers in the FastAPI app, and replaces a deps HTTPException with ServiceUnavailableException; plus .gitignore expansions.

Changes

Cohort / File(s) Summary
Project config
.gitignore
Broadened ignore patterns: *.egg-info, *.py[cod]; added build/ and dist/.
Exception types
backend/app/core/exceptions.py
Added APIException base and subclasses (ValidationException, AuthenticationException, AuthorizationException, NotFoundException, ConflictException, BusinessLogicException, ServiceUnavailableException) with error codes and HTTP statuses.
Exception handlers
backend/app/core/exception_handlers.py
New handlers: api_exception_handler, validation_exception_handler, general_exception_handler — produce standardized error envelope { error: { code, message, http_status, details, request_id } } and set X-Request-ID.
Request middleware
backend/app/api/middlewares.py
New add_request_id HTTP middleware: reads X-Request-ID or generates UUID4, stores request.state.request_id, and ensures response header.
Schemas
backend/app/schemas/response.py
New Pydantic types: ErrorCode enum, SuccessResponse[T], ErrorDetail, ErrorResponse (includes json_schema example).
App wiring & deps
backend/app/main.py, backend/app/api/deps.py
Registered middleware and global exception handlers in main.py. deps.get_db now raises ServiceUnavailableException when DB pool is uninitialized (replacing prior HTTPException).

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Middleware as add_request_id
    participant Router as FastAPI Handler
    participant DB as deps.get_db
    participant ExceptionHandler as Global Exception Handlers
    participant Response as HTTP Response

    Client->>Middleware: HTTP Request (maybe X-Request-ID)
    Middleware->>Middleware: extract or generate request_id (UUID4)
    Middleware->>Router: call_next(request) with request.state.request_id

    alt Handler calls DB
        Router->>DB: deps.get_db()
        DB-->>Router: connection or raise ServiceUnavailableException
    end

    alt Success
        Router-->>Response: JSONResponse (success) + X-Request-ID
    else APIException
        Router->>ExceptionHandler: APIException
        ExceptionHandler->>ExceptionHandler: build error envelope with request_id
        ExceptionHandler-->>Response: JSONResponse (API status) + X-Request-ID
    else RequestValidationError
        Router->>ExceptionHandler: RequestValidationError
        ExceptionHandler->>ExceptionHandler: parse validation errors, build envelope
        ExceptionHandler-->>Response: JSONResponse (422) + X-Request-ID
    else Unhandled Exception
        Router->>ExceptionHandler: Exception
        ExceptionHandler->>ExceptionHandler: log traceback, build 500 envelope
        ExceptionHandler-->>Response: JSONResponse (500) + X-Request-ID
    end

    Response->>Client: HTTP Response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main changes: implementing API response contracts and custom exception handling, which are central to all modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/api-response-contract

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/API_RESPONSE_CONTRACT.md`:
- Around line 145-165: The fenced code blocks under the "Example
Request/Response with Request ID" section (and the other flagged ranges) are
missing surrounding blank lines and language tags; update each request/response
fence to have a blank line before and after the triple-backticks and add an
appropriate language tag (e.g., ```http for the request/response examples and
```json for JSON bodies) so that all examples follow markdownlint rules and
render correctly.

In `@backend/app/core/exceptions.py`:
- Around line 26-35: The ValidationException currently constructs the base error
with http_status=400 while using ErrorCode.VALIDATION_FAILED; change the
http_status passed in ValidationException.__init__ to 422 so the class aligns
with the API contract and existing Pydantic handler; also update the example
entry in the response schema (in backend/app/schemas/response.py) that documents
VALIDATION_FAILED to show http_status: 422 instead of 400 to keep docs
consistent with the exception class.

In `@backend/app/main.py`:
- Around line 91-94: Replace eager f-string interpolation in logging calls with
lazy %-style formatting: change the logger.warning call that currently uses
f"API Exception: {exc.error_code} - {exc.message}" to use a format string and
pass exc.error_code and exc.message as separate positional args (keeping the
extra={"request_id": request_id, "error_code": exc.error_code} argument). Do the
same refactor for the other logging statements mentioned (the similar logger.*
calls at the other occurrences around the API exception handling blocks) so all
logging uses format strings like "API Exception: %s - %s" with parameters
instead of f-strings.
- Line 24: The code passes settings.DATABASE_DSN (a PostgresDsn object) directly
into ConnectionPool; convert it to a plain string before use. Update the
instantiation of ConnectionPool so it receives str(settings.DATABASE_DSN) (same
fix applied in pre_start.py), ensuring any code that constructs the pool (the
ConnectionPool call in main.py) receives a string DSN rather than the
PostgresDsn object.

In `@build/lib/backend/app/api/deps.py`:
- Around line 11-12: get_db currently raises a plain HTTPException which
bypasses the new standardized error handlers; replace the HTTPException raise in
get_db with raising APIException(status_code=500, detail="Database pool not
initialized") (or equivalent constructor used by your APIException), and
add/import APIException into deps.py so the custom exception middleware will
format the response consistently.

In `@build/lib/backend/app/main.py`:
- Line 3: The imports typing.Any and http_exception_handler are unused in
main.py; remove the unused import statement "from typing import Any" and the
import of http_exception_handler (where it's imported) so the file only imports
symbols that are actually used (e.g., delete or trim the import lines
referencing Any and http_exception_handler and run the linter/type-check to
confirm no references remain).
- Line 26: settings.DATABASE_DSN is a PostgresDsn object, not a plain string, so
passing it directly to ConnectionPool causes a type/serialization issue; update
the ConnectionPool instantiation in main.py to pass str(settings.DATABASE_DSN)
(i.e., convert the PostgresDsn to a string) so
ConnectionPool(settings.DATABASE_DSN) becomes
ConnectionPool(str(settings.DATABASE_DSN)), ensuring ConnectionPool receives a
proper DSN string.
- Around line 88-97: Remove the dead stub function get_request_id_from_context()
from the module: it only imports fastapi/Headers inside itself, catches all
exceptions, and always returns a new UUID, so delete the entire function
definition (def get_request_id_from_context(...)) to eliminate non-functional
code; ensure no remaining references to get_request_id_from_context() exist in
the file before committing.

---

Duplicate comments:
In `@backend/app/schemas/response.py`:
- Around line 56-67: The example in Config.json_schema_extra is inconsistent:
the "error.code" ("VALIDATION_FAILED") doesn't match the canonical HTTP status
chosen elsewhere; update this example in backend/app/schemas/response.py (Config
-> json_schema_extra -> example) so the "error.code", "error.message" and
"error.http_status" reflect the canonical status decided in the exception
hierarchy (e.g., if the canonical status for validation is 422 change
http_status to 422 and keep code "VALIDATION_FAILED" and a validation message;
if the canonical status is 400 change code to "BAD_REQUEST" and adjust message
accordingly), and ensure "details" and "request_id" remain representative.

In `@build/lib/backend/app/core/exceptions.py`:
- Around line 26-35: The ValidationException currently sets http_status=400 in
__init__ (class ValidationException) which conflicts with the API contract;
update ValidationException to use the contract-approved status (e.g.,
http_status=422) and ensure the change is applied both in the source
ValidationException implementation and in this built artifact
(build/lib/backend/app/core/exceptions.py) so they stay in sync, then regenerate
the build artifacts so the compiled module reflects the corrected http_status.

In `@build/lib/backend/app/schemas/response.py`:
- Around line 57-67: The json_schema_extra example in response.py currently
shows an error object with code "VALIDATION_FAILED" but the http_status value is
inconsistent with the canonical status chosen; update the example inside
json_schema_extra so the "http_status" matches the officially chosen status for
VALIDATION_FAILED (e.g., set to 400 if validation errors are 400, or change the
"code" to the status-aligned code), and ensure the "code" and "http_status"
inside the example error object are consistent with the canonical exception
handling used elsewhere.

@saudzahirr
Copy link
Copy Markdown
Member Author

@laraibg786 Please take a look!

@saudzahirr
Copy link
Copy Markdown
Member Author

Resolves #5

Copy link
Copy Markdown
Collaborator

@laraibg786 laraibg786 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a build dir in the diff of this PR. I think you forgot to remove this. Please also check if this is something we want to be included in .gitignore

@laraibg786 laraibg786 linked an issue Feb 21, 2026 that may be closed by this pull request
12 tasks
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/app/main.py`:
- Around line 77-103: The API exception handlers (api_exception_handler,
validation_exception_handler, general_exception_handler) currently log a
request_id and include it in the JSON body but don't set the X-Request-ID
response header; update each handler to read/derive request_id the same way and
include it in the JSONResponse by passing headers={"X-Request-ID": request_id}
so clients receive the header consistently when exceptions bypass middleware.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e89838e and eca94a7.

📒 Files selected for processing (6)
  • .gitignore
  • backend/app/api/deps.py
  • backend/app/api/middlewares.py
  • backend/app/core/exceptions.py
  • backend/app/main.py
  • backend/app/schemas/response.py

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
backend/app/core/exception_handlers.py (1)

15-107: ⚠️ Potential issue | 🟠 Major

Add X-Request-ID header on error responses.

When exceptions occur, the request ID middleware won’t always set the header; clients will miss trace IDs even though the body includes request_id. Add the header directly in each handler’s JSONResponse.

🐛 Proposed fix (apply same pattern to all three handlers)
-    return JSONResponse(
-        status_code=exc.http_status,
-        content=error_response,
-    )
+    return JSONResponse(
+        status_code=exc.http_status,
+        content=error_response,
+        headers={"X-Request-ID": request_id},
+    )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/core/exception_handlers.py` around lines 15 - 107, The error
handlers api_exception_handler, validation_exception_handler, and
general_exception_handler should include the trace ID in the response headers;
after you compute request_id in each handler add the header "X-Request-ID" to
the JSONResponse (e.g., return JSONResponse(..., headers={"X-Request-ID":
request_id} ) ) so clients receive the same request_id shown in the response
body; update each handler (api_exception_handler, validation_exception_handler,
general_exception_handler) to pass the headers dict to JSONResponse.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@backend/app/core/exception_handlers.py`:
- Around line 15-107: The error handlers api_exception_handler,
validation_exception_handler, and general_exception_handler should include the
trace ID in the response headers; after you compute request_id in each handler
add the header "X-Request-ID" to the JSONResponse (e.g., return
JSONResponse(..., headers={"X-Request-ID": request_id} ) ) so clients receive
the same request_id shown in the response body; update each handler
(api_exception_handler, validation_exception_handler, general_exception_handler)
to pass the headers dict to JSONResponse.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eca94a7 and f6e5a10.

📒 Files selected for processing (2)
  • backend/app/core/exception_handlers.py
  • backend/app/main.py

Copy link
Copy Markdown
Collaborator

@laraibg786 laraibg786 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/app/api/middlewares.py`:
- Around line 6-9: The middleware add_request_id currently uses
request.headers.get("X-Request-ID", str(uuid.uuid4())) which will propagate an
empty string if the header exists but is blank; change the logic in
add_request_id to check the header value explicitly (e.g., read raw_header =
request.headers.get("X-Request-ID") and set request_id = raw_header if
raw_header and raw_header.strip() else str(uuid.uuid4())) and assign that to
request.state.request_id so an empty header falls back to a generated UUID.

In `@backend/app/core/exception_handlers.py`:
- Around line 49-56: When building validation details in the exception handler
loop (for error in exc.errors()) avoid leaving the "field" as an empty string
for root-level errors: compute the joined locator string from error["loc"][1:],
and if that join yields "" set the field to None before appending to details;
update the block that constructs the dict (the "field" entry) in the exception
handler module so root-level validation errors use None instead of "".

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 683b216 and a7953fe.

📒 Files selected for processing (8)
  • .gitignore
  • backend/app/api/deps.py
  • backend/app/api/middlewares.py
  • backend/app/core/exception_handlers.py
  • backend/app/core/exceptions.py
  • backend/app/main.py
  • backend/app/schemas/__init__.py
  • backend/app/schemas/response.py

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/app/api/middlewares.py`:
- Around line 6-10: The middleware add_request_id currently uses the raw
X-Request-ID header value (raw_header) when present, which preserves surrounding
whitespace; change it to trim the header before using it: compute a stripped
value (raw_header.strip()) and set request.state.request_id to that stripped
value only if it's non-empty, otherwise generate a new uuid with
str(uuid.uuid4()); update the logic around request.headers.get("X-Request-ID")
and request.state.request_id in add_request_id accordingly.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a7953fe and c223bb7.

📒 Files selected for processing (3)
  • backend/app/api/middlewares.py
  • backend/app/core/exception_handlers.py
  • backend/app/main.py

@saudzahirr saudzahirr merged commit 4dee900 into main Feb 24, 2026
1 check passed
@saudzahirr saudzahirr deleted the feature/api-response-contract branch February 24, 2026 10:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Issue 05: API Response Contract

2 participants