Skip to main content

FastAPI Debugging

Introduction

Debugging is an essential skill for any developer. When working with FastAPI applications, having effective debugging strategies can save hours of development time and frustration. This guide will walk you through various techniques and tools to debug your FastAPI applications efficiently.

FastAPI, built on top of Starlette and Pydantic, provides several features that make debugging easier than in traditional web frameworks. We'll explore how to leverage these features and combine them with standard Python debugging tools to quickly identify and fix issues in your APIs.

Setting Up for Debugging

Enabling Debug Mode

The first step in effective debugging is enabling FastAPI's debug mode. When running your application with debug mode enabled, FastAPI will automatically reload your application when code changes are detected and provide more detailed error information.

python
import uvicorn
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def root():
return {"message": "Hello World"}

if __name__ == "__main__":
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

The key parameter here is reload=True, which enables the auto-reloading feature. This means that whenever you make changes to your code, the server will automatically restart, allowing you to see the effects immediately.

Using FastAPI's Built-in Debugging Tools

Interactive API Documentation

One of FastAPI's most powerful features for debugging is its interactive documentation. By default, FastAPI generates OpenAPI documentation at /docs and /redoc endpoints.

python
from fastapi import FastAPI

app = FastAPI(
title="My Debugging API",
description="An API with debugging features enabled",
version="0.1.0",
debug=True
)

@app.get("/items/{item_id}")
async def read_item(item_id: int):
return {"item_id": item_id}

Navigate to http://localhost:8000/docs to see the interactive Swagger UI documentation where you can:

  • Test API endpoints directly
  • See detailed request and response models
  • Understand validation errors

This interactive documentation is invaluable for debugging API behavior and request/response patterns.

Request Validation Errors

FastAPI provides detailed validation errors when requests don't match the expected schema. These errors can be extremely helpful for debugging client-side issues.

python
from fastapi import FastAPI
from pydantic import BaseModel, Field

app = FastAPI()

class Item(BaseModel):
name: str
price: float = Field(..., gt=0)
is_offer: bool = False

@app.post("/items/")
async def create_item(item: Item):
return item

If a client sends invalid data, such as a negative price:

json
{
"name": "Screwdriver",
"price": -5.0,
"is_offer": true
}

FastAPI will automatically respond with a detailed error:

json
{
"detail": [
{
"loc": ["body", "price"],
"msg": "ensure this value is greater than 0",
"type": "value_error.number.not_gt",
"ctx": {"limit_value": 0}
}
]
}

This helps you quickly identify what went wrong without additional logging.

Advanced Debugging Techniques

Custom Exception Handlers

Custom exception handlers allow you to control how exceptions are processed and presented to users. This is particularly useful for debugging specific types of errors.

python
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse

app = FastAPI()

class CustomException(Exception):
def __init__(self, name: str):
self.name = name

@app.exception_handler(CustomException)
async def custom_exception_handler(request: Request, exc: CustomException):
return JSONResponse(
status_code=418,
content={
"message": f"Oops! {exc.name} did something wrong.",
"path": request.url.path,
"query_params": str(request.query_params),
"debug_info": {
"request_headers": dict(request.headers),
"client_host": request.client.host if request.client else None,
}
},
)

@app.get("/debug-me")
async def create_error():
raise CustomException(name="DebugExample")

This custom exception handler provides rich debugging information while still being able to present a clean error to end users in production by simply modifying the handler.

Middleware for Debugging

Middleware can be extremely useful for debugging as it allows you to intercept requests and responses. Here's an example of a simple timing middleware:

python
import time
from fastapi import FastAPI, Request

app = FastAPI()

@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
start_time = time.time()

# Process the request
response = await call_next(request)

# Add processing time to response headers
process_time = time.time() - start_time
response.headers["X-Process-Time"] = str(process_time)

# For debugging, print the time taken
print(f"Request to {request.url.path} took {process_time:.4f} seconds")

return response

This middleware logs the time taken for each request, helping you identify performance bottlenecks in your API.

Integrating with Python Debugging Tools

Using Python's debugger (pdb)

The Python Debugger (pdb) is a powerful tool that can be integrated with FastAPI for interactive debugging:

python
from fastapi import FastAPI
import pdb

app = FastAPI()

@app.get("/debug-with-pdb/{item_id}")
async def debug_with_pdb(item_id: int):
# This will pause execution and open an interactive debugger
pdb.set_trace()

result = complex_calculation(item_id)
return {"result": result}

def complex_calculation(value):
# Some complex logic
intermediate = value * 2
final = intermediate + 10
return final

When the endpoint is called, the application will pause execution at the pdb.set_trace() line, and you'll get an interactive console to inspect variables, execute code, and step through the function.

Using VS Code Debugger

If you're using Visual Studio Code, you can set up a launch configuration for debugging FastAPI applications:

json
{
"version": "0.2.0",
"configurations": [
{
"name": "FastAPI",
"type": "python",
"request": "launch",
"module": "uvicorn",
"args": [
"main:app",
"--reload",
"--port",
"8000"
],
"jinja": true
}
]
}

Save this in .vscode/launch.json, then you can set breakpoints in your code and start debugging using VS Code's debug panel.

Logging Strategies for FastAPI

Setting Up Comprehensive Logging

Proper logging is crucial for debugging FastAPI applications, especially in production environments:

python
import logging
from fastapi import FastAPI, Request

# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler("app.log"),
logging.StreamHandler()
]
)

logger = logging.getLogger(__name__)

app = FastAPI()

@app.middleware("http")
async def log_requests(request: Request, call_next):
logger.info(f"Request: {request.method} {request.url}")
try:
response = await call_next(request)
logger.info(f"Response status: {response.status_code}")
return response
except Exception as e:
logger.error(f"Request failed: {str(e)}")
raise

@app.get("/items/{item_id}")
async def read_item(item_id: int):
logger.info(f"Processing item {item_id}")
if item_id == 0:
logger.warning("Zero item_id received, this might cause issues")
return {"item_id": item_id}

This setup logs all requests and responses, making it easier to trace issues even in production environments.

Debugging Database Interactions

Echoing SQL Queries

If you're using SQLAlchemy with FastAPI, you can enable SQL query logging:

python
from fastapi import FastAPI
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

# Create engine with echo=True to log all SQL queries
DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(
DATABASE_URL, echo=True, connect_args={"check_same_thread": False}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

app = FastAPI()

# The rest of your database models and FastAPI app...

With echo=True, all SQL queries will be logged, helping you debug database-related issues.

Real-world Debugging Example: Asynchronous API

Let's look at a more complex example that demonstrates debugging techniques for an asynchronous API:

python
import asyncio
import logging
from fastapi import FastAPI, Depends, HTTPException
from pydantic import BaseModel

# Setup logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

app = FastAPI(debug=True)

class UserInput(BaseModel):
username: str
email: str

# Simulated database
users_db = {}

# Simulated async database operation
async def save_user_to_db(user_data):
logger.debug(f"Saving user: {user_data}")
await asyncio.sleep(1) # Simulating DB delay

# Intentional bug for demonstration: email uniqueness check
for user_id, existing_user in users_db.items():
if existing_user["email"] == user_data["email"]:
logger.error(f"Email already exists: {user_data['email']}")
raise ValueError(f"Email {user_data['email']} already exists")

user_id = len(users_db) + 1
users_db[user_id] = user_data
logger.info(f"User saved with ID: {user_id}")
return user_id

@app.post("/users/")
async def create_user(user: UserInput):
try:
# Convert to dict for simplicity
user_dict = user.dict()

# Debug point 1: Log the incoming data
logger.debug(f"Received user data: {user_dict}")

# Process the user (this may fail)
user_id = await save_user_to_db(user_dict)

# Debug point 2: Log success
logger.debug(f"Successfully created user with ID: {user_id}")

return {"user_id": user_id, "status": "created"}
except ValueError as e:
# Debug point 3: Log the specific error
logger.error(f"Validation error: {str(e)}")
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
# Debug point 4: Log unexpected errors
logger.exception("Unexpected error creating user")
raise HTTPException(status_code=500, detail="Internal server error")

To debug this API:

  1. Send a POST request to create a user
  2. Check the logs to see the flow of data
  3. Send another POST request with the same email
  4. Observe how the error is caught, logged, and returned

This example demonstrates:

  • Strategic logging points
  • Error handling and propagation
  • Debugging asynchronous code
  • Using exceptions for flow control

Common Debugging Pitfalls in FastAPI

Path Parameters vs. Query Parameters

A common source of bugs is confusing path parameters and query parameters:

python
from fastapi import FastAPI

app = FastAPI()

# Path parameter
@app.get("/items/{item_id}")
async def read_item_path(item_id: int):
return {"item_id": item_id, "source": "path"}

# Query parameter
@app.get("/items/")
async def read_item_query(item_id: int):
return {"item_id": item_id, "source": "query"}

This creates a routing conflict! The second route will never be reached because the first one captures all requests to /items/{anything}. To debug this:

  1. Check the API documentation at /docs
  2. Reorder your routes with the most specific ones first
  3. Use different base paths for different parameter types

Dependency Injection Issues

When debugging dependency injection problems:

python
from fastapi import FastAPI, Depends, HTTPException

app = FastAPI()

async def verify_token(token: str):
if token != "secret-token":
raise HTTPException(status_code=401, detail="Invalid token")
return token

async def get_current_user(token: str = Depends(verify_token)):
# In a real app, you'd decode the token and get the user
return {"username": "john_doe"}

@app.get("/me")
async def read_users_me(current_user: dict = Depends(get_current_user)):
return current_user

If you're seeing unexpected authentication failures, you can add debug prints to your dependency functions or use an HTTP client like curl or Postman to ensure you're sending the correct token.

Summary

Debugging FastAPI applications requires a combination of:

  1. Built-in tools: Using FastAPI's automatic validation, interactive docs, and exception handling
  2. Python debugging: Leveraging pdb, IDE debuggers, and logging
  3. Strategic logging: Adding context-aware logs at key points in your application
  4. Middleware: Intercepting requests and responses for debugging purposes
  5. Custom error handlers: Creating detailed error responses for easier troubleshooting

By mastering these debugging techniques, you'll be able to identify and fix issues in your FastAPI applications more efficiently, leading to more robust and reliable APIs.

Additional Resources

Exercises

  1. Create a FastAPI endpoint that intentionally produces an error, then build a custom exception handler to provide detailed debugging information.

  2. Implement logging middleware that records request bodies for POST/PUT requests (be careful not to log sensitive information in production).

  3. Write an API endpoint that performs a complex calculation and debug it using pdb or your IDE's debugger.

  4. Set up comprehensive logging for a FastAPI application with different log levels for different types of information.

  5. Create a test endpoint that makes database queries and enable SQL query logging to see what's happening under the hood.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)