pythonfastapistarlette

FastAPI/Starlette's Request object is empty when used in a normal function


When I use request inside a FastAPI route, the request is filled with the proper headers, such as:

request.headers.get('X-Forwarded-For')

However, when I use the request object inside a function, which isn't a route, all the values are empty. I guess the whole object is not evaluated?

Working:

from fastapi import Request


@app.get("/logip")
async def log_request_ip(request: Request):
     print(request.headers.get("X-Forwarded-For))

Not working:

from fastapi import Request


@app.get("/logip")
async def log_request_ip():
     log_ip()
   

def log_ip(request: Request = Request):
      print(request.headers.get("X-Forwarded-For))

Is there a reason for this? Does FastAPI only evaluate the request object when it is injected into the route, but not in a function? Is there any way to make this possible? I want to avoid injecting the request into every route, and just keep this in the function that actually uses this value.

Is there maybe a way to tell FastAPI that this function is only used within a request context?


Solution

  • It won't be possible.

    Your log_ip function is not called by FastAPI, so it can't inject the request into it.

    I think you have two options to make it work:

    1. Pass the request yourself: log_ip(request=request)
      This will be enough if you only need to call log_ip in this specific endpoint.

    2. The better solution would be to create a middleware that will automatically call your function on every request, this way you won't have to manually call log_ip at all and it will be handled by FastAPI

    # Register the middleware on app instance
    @app.middleware('http')
    async def log_ip_middleware(request: Request, call_next: Callable[..., Awaitable[Any]]) -> Response:
        # Call log_ip
        log_ip(request=request)
    
        # Return result of call_next (the response)
        return await call_next(request)
    

    EDIT.

    With your comment in mind, you can change the middleware to set request to the contextvar that will be accessible from anywhere.

    So the middleware becomes:

    from contextvars import ContextVar
    
    
    current_request: ContextVar[Request] = ContextVar('current_request')
    
    
    # Register the middleware on app instance
    @app.middleware('http')
    async def current_request_middleware(request: Request, call_next: Callable[..., Awaitable[Any]]) -> Response:
        current_request.set(request)
    
        # Return result of call_next (the response)
        return await call_next(request)
    

    And then in your log_ip function:

    from .<module> import current_request
    
    def log_ip():
        # now this function can access the current request
        request = current_request.get()
    
        print(request.headers.get("X-Forwarded-For"))
    

    This way your log_ip function can always access the request no matter the place it is being called from. Keep in mind that it might be a good idea to make contextvar Request | None type, setting default to None, and then check if request was set inside the log_ip in case you will call it outside the framework scope (like inside a background worker).

    You can read more on contextvars here.