FastAPI 404 Not Found Error - Complete Debugging Guide
FastAPI 404 errors are among the most common issues developers encounter when building APIs. This guide provides systematic solutions to identify and fix 404 errors in your FastAPI applications.
Common 404 Error Scenarios
1. Route Path Mismatches
Problem: URL path doesn't match route definition
# Route definition
@app.get("/users/{user_id}")
async def get_user(user_id: int):
return {"user_id": user_id}
# These will cause 404 errors:
# GET /user/123 (missing 's')
# GET /users/ (missing user_id)
# GET /Users/123 (wrong case)
Solution:
# Correct route definitions
@app.get("/users/{user_id}")
async def get_user(user_id: int):
return {"user_id": user_id}
@app.get("/users/") # Handle list endpoint separately
async def list_users():
return {"users": []}
2. HTTP Method Mismatches
Problem: Using wrong HTTP method for the route
# Route only accepts POST
@app.post("/users/")
async def create_user(user_data: dict):
return user_data
# This will cause 404:
# GET /users/ (method not allowed, returns 404 in some cases)
Solution:
# Define routes for different methods
@app.get("/users/")
async def list_users():
return {"users": []}
@app.post("/users/")
async def create_user(user_data: dict):
return user_data
3. Missing Route Includes
Problem: Router not included in main app
# user_router.py
from fastapi import APIRouter
router = APIRouter()
@router.get("/users/")
async def get_users():
return {"users": []}
# main.py - WRONG (router not included)
from fastapi import FastAPI
app = FastAPI()
# Missing: app.include_router(router)
Solution:
# main.py - CORRECT
from fastapi import FastAPI
from user_router import router as user_router
app = FastAPI()
app.include_router(user_router, prefix="/api", tags=["users"])
4. Path Parameter Type Issues
Problem: Path parameter type validation fails
@app.get("/users/{user_id}")
async def get_user(user_id: int):
return {"user_id": user_id}
# These cause 404 (not validation error):
# GET /users/abc (string instead of int)
# GET /users/123.45 (float instead of int)
Solution:
from typing import Union
from fastapi import HTTPException
@app.get("/users/{user_id}")
async def get_user(user_id: str): # Accept string first
try:
user_id_int = int(user_id)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid user ID format")
return {"user_id": user_id_int}
# Or use Union types for flexibility
@app.get("/items/{item_id}")
async def get_item(item_id: Union[int, str]):
return {"item_id": item_id}
Debugging Methods
1. Enable Route Listing
from fastapi import FastAPI
app = FastAPI()
# Add this after all routes are defined
@app.on_event("startup")
async def startup_event():
routes = []
for route in app.routes:
if hasattr(route, 'methods'):
routes.append(f"{list(route.methods)} {route.path}")
print("Available routes:")
for route in routes:
print(f" {route}")
2. Add Debugging Middleware
from fastapi import FastAPI, Request
import logging
app = FastAPI()
@app.middleware("http")
async def log_requests(request: Request, call_next):
logger = logging.getLogger("fastapi")
logger.info(f"Request: {request.method} {request.url}")
response = await call_next(request)
if response.status_code == 404:
logger.warning(f"404 Not Found: {request.method} {request.url}")
return response
3. Use FastAPI's Automatic Documentation
Check your routes at http://localhost:8000/docs
to see all available endpoints.
Advanced 404 Scenarios
1. Trailing Slash Issues
# These are different routes in FastAPI
@app.get("/users") # No trailing slash
@app.get("/users/") # With trailing slash
# Solution: Handle both
@app.get("/users")
@app.get("/users/")
async def get_users():
return {"users": []}
2. Route Order Matters
# WRONG - specific route after generic route
@app.get("/users/{user_id}")
async def get_user(user_id: str):
return {"user_id": user_id}
@app.get("/users/me") # This will never be reached
async def get_current_user():
return {"user": "current"}
# CORRECT - specific routes first
@app.get("/users/me")
async def get_current_user():
return {"user": "current"}
@app.get("/users/{user_id}")
async def get_user(user_id: str):
return {"user_id": user_id}
3. Subdirectory Deployment
# When deploying under subdirectory like /api/v1
app = FastAPI(root_path="/api/v1")
# Or use proxy headers
app = FastAPI()
@app.middleware("http")
async def add_root_path(request: Request, call_next):
# Handle X-Forwarded-Prefix header
forwarded_prefix = request.headers.get("X-Forwarded-Prefix")
if forwarded_prefix:
request.scope["root_path"] = forwarded_prefix
return await call_next(request)
Testing for 404 Errors
1. Basic Route Testing
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_existing_route():
response = client.get("/users/123")
assert response.status_code == 200
def test_non_existing_route():
response = client.get("/nonexistent")
assert response.status_code == 404
def test_wrong_method():
response = client.get("/users/") # If only POST is defined
assert response.status_code in [404, 405] # Method not allowed
2. Automated Route Testing
import pytest
from fastapi.testclient import TestClient
@pytest.fixture
def client():
return TestClient(app)
def test_all_documented_routes(client):
"""Test that all documented routes are accessible"""
for route in app.routes:
if hasattr(route, 'methods') and hasattr(route, 'path'):
for method in route.methods:
if method in ['GET', 'POST', 'PUT', 'DELETE']:
# Test with dummy data for path parameters
path = route.path.replace('{', '123').replace('}', '')
response = getattr(client, method.lower())(path)
assert response.status_code != 404, f"Route {method} {route.path} returns 404"
Custom 404 Handler
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse
app = FastAPI()
@app.exception_handler(404)
async def not_found_handler(request: Request, exc: HTTPException):
return JSONResponse(
status_code=404,
content={
"error": "Not Found",
"message": f"The requested URL {request.url.path} was not found",
"suggestions": [
"Check the URL spelling",
"Verify the HTTP method",
"Check available routes at /docs"
]
}
)
Prevention Best Practices
1. Consistent Route Naming
# Use consistent patterns
@app.get("/users/") # List all users
@app.post("/users/") # Create user
@app.get("/users/{user_id}") # Get specific user
@app.put("/users/{user_id}") # Update user
@app.delete("/users/{user_id}") # Delete user
2. Route Organization
# Group related routes in routers
from fastapi import APIRouter
users_router = APIRouter(prefix="/users", tags=["users"])
posts_router = APIRouter(prefix="/posts", tags=["posts"])
@users_router.get("/")
async def list_users():
return {"users": []}
@users_router.get("/{user_id}")
async def get_user(user_id: int):
return {"user_id": user_id}
# Include in main app
app.include_router(users_router)
app.include_router(posts_router)
3. Input Validation
from pydantic import BaseModel, validator
from typing import Optional
class UserQuery(BaseModel):
user_id: int
include_posts: Optional[bool] = False
@validator('user_id')
def user_id_must_be_positive(cls, v):
if v <= 0:
raise ValueError('user_id must be positive')
return v
@app.get("/users/{user_id}")
async def get_user(user_id: int, query: UserQuery = Depends()):
# user_id is already validated by Pydantic
return {"user_id": user_id}
Troubleshooting Checklist
When encountering 404 errors, check:
- Route Definition: Is the route properly defined with correct decorators?
- HTTP Method: Does the request method match the route method?
- Path Parameters: Are path parameters correctly formatted and typed?
- Router Inclusion: Are all routers included in the main app?
- Route Order: Are specific routes defined before generic ones?
- Case Sensitivity: URLs are case-sensitive in FastAPI
- Trailing Slashes: Handle both
/users
and/users/
if needed - Documentation: Check
/docs
to see all available routes