How to return a custom 404 Not Found page using FastAPI?

Question:

I am making a rick roll site for Discord and I would like to redirect to the rick roll page on 404 response status codes.

I’ve tried the following, but didn’t work:

 @app.exception_handler(fastapi.HTTPException)
 async def http_exception_handler(request, exc):
     ...
Asked By: AshKetchumPL

||

Answers:

Update

A more elegant solution would be to use a custom exception handler, passing the status code of the exception you would like to handle, as shown below:

from fastapi.responses import RedirectResponse
from fastapi.exceptions import HTTPException

@app.exception_handler(404)
async def not_found_exception_handler(request: Request, exc: HTTPException):
    return RedirectResponse('https://fastapi.tiangolo.com')

or, use the exception_handlers parameter of the FastAPI class like this:

async def not_found_error(request: Request, exc: HTTPException):
    return RedirectResponse('https://fastapi.tiangolo.com')

exception_handlers = {404: not_found_error}
app = FastAPI(exception_handlers=exception_handlers)

Note: In the examples above, a RedirectResponse is returned, as OP asked for redirecting the user. However, you could instead return some custom Response, HTMLResponse or Jinja2 TemplateResponse, as demosntrated in the example below.

Working Example

app.py

from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
from fastapi.exceptions import HTTPException


async def not_found_error(request: Request, exc: HTTPException):
    return templates.TemplateResponse('404.html', {'request': request}, status_code=404)


async def internal_error(request: Request, exc: HTTPException):
    return templates.TemplateResponse('500.html', {'request': request}, status_code=500)

    
templates = Jinja2Templates(directory='templates')

exception_handlers = {
    404: not_found_error,
    500: internal_error
}

app = FastAPI(exception_handlers=exception_handlers)

templates/404.html

<!DOCTYPE html>
<html>
   <title>Not Found</title>
   <body>
      <h1>Not Found</h1>
      <p>The requested resource was not found on this server.</p>
   </body>
</html>

templates/500.html

<!DOCTYPE html>
<html>
   <title>Internal Server Error</title>
   <body>
      <h1>Internal Server Error</h1>
      <p>The server encountered an internal error or 
         misconfiguration and was unable to complete your request.
      </p>
   </body>
</html>

Original answer

You would need to create a middleware and check for the status_code of the response. If it is 404, then return a RedirectResponse. Example:

from fastapi import Request
from fastapi.responses import RedirectResponse

@app.middleware("http")
async def redirect_on_not_found(request: Request, call_next):
    response = await call_next(request)
    if response.status_code == 404:
        return RedirectResponse("https://fastapi.tiangolo.com")
    else:
        return response
Answered By: Chris
from fastapi import FastAPI
from fastapi.templating import Jinja2Templates
from starlette.exceptions import HTTPException

# --- Constants --- #

templates = Jinja2Templates(directory="./templates")

# --- Error handler --- #

def lost_page(request, exception):
    headers = {"Content-Type": "text/html"}

    if isinstance(exception, HTTPException):
        status_code = exception.status_code
        detail = exception.detail
    elif isinstance(exception, Exception):
        status_code = 500
        detail = "Server Error"
        headers["X-Error-Message"] = exception.__class__.__name__
        headers["X-Error-Line"] = str(exception.__traceback__.tb_lineno)
    else:
        status_code = 500
        detail = f"Server ErrornnDetails: {exception}"

    return templates.TemplateResponse(
        "404.html",
        {"request": request, "status_code": status_code, "detail": detail},
        status_code=status_code,
        headers=headers,
    )


exception_handlers = {num: lost_page for num in range(400, 599)}
app = FastAPI(exception_handlers=exception_handlers)

This is a snippet I’ve used across a few projects, it’s essentially a catch-all for all 400 and 500 status codes.

from fastapi import FastAPI
from fastapi.templating import Jinja2Templates
from starlette.exceptions import HTTPException

# --- Constants --- #

templates = Jinja2Templates(directory="./templates")

This block imports relevant libraries and initializes Jinja2Templates, which allows us to render HTML using FastAPI. Docs.

Let’s dissect

def lost_page(request, exception):
    headers = {"Content-Type": "text/html"}

    if isinstance(exception, HTTPException):
        status_code = exception.status_code
        detail = exception.detail
    elif isinstance(exception, Exception):
        status_code = 500
        detail = "Server Error"
        headers["X-Error-Message"] = exception.__class__.__name__
        headers["X-Error-Line"] = str(exception.__traceback__.tb_lineno)
    else:
        status_code = 500
        detail = f"Server ErrornnDetails: {exception}"

    return templates.TemplateResponse(
        "404.html",
        {"request": request, "status_code": status_code, "detail": detail},
        status_code=status_code,
        headers=headers,
    )

FastAPI’s exception handler provides two parameters, the request object that caused the exception, and the exception that was raised.

def lost_page(request, exception):

^^ Our function takes these two parameters.

headers = {"Content-Type": "text/html"}

These are the headers we’re going to send back along with the request.

    if isinstance(exception, HTTPException):
        status_code = exception.status_code
        detail = exception.detail

    elif isinstance(exception, Exception):
        status_code = 500
        detail = "Server Error"
        headers["X-Error-Name"] = exception.__class__.__name__

    else:
        status_code = 500
        detail = f"Server ErrornnDetails: {exception}"

If the exception parameter is a HTTPException (raised by Starlette/FastAPI), then we’re going to set the status_code and detail appropriately. An example of an HTTPException is a 404 error, if you try accessing an endpoint that doesn’t exist, a HTTPException is raised and handled automatically by FastAPI.

Then, we check if it’s an instance of Exception, which is one of Python’s in-built exception classes. This covers exceptions such as ZeroDivisionError, FileNotFoundError, etc. This usually means that it’s an issue with our code, such as trying to open a file that doesn’t exist, dividing by zero, using an unknown attribute, or something else that raised an exception which wasn’t handled inside of the endpoint function.

The else block shouldn’t trigger in any case, and can be removed, it’s just something I keep to appease my conscience.

After the status_code, detail and headers are set,

    return templates.TemplateResponse(
        "404.html",
        {"request": request, "status_code": status_code, "detail": detail},
        status_code=status_code,
        headers=headers,
    )

We return our 404 template, the TemplateResponse function takes in a few parameters, "404.html" being the file we want to return, {"request": request, "status_code": status_code, "detail": detail} being the request object and the values for embeds we want to fill (embeds are a way to pass information between jinja2 and Python). Then we define the status code of the response, along with its headers.

This is a 404 html template I use alongside the error handler.

exception_handlers = {num: lost_page for num in range(400, 599)}
app = FastAPI(exception_handlers=exception_handlers)

Exception handlers uses dict comprehension to create a dictionary of status codes, and the functions that should be called,

exception_handlers = {400: lost_page, 401: lost_page, 402: lost_page, ...}

Is how it’ll look after the comprehension, until 599.

FastAPI Allows us to pass this dict as a parameter of the FastAPI class,

app = FastAPI(exception_handlers=exception_handlers)

This tells FastAPI to run the following functions when the endpoint function returns a particular status code.

To conclude, the snippet above and this error template should help you handle all FastAPI errors in a nice, user-friendly and clean way.

Answered By: TheOnlyWayUp
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.