Mapping Custom Exceptions to HTTP Status Codes in the Open Notebook FastAPI API

Open Notebook maps its domain-specific custom exceptions to precise HTTP status codes using FastAPI's global exception handlers, ensuring API consumers receive consistent, actionable error responses instead of generic 500 errors.

Open Notebook uses a structured error handling strategy to translate internal domain errors into standard HTTP responses. By defining a custom exception hierarchy in open_notebook/exceptions.py and registering dedicated handlers in the API layer, the codebase maintains a strict separation between business logic and transport concerns. This article explains how the project implements mapping custom exceptions to HTTP status codes using FastAPI's exception handler registry.

The Custom Exception Hierarchy

Domain errors are defined as specific Python exception classes in open_notebook/exceptions.py. Each class represents a distinct failure condition, allowing the service layer to communicate intent without knowing HTTP specifics.

Custom Exception HTTP Status Meaning
NotebookNotFound 404 Not Found Requested notebook ID does not exist.
InvalidSource 400 Bad Request Malformed or unsupported source file/URL provided.
AuthenticationError 401 Unauthorized Missing or invalid credentials for an AI provider.
PermissionDenied 403 Forbidden Authenticated user lacks permission to modify the resource.
RateLimitExceeded 429 Too Many Requests Client exceeded the allowed request quota.
ServerError 500 Internal Server Error Unhandled runtime failure base class.

These inherit from a common base where appropriate, allowing handlers to catch specific errors or broad categories.

Global Exception Handler Registration

The actual mapping custom exceptions to HTTP status codes occurs in api/exception_handlers.py (or alternatively within api/main.py). Here, FastAPI's @app.exception_handler decorator binds each Python exception to a function that returns a JSONResponse with the appropriate status code.

The project uses a private _json_error utility to enforce a consistent error payload structure across all handlers:


# api/exception_handlers.py

from fastapi import Request, FastAPI
from fastapi.responses import JSONResponse
from open_notebook.exceptions import (
    NotebookNotFound,
    InvalidSource,
    AuthenticationError,
    PermissionDenied,
    RateLimitExceeded,
    ServerError,
)
import logging

logger = logging.getLogger(__name__)
app = FastAPI()

def _json_error(message: str, status_code: int):
    """Builds a standardized JSON error response."""
    return JSONResponse(
        status_code=status_code,
        content={"detail": message},
    )

@app.exception_handler(NotebookNotFound)
async def notebook_not_found_handler(request: Request, exc: NotebookNotFound):
    return _json_error(str(exc), 404)

@app.exception_handler(InvalidSource)
async def invalid_source_handler(request: Request, exc: InvalidSource):
    return _json_error(str(exc), 400)

@app.exception_handler(AuthenticationError)
async def auth_error_handler(request: Request, exc: AuthenticationError):
    return _json_error(str(exc), 401)

@app.exception_handler(PermissionDenied)
async def permission_denied_handler(request: Request, exc: PermissionDenied):
    return _json_error(str(exc), 403)

@app.exception_handler(RateLimitExceeded)
async def rate_limit_handler(request: Request, exc: RateLimitExceeded):
    return _json_error(str(exc), 429)

@app.exception_handler(ServerError)
async def server_error_handler(request: Request, exc: ServerError):
    logger.error("Unhandled server error: %s", exc)
    return _json_error("An unexpected error occurred.", 500)

Routers are imported after these handlers are registered, ensuring any exception raised downstream is automatically intercepted and transformed.

Practical Implementation

Raising Exceptions from the Service Layer

Business logic in open_notebook/services/notebook_service.py raises custom exceptions to signal failure conditions. This keeps the service layer agnostic of HTTP transport details:


# open_notebook/services/notebook_service.py

from open_notebook.exceptions import NotebookNotFound

async def get_notebook(notebook_id: str):
    notebook = await repository.fetch_notebook(notebook_id)
    if notebook is None:
        raise NotebookNotFound(f"Notebook '{notebook_id}' does not exist.")
    return notebook

Automatic HTTP Response Conversion

API routes in api/routers/notebooks.py call these services directly. When get_notebook raises NotebookNotFound, the global handler converts it to a 404 response automatically:


# api/routers/notebooks.py

from fastapi import APIRouter
from open_notebook.services.notebook_service import get_notebook

router = APIRouter()

@router.get("/{notebook_id}")
async def read_notebook(notebook_id: str):
    # Raises NotebookNotFound if missing; handler returns 404

    notebook = await get_notebook(notebook_id)
    return {"data": notebook}

The resulting HTTP exchange looks like this:


GET /api/notebooks/unknown-id HTTP/1.1
Host: localhost

HTTP/1.1 404 Not Found
Content-Type: application/json

{
  "detail": "Notebook 'unknown-id' does not exist."
}

Summary

  • Open Notebook defines domain-specific exceptions in open_notebook/exceptions.py to represent business logic errors like missing notebooks or authentication failures.
  • FastAPI exception handlers in api/exception_handlers.py map each custom exception to a specific HTTP status code (e.g., 404, 401, 429) using the @app.exception_handler decorator.
  • The service layer raises these exceptions naturally, while the API layer handles the translation to JSON responses, ensuring a clean separation of concerns.
  • A utility function _json_error standardizes the error payload structure across all mapped exceptions.

Frequently Asked Questions

What is the benefit of using custom exceptions instead of HTTPException directly?

Custom exceptions keep the core business logic independent of the web framework. If the transport layer changes from HTTP to a message queue or CLI, the domain errors remain valid without refactoring status code logic scattered through services.

Where are the exception handlers registered in Open Notebook?

Handlers are registered in api/exception_handlers.py using FastAPI's @app.exception_handler decorator. This module is imported and initialized before API routers are included, ensuring global coverage for all endpoint code.

How can I add a new custom exception and status code mapping?

First, define the new exception class in open_notebook/exceptions.py. Then add a handler function in api/exception_handlers.py decorated with @app.exception_handler(YourNewException), returning _json_error(str(exc), your_status_code). Restart the application to load the new mapping.

What happens if an exception is not mapped to a handler?

Unmapped exceptions bubble up as standard Python exceptions. FastAPI's default behavior catches these and returns a 500 Internal Server Error without a structured JSON body, bypassing the _json_error formatting and logging defined for known errors.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →