How to add a custom decorator to a FastAPI route?

Question:

I want to add an auth_required decorator to my endpoints.
(Please consider that this question is about decorators, not middleware)

So a simple decorator looks like this:

def auth_required(func):
    def wrapper(*args, **kwargs):
        if user_ctx.get() is None:
            raise HTTPException(...)
        return func(*args, **kwargs)
    return wrapper

So there are 2 usages:

@auth_required
@router.post(...)

or

@router.post(...)
@auth_required

The first way doesn’t work because router.post creates a router that saved into self.routes of APIRouter object. The second way doesn’t work because it fails to verify pydantic object. For any request model, it says missing args, missing kwargs.

So my question is – how can I add any decorators to FastAPI endpoints? Should I get into router.routes and modify the existing endpoint? Or use some functools.wraps like functions?

Asked By: sashaaero

||

Answers:

How can I add any decorators to FastAPI endpoints?

As you said, you need to use @functools.wraps(...)–(PyDoc) decorator as,

from functools import wraps

from fastapi import FastAPI
from pydantic import BaseModel


class SampleModel(BaseModel):
    name: str
    age: int


app = FastAPI()


def auth_required(func):
    @wraps(func)
    async def wrapper(*args, **kwargs):
        return await func(*args, **kwargs)

    return wrapper


@app.post("/")
@auth_required # Custom decorator
async def root(payload: SampleModel):
    return {"message": "Hello World", "payload": payload}

The main caveat of this method is that you can’t access the request object in the wrapper and I assume it is your primary intention.

If you need to access the request, you must add the argument to the router function as,

from fastapi import Request


@app.post("/")
@auth_required  # Custom decorator
async def root(request: Request, payload: SampleModel):
    return {"message": "Hello World", "payload": payload}

I am not sure what’s wrong with the FastAPI middleware, after all, the @app.middleware(...) is also a decorator.

Answered By: JPG

Here is how you can use a decorator that adds extra parameters to the route handler:

from fastapi import FastAPI, Request
from pydantic import BaseModel


class SampleModel(BaseModel):
    name: str
    age: int


app = FastAPI()

def do_something_with_request_object(request: Request):
    print(request)

def auth_required(handler):
    async def wrapper(request: Request, *args, **kwargs):
        do_something_with_request_object(request)
        return await handler(*args, **kwargs)

    # Fix signature of wrapper
    import inspect
    wrapper.__signature__ = inspect.Signature(
        parameters = [
            # Use all parameters from handler
            *inspect.signature(handler).parameters.values(),

            # Skip *args and **kwargs from wrapper parameters:
            *filter(
                lambda p: p.kind not in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD),
                inspect.signature(wrapper).parameters.values()
            )
        ],
        return_annotation = inspect.signature(handler).return_annotation,
    )

    return wrapper


@app.post("/")
@auth_required # Custom decorator
async def root(payload: SampleModel):
    return {"message": f"Hello {payload.name}, {payload.age} years old!"}
Answered By: md2perpe

Simply use the dependencies inside of the path operation decorator:

from fastapi import Depends, FastAPI, Header, HTTPException

app = FastAPI()


async def verify_token(x_token: str = Header()):
    if x_token != "fake-super-secret-token":
        raise HTTPException(status_code=400, detail="X-Token header invalid")


async def verify_key(x_key: str = Header()):
    if x_key != "fake-super-secret-key":
        raise HTTPException(status_code=400, detail="X-Key header invalid")
    return x_key


@app.get("/items/", dependencies=[Depends(verify_token), Depends(verify_key)])
async def read_items():
    return [{"item": "Foo"}, {"item": "Bar"}]
Answered By: shapale

In addtion to JPG’s answer, you can access the Request object inside your decorator with kwargs.get('request'). A full decorator would look something like:

def render_template(template):
    """decorator to render a template with a context"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):

            # access request object
            request = kwargs.get('request')

            context = func(*args, **kwargs)
            if context is None:
                context = {}
            return templates.TemplateResponse(template, {**context, 'request': request})
        return wrapper
    return decorator

The decorated function will need to take the Request as a parameter, however.

Answered By: Cameron Sima