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

Document interaction of BaseHTTPMiddleware and contextvars #1525

Merged

Conversation

adriangb
Copy link
Member

@adriangb adriangb commented Feb 18, 2022

Using BaseHTTPMiddleware changes the behavior of the application because creating a TaskGroup erases changes to the context made within tasks spawned from the TaskGroup.

This PR is merely documenting the behavior, not necessarily proposing a fix or change.

As I understand it, the implications of this behavior were direct cause for a recent FastAPI minor version release with breaking changes (https://github.com/tiangolo/fastapi/releases/tag/0.74.0). I think it is important to at least document this behavior so that it is well understood going forward and so that we can know if it changes.

That said, one possible fix, if we decide we want to "fix" this, would be something along the lines of #1258

@adriangb adriangb changed the title Document interaction of BaseHTTPMiddleware and context variables Document interaction of BaseHTTPMiddleware and contextvars Feb 19, 2022
@peku33
Copy link

peku33 commented Mar 2, 2022

Got into the same issue.
When settings contextvars in app with multiple BaseHTTPMiddlewares they are not visible to each other,

@adriangb
Copy link
Member Author

adriangb commented Mar 2, 2022

Would you be able to share your concrete use case @peku33 ? As is right now this is a very "theoretical" issue, but it would help to get some actual use cases 😃

@peku33
Copy link

peku33 commented Mar 2, 2022

I am using one middleware to populate request context (contextvar) with some details and another middleware to provide logging based on that data.

Data added to contextvar is only visible to "inner" middlewares, like if the context was cloned and frozen before running it.

example:

def debug_middleware(index: int) -> Middleware:
    async def _dispatch(
        request: Request,
        call_next: RequestResponseEndpoint,
    ) -> Response:
        if index == 2:
            # stub populator
            user = User("a", "b", True)  # FIXME
            request_context = RequestContext(
                user=user,
            )
            REQUEST_CONTEXT_VAR.set(request_context)
            print("populated with", request_context)

        print("debug pre", index, REQUEST_CONTEXT_VAR.get())
        r = await call_next(request)
        print("debug post", index, REQUEST_CONTEXT_VAR.get())

        return r

    return Middleware(BaseHTTPMiddleware, dispatch=_dispatch)


app = Starlette(
    middleware=[
        request_context.debug_middleware(1),
        request_context.debug_middleware(2),
        request_context.debug_middleware(3),
    ],
)

the result is:

debug pre 1 None
populated with RequestContext(user=...)
debug pre 2 RequestContext(user=...)
debug pre 3 RequestContext(user=...)
debug post 3 RequestContext(user=...)
debug post 2 RequestContext(user=...)
debug post 1 None

while I would rather expect it to be:

debug pre 1 None
populated with RequestContext(user=...)
debug pre 2 RequestContext(user=...)
debug pre 3 RequestContext(user=...)
debug post 3 RequestContext(user=...)
debug post 2 RequestContext(user=...)
debug post 1 RequestContext(user=...) << different here

What is even funnier - it looks like the last middleware shares the context with the worker (with unicorn), because if the contextvar is set in the first (outermost) middleware - it is visible from inside of the worker.

@Kludex Kludex added this to the Version 0.20.0 milestone Apr 21, 2022
tests/middleware/test_base.py Outdated Show resolved Hide resolved
Comment on lines 210 to 211
# on sync endpoints which suffers from the same problem of erasing changes
# to the context
Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What "same problem"? You mean the same as caused by the TaskGroup?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Although I think this has been fixed in the meantime. Let me re-confirm.

Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep I think that should fix the issue. It doesn't change the rest of this PR, but we can just remove my comment.

Copy link
Member Author

@adriangb adriangb Apr 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah okay, no the issue is different: to_thread.run_sync() now propagates the context _ forwards_ but not backwards:

from contextvars import ContextVar
import anyio

ctx = ContextVar[str]("ctx")

async def async_func() -> None:
    assert ctx.set("bar")

def sync_func() -> None:
    assert ctx.set("foo")

async def main() -> None:
    await async_func()
    assert ctx.get() == "bar"
    await anyio.to_thread.run_sync(sync_func)
    assert ctx.get() == "foo"  # fails

anyio.run(main)

So there is still a subtle difference between a async and async endpoint. Side note: I think at some point before a 1.0 support for sync endpoints should be dropped, I think @tomchristie suggested this as well in the past via Gitter (Tom if you recall any of this conversation, or don't think it happened, please correct me if I am wrong).

I'm also happy to add a similar test in another PR documenting that edge case.

@Kludex Kludex mentioned this pull request Apr 22, 2022
2 tasks
Copy link
Sponsor Member

@Kludex Kludex left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's go with this. It just adds a confirmation of the behavior we are aware.

@Kludex
Copy link
Sponsor Member

Kludex commented Apr 24, 2022

Thanks @adriangb :)

@Kludex Kludex merged commit 5a9b414 into encode:master Apr 24, 2022
@adriangb adriangb deleted the documente-basehttpmiddleware-context-behavior branch April 24, 2022 04:58
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

Successfully merging this pull request may close these issues.

None yet

3 participants