# How Custom Exception Types Map to HTTP Status Codes in FastAPI Handlers

> Learn how custom exception types map to HTTP status codes in FastAPI handlers. Open Notebook automatically translates Python exceptions to their correct HTTP status for cleaner API error handling.

- Repository: [Luis Novo/open-notebook](https://github.com/lfnovo/open-notebook)
- Tags: how-to-guide
- Published: 2026-06-28

---

**Open Notebook registers dedicated exception handlers in [`api/main.py`](https://github.com/lfnovo/open-notebook/blob/main/api/main.py) that automatically translate domain-specific Python exceptions into appropriate HTTP status codes**, ensuring that raising `NotFoundError` returns a 404, `AuthenticationError` returns a 401, and other custom errors map to their respective status codes without additional boilerplate in route handlers.

In the Open Notebook project, custom exception types defined in [`open_notebook/exceptions.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/exceptions.py) create a clean domain-driven error hierarchy. The FastAPI application centralizes HTTP response mapping in [`api/main.py`](https://github.com/lfnovo/open-notebook/blob/main/api/main.py), where each exception type registers a specific handler that converts Python errors into properly formatted JSON responses with correct status codes and CORS headers.

## The Exception Hierarchy in [`open_notebook/exceptions.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/exceptions.py)

All domain-specific exceptions inherit from `OpenNotebookError`, which extends the base Python `Exception` class. This hierarchy allows the API layer to distinguish between application-specific errors and unexpected Python exceptions while enabling granular error handling.

The following concrete exception types represent distinct failure modes:

- **NotFoundError** – Raised when a requested resource does not exist
- **InvalidInputError** – Raised when request validation fails
- **AuthenticationError** – Raised when credentials are missing or invalid
- **RateLimitError** – Raised when rate limiting thresholds are exceeded
- **ConfigurationError** – Raised when the application configuration is invalid
- **NetworkError** – Raised when upstream network requests fail
- **ExternalServiceError** – Raised when external AI services return errors

```python

# open_notebook/exceptions.py

class OpenNotebookError(Exception):
    """Base exception for all Open Notebook errors."""
    pass

class NotFoundError(OpenNotebookError):
    """Raised when a requested resource is not found."""
    pass

class AuthenticationError(OpenNotebookError):
    """Raised when authentication fails."""
    pass

```

## Registering Exception Handlers in [`api/main.py`](https://github.com/lfnovo/open-notebook/blob/main/api/main.py)

The FastAPI application registers explicit exception handlers for each custom type using the `@app.exception_handler()` decorator. According to the Open Notebook source code, each handler constructs a `JSONResponse` with the appropriate HTTP status code, a `detail` field containing the exception message, and CORS headers via the `_cors_headers(request)` helper.

### Client Errors (4xx)

Domain exceptions indicating client-side problems map to standard 4xx status codes according to the following implementation in [`api/main.py`](https://github.com/lfnovo/open-notebook/blob/main/api/main.py):

- **404 Not Found**: `NotFoundError` (lines 217-224)
- **400 Bad Request**: `InvalidInputError` (lines 226-233)
- **401 Unauthorized**: `AuthenticationError` (lines 235-242)
- **429 Too Many Requests**: `RateLimitError` (lines 244-251)
- **422 Unprocessable Entity**: `ConfigurationError` (lines 253-260)

```python

# api/main.py

@app.exception_handler(NotFoundError)
async def not_found_error_handler(request: Request, exc: NotFoundError):
    return JSONResponse(
        status_code=404,
        content={"detail": str(exc)},
        headers=_cors_headers(request),
    )

@app.exception_handler(AuthenticationError)
async def authentication_error_handler(request: Request, exc: AuthenticationError):
    return JSONResponse(
        status_code=401,
        content={"detail": str(exc)},
        headers=_cors_headers(request),
    )

```

### Server and External Errors (5xx)

Exceptions indicating infrastructure or upstream failures map to 5xx status codes:

- **502 Bad Gateway**: `NetworkError` (lines 262-269) and `ExternalServiceError` (lines 271-278)
- **500 Internal Server Error**: Generic `OpenNotebookError` catch-all (lines 280-287)

```python

# api/main.py

@app.exception_handler(NetworkError)
async def network_error_handler(request: Request, exc: NetworkError):
    return JSONResponse(
        status_code=502,
        content={"detail": str(exc)},
        headers=_cors_headers(request),
    )

@app.exception_handler(OpenNotebookError)
async def open_notebook_error_handler(request: Request, exc: OpenNotebookError):
    return JSONResponse(
        status_code=500,
        content={"detail": str(exc)},
        headers=_cors_headers(request),
    )

```

## Ensuring CORS Compliance with `_cors_headers`

All exception handlers delegate to a shared helper function `_cors_headers(request)` defined earlier in [`api/main.py`](https://github.com/lfnovo/open-notebook/blob/main/api/main.py). This ensures that error responses include proper CORS headers even when the exception occurs before the `CORSMiddleware` processes the request, preventing browser-side errors during cross-origin requests.

```python

# api/main.py

def _cors_headers(request: Request):
    # Returns appropriate CORS headers based on the request

    return {
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
        "Access-Control-Allow-Headers": "Content-Type, Authorization",
    }

```

## Practical Usage in Route Handlers

With the mapping centralized in [`api/main.py`](https://github.com/lfnovo/open-notebook/blob/main/api/main.py), route handlers remain clean and declarative. Developers simply raise the appropriate domain exception, and FastAPI automatically invokes the registered handler.

```python

# api/routers/sources.py

from open_notebook.exceptions import NotFoundError

@router.get("/{source_id}")
async def get_source(source_id: str):
    try:
        return await Source.get(source_id)
    except NotFoundError:
        # Automatically returns HTTP 404 with JSON detail

        raise

```

When `NotFoundError` bubbles up from the service layer, FastAPI invokes `not_found_error_handler` at lines 217-224 of [`api/main.py`](https://github.com/lfnovo/open-notebook/blob/main/api/main.py), returning a 404 response with JSON content `{"detail": "Resource not found"}` and proper CORS headers.

## Summary

- **Open Notebook** defines a hierarchy of domain-specific exceptions in [`open_notebook/exceptions.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/exceptions.py) inheriting from `OpenNotebookError`.
- **Exception handlers** in [`api/main.py`](https://github.com/lfnovo/open-notebook/blob/main/api/main.py) map each custom type to a specific HTTP status code using `@app.exception_handler()` decorators.
- **Status code mappings** include 404 for `NotFoundError`, 401 for `AuthenticationError`, 429 for `RateLimitError`, 422 for `ConfigurationError`, 502 for network/external errors, and 500 for generic failures.
- **CORS compliance** is ensured via the `_cors_headers(request)` helper, which adds headers to all error responses.
- **Route handlers** remain clean by raising domain exceptions directly, letting the registered handlers manage HTTP translation automatically.

## Frequently Asked Questions

### How does FastAPI know which status code to return for custom exceptions?

FastAPI uses the `@app.exception_handler()` decorator to register specific functions for each exception type. When an exception is raised, FastAPI checks the registry and invokes the matching handler. In Open Notebook, handlers in [`api/main.py`](https://github.com/lfnovo/open-notebook/blob/main/api/main.py) explicitly set the `status_code` parameter when constructing `JSONResponse` objects, creating a direct mapping between exception types and HTTP codes.

### Can I raise these exceptions from anywhere in the application?

Yes. As long as the exception inherits from the custom classes defined in [`open_notebook/exceptions.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/exceptions.py), raising it from service layers, repositories, or utility functions will trigger the same handler. The exception propagates up the call stack until FastAPI catches it and invokes the registered handler in [`api/main.py`](https://github.com/lfnovo/open-notebook/blob/main/api/main.py).

### Why map both `NetworkError` and `ExternalServiceError` to 502?

Both exceptions indicate upstream failures that are outside the client's control. According to the Open Notebook source code, `NetworkError` handles generic connectivity issues while `ExternalServiceError` specifically handles AI service failures. Using 502 Bad Gateway for both communicates that the API itself is functioning, but an upstream dependency is unavailable or returning errors.

### What happens if I raise an exception not in the custom hierarchy?

Any exception that does not inherit from `OpenNotebookError` (or one of its specific subclasses) will not match the custom handlers in [`api/main.py`](https://github.com/lfnovo/open-notebook/blob/main/api/main.py). FastAPI will return a standard 500 Internal Server Error without the custom JSON formatting or CORS headers, unless you have configured additional default exception handlers.