Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Globally importable request object #420

Closed
MarcDufresne opened this issue Mar 6, 2019 · 12 comments
Closed

Globally importable request object #420

MarcDufresne opened this issue Mar 6, 2019 · 12 comments

Comments

@MarcDufresne
Copy link

Hello, I had an issue with Starlette where I had to access request information outside of a view function. I successfully fixed my issue and would like to share the discussion I had with @tomchristie and how I solved the issue.


Here's a snippet of the question I asked and the answer I got:

From me:

Hi!

I am currently working with Starlette (porting over a Flask project to FastAPI) and I'm wondering if there's some way to get the current request from anywhere in the code, like you can do with Flask using from flask import request.
I need to access data from the request but from a non-view part of the code (a logging filter, so I can't even pass it along).

I've read the whole documentation and looked at all the GitHub issues but couldn't find anything that fits my needs, this #379 is the closest I found, which seems to be part of what I want. However I found no way of importing the request object so it's useless to me.

Reply:
No we don't currently do that. Using contextvars would be the way to implement it. You could do this in a middleware class without having to alter Starlette directly. Happy to chat through how you'd achieve that, but let's open a ticket if there's more to talk over here. :)


Solution

RequestContextMiddleware:

from contextvars import ContextVar
from uuid import uuid4

from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
from starlette.requests import Request


REQUEST_ID_CTX_KEY = "request_id"

_request_id_ctx_var: ContextVar[str] = ContextVar(REQUEST_ID_CTX_KEY, default=None)


def get_request_id() -> str:
    return _request_id_ctx_var.get()


class RequestContextMiddleware(BaseHTTPMiddleware):

    async def dispatch(
        self, request: Request, call_next: RequestResponseEndpoint
    ):
        request_id = _request_id_ctx_var.set(str(uuid4()))

        response = await call_next(request)

        _request_id_ctx_var.reset(request_id)

        return response

In your app initialisation:

app.add_middleware(RequestContextMiddleware)

Usage:
Now you will be able to import the get_request_id function anywhere and get the current request ID, or None if you are not in the context of a request.

You can also add many more context vars by creating new ContextVar objects, and using them the same way as _request_id_ctx_var in my example.


For the future

Maybe Starlette could expose an object that abstracts the context vars in some way. But my idea on this isn't clear, I don't know how generic this could be.

@tiangolo
Copy link
Sponsor Member

Hi @MarcDufresne, FastAPI creator here 😄

I see you solved your problem, but in case it's useful here are some additional notes you might want to have in mind:

ContextVars are Python 3.7 only. So, for example, not compatible with TensorFlow for now (although there's PyTorch for Python 3.7). Also ContextVars might be hard to understand, a bit black magic-like (might be difficult for teammates).

Given that you are using FastAPI, there's a chance you can solve it using the Dependency Injection system.

I'm curious about how your logging system works, I understand you can't pass the request as a parameter but it can access the global context var...

I can imagine, for example, a middleware that adds the UUID to the requests' .state, and then a dependency that requests the logger (I imagine you call a logger object at some point, or something similar). Then, in the dependency, you could get the request, read the UUID and do the filtering at that level, before passing one object or another to the actual path operation function/route.

As it's less "magic" and (maybe) more "functional", it might be easier to grasp for teammates, etc.

An example of something similar is the SQL tutorial, it adds a SQLAlchemy session to the request, that is then received with dependency injection in each route/"path operation function".

I don't wanna hijack this issue (in Starlette), but if something like that makes sense (at the FastAPI level) feel free to open an issue there: https://github.com/tiangolo/fastapi/issues/new/choose

@bohea
Copy link

bohea commented Apr 30, 2019

@tiangolo logger may be called outside of view function, if using Dependency Injection, logger object has to be passed to all functions again and again.

@tomchristie
Copy link
Member

Would very much welcome a third party packaging along the lines of @MarcDufresne's middleware.

@tomwojcik
Copy link
Contributor

I needed something like this so I made a package.
https://github.com/tomwojcik/starlette-context

One of the examples show how to use logging with context so request id and context id are automatically injected into json logs. Hope it helps : ) Feedback welcome.

CC @tomchristie

@tomchristie
Copy link
Member

Fantastic, want to open a PR adding it to the “Third Party Packages” docs?

@tomwojcik
Copy link
Contributor

Fantastic, want to open a PR adding it to the “Third Party Packages” docs?

There goes my first PR in OSS!

#770

@cperezabo
Copy link

Hi folks, would it work to use the starlette-context data in the scopefunc of scoped_session

from starlette_context import context
...
Session = scoped_session(sessionmaker(bind=some_engine), scopefunc=lambda: context.data)
...

@tomwojcik ?

@tomwojcik
Copy link
Contributor

I honestly have no idea but I don't see why not. It's just a matter of proper execution order.

If you manage to get it working please let me know. If so, I think it should be part of starlette-context docs (contribution also welcome).

@cperezabo
Copy link

Yes, I made it yesterday. It works like a charm.

engine = create_engine("postgresql://localhost")

# 1) Create a scoped session bound to request ID
Session = scoped_session(
    sessionmaker(bind=engine),
    scopefunc=lambda: context.data.get('X-Request-ID'),
)


# 2) Register an event listener for automatically add instances of mapped objects to session when they are created
@event.listens_for(mapper, 'init')
def _(target, args, kwargs):
    Session.add(target)


# 3) Commit changes before returning the request's response
class AutoCommitMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):
        response = await call_next(request)
        Session.commit()
        Session.remove()
        return response


app = Starlette(middleware=[
    Middleware(
        RawContextMiddleware,
        plugins=(
            plugins.RequestIdPlugin(),
        )
    ),
    Middleware(AutoCommitMiddleware)
])


# 4) Just use the objects and they will be automatically persisted
@app.route("/")
async def index():
    prod = Product()
    prod.do_domething()
    return JSONResponse({"status": "success"})


uvicorn.run(app, host="0.0.0.0")

@gareth-leake
Copy link

Came across a similar issue as @MarcDufresne. I am using FastAPI (thanks @tiangolo), and needed a way to access information on the request object outside of a view. I initially looked at using starlette-context (thanks @tomwojcik) but found the below solution to work for my needs.

Marc, first off just wanna say thanks so much for this solution! It got me started on the right path. Wanted to post an update here, for anyone having a similar issue in 2022.

Several months after this initial solution, the authors warn against using BaseHTTPMiddleware -- the parent class Marc's middleware inherits from.

Instead, the suggestion is to use a raw ASGI middleware. However, there isn't much documentation for this. I was able to use Starlette's AuthenticationMiddleware as a reference point, and develop what I needed in combination with Marc's wonderful solution of ContextVars.

# middleware.py
from starlette.types import ASGIApp, Receive, Scope, Send

REQUEST_ID_CTX_KEY = "request_id"

_request_id_ctx_var: ContextVar[str] = ContextVar(REQUEST_ID_CTX_KEY, default=None)

def get_request_id() -> str:
    return _request_id_ctx_var.get()

class CustomRequestMiddleware:
    def __init__(
        self,
        app: ASGIApp,
    ) -> None:
        self.app = app

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        if scope["type"] not in ["http", "websocket"]:
            await self.app(scope, receive, send)
            return

        request_id = _request_id_ctx_var.set(str(uuid4()))

        await self.app(scope, receive, send)

        _request_id_ctx_var.reset(request_id)

And then in the app setup:

# main.py
app.add_middleware(CustomRequestMiddleware)

And finally, the non-view function:

# myfunc.py
import get_request_id

request_id = get_request_id()

Thanks again to everyone in this thread for all the help, and I hope the above is useful!

@jaksky
Copy link

jaksky commented Feb 7, 2023

@gareth-leake Is there a way how to get access to the response HTTP status code?

@gareth-leake
Copy link

@jaksky -- I haven't thought about this in a while, so I'm not sure. I'd say just use a debugger and inspect the function to see if you can grab it. The middleware is just setting a random uuid as the request_id to be tracked later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants