44

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?

Gino Mempin
  • 25,369
  • 29
  • 96
  • 135
sashaaero
  • 2,618
  • 5
  • 23
  • 41
  • 8
    Is there a reason you need it to be a decorator? Coming from Flask to FastAPI, I sometimes think I need a decorator, but a [custom APIRoute class](https://fastapi.tiangolo.com/advanced/custom-request-and-route/) for endpoints that need auth or a Depends(User) injection can also solve the problem. – Gino Mempin Oct 23 '20 at 09:45
  • 1
    I want to add that decorator to some endpoints, not every. So custom APIRoute class (Im actually using it) doesnt help. And I have an issue with middleware - it works in another thread, so I can't set up global context variable from another thread. I saw some solutions to it, but now i really want to know is decorators possible. – sashaaero Oct 23 '20 at 09:51
  • 5
    The recommended style with FastAPI seems to be to use Dependencies. You add something like `user: User = Depends(auth_function)` to the path or function. That gets called before your endpoint function, similar to how a decorator wraps it. It should also have access to the req-resp context. – Gino Mempin Oct 23 '20 at 09:57
  • 1
    I know how to use depends. It has access to context, but since it is working in another thread, im getting empty context in main thread. – sashaaero Oct 23 '20 at 11:01
  • "Depends" also can't do any "around" actions (it can't do stuff after the route method body has completed. – Shannon Jul 11 '23 at 22:51

4 Answers4

80

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.

JPG
  • 82,442
  • 19
  • 127
  • 206
  • 1
    can you please elaborate on the `@app.middleware(...)` you mean that can work as decorators also? any example or tutorial of this? – uberrebu Aug 21 '21 at 17:57
  • This works for post requests but not for get requests. Any idea why? – Ankit Jain Apr 13 '22 at 16:51
  • `@auth_required` is independent of the request method. – JPG Apr 13 '22 at 17:17
  • 3
    @AnkitJain Please make sure to declare the endpoint functions with `async def`. Above `@auth_required` decorator will only work with functions declared with `async def`. If you don't use `async`/`await` in endpoint functions, just drop it from `def wrapper(*args, **kwargs)` definition. – Fahad Munir Apr 14 '22 at 12:23
  • @FahadMunir Thanks a lot! This worked. I have added async in endpoint functions. However, this was not an issue with post requests. – Ankit Jain Apr 14 '22 at 15:55
  • @FahadMunir Hi, I want to ask question, why I got error when I remove `async` of `wrapper`? But after adding `async` everything will work. How it work? – chilin May 01 '23 at 14:02
  • "what's wrong with the FastAPI middleware" Two things about Middlewares: they apply to all routes so you end up having to write a bunch of awkward centralized conditionals. Also, they don't have access to the deserialized object(s) passed to the controller method. All they have is the raw request. – Shannon Jul 11 '23 at 23:01
7

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!"}
md2perpe
  • 3,372
  • 2
  • 18
  • 22
  • Tried, but got `TypeError: wrapper() missing 1 required positional argument: 'request'` – JPG Jan 02 '21 at 17:58
  • @JPG. I have updated the code after a lot of investigation and testing. However, I have tested with my own code; I haven't tested the code above. – md2perpe Jan 03 '21 at 00:23
  • https://gist.github.com/md2perpe/ee146e547a0bd910ea9683a2eea47c59 – md2perpe Jan 03 '21 at 13:07
  • same error `TypeError: wrapper() missing 1 required positional argument: 'request'` – gocreating Jan 16 '21 at 19:59
  • @gocreating. Did you try my gist? – md2perpe Jan 17 '21 at 11:42
  • @md2perpe Sure, but my handler takes both positional and keyword args: `def create_user(user: schemas.UserCreate, session: Session = Depends(get_session)):` so I get `ValueError: non-default argument follows default argument` – gocreating Jan 17 '21 at 12:39
  • 1
    I have answered myself on fastapi's issue: https://github.com/tiangolo/fastapi/issues/2662 – gocreating Jan 17 '21 at 15:26
7

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"}]
shapale
  • 123
  • 2
  • 9
-1

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.

Cameron Sima
  • 5,086
  • 6
  • 28
  • 47