Globally importable request object
Closed this issue · 12 comments
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.
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
@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.
Would very much welcome a third party packaging along the lines of @MarcDufresne's middleware.
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
Fantastic, want to open a PR adding it to the “Third Party Packages” docs?
Fantastic, want to open a PR adding it to the “Third Party Packages” docs?
There goes my first PR in OSS!
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)
...
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).
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")
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!
@gareth-leake Is there a way how to get access to the response HTTP status code?
@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.