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
✨ Update internal AsyncExitStack
to fix context for dependencies with yield
#4575
Conversation
🚀 Deployed on https://620e41afd17a68060457cc5e--fastapi.netlify.app |
📝 Docs preview for commit 017c235 at: https://620a9717c1c59f5273ea6a49--fastapi.netlify.app |
📝 Docs preview for commit cea488d at: https://620a981053e73a57a147c75e--fastapi.netlify.app |
after being handled by dependencies
…res and AsyncExitStack
Codecov Report
@@ Coverage Diff @@
## master #4575 +/- ##
==========================================
Coverage 100.00% 100.00%
==========================================
Files 522 525 +3
Lines 13136 13281 +145
==========================================
+ Hits 13136 13281 +145
Continue to review full report at Codecov.
|
📝 Docs preview for commit 4531f6a at: https://620e3a9379f92813c67853d1--fastapi.netlify.app |
📝 Docs preview for commit a69d916 at: https://620e41e363bc9107a4202168--fastapi.netlify.app |
self.context_name = context_name | ||
|
||
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: | ||
if AsyncExitStack: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems to me that AsyncExitStack
is always true, it’s imported and concurrency provides it from either of two locations, but it’s always provided.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah... 🤔
Is it (or will it be) possible to access the contextvars in the view that depends on said dependency? For example: def my_dep():
# Define some context var here
yield
@router.get('/', dependencies=[Depends(my_dep)])
def view():
# Use context var set in my_dep In my case It's done for carrying log context fields (using structlog) |
This also allows catching exceptions like
HTTPException
inside of dependencies withyield
. ✨Dependencies with
yield
are converted toasynccontextmanagers
internally and then are run withAsyncExitStack
.But before this change, the
AsyncExitStack
was created on the application's__call__()
, right before custom user middlewares.But custom user middlewares are created using a new AnyIO task group, and that creates a new contextvars context.
The call to enter the
asynccontextmanager
is done when calling the internal app, inside of the custom middlewares. But the call to exit theAsyncExitStack
is done outside, so, the exit code of dependencies withyield
(the code afteryield
) was run in a different context than the code aboveyield
.Because of this, it was not possible to set a context variable in a dependency with
yield
beforeyield
and then reset it or use its value afterwards:...that example would throw an error at
legacy_request_state_context_var.reset(contextvar_token)
.This PR sets up the
AsyncExitStack
in a new middleware that is run inside of the custom user middlewares and is run directly, without a new task group. This way the same contextvar context used to enter the asynccontextmanager created for a dependency withyield
is also used to run the exit code (afteryield
) of the dependency.Note:
contextvars
are very powerful, but also complex and the way they work can be a bit magical and hard to debug. If you can, try not to use them, avoid them, pass parameters to functions directly, and savecontextvars
for only the advanced use cases that really need them.An example of an advanced use case that would need
contextvars
is if you are migrating from Flask to FastAPI (or anything else) and you are using Flask'sg
semi-global variable to store data. And then you use thatg
in some function hidden deep down, and passing parameters directly through all the functions up to that point would be a refactor so big and cumbersome that would prevent the migration entirely. In that case, you can replaceg
with a context var, independent of Flask. This would apply to a migration to FastAPI or to anything else. In the case of FastAPI, you could set any of thatg
data in a context variable instead, in a dependency withyield
, and then use the context variable in that function deep down instead of Flask'sg
object. You can also do this with Flask, and that would make your internal function independent of Flask.