-
-
Notifications
You must be signed in to change notification settings - Fork 948
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
Prevent anyio.ExceptionGroup in error views under a BaseHTTPMiddleware #1262
Conversation
ed4246f
to
525215a
Compare
@@ -52,8 +52,9 @@ def test_custom_middleware(test_client_factory): | |||
response = client.get("/") | |||
assert response.headers["Custom-Header"] == "Example" | |||
|
|||
with pytest.raises(Exception): | |||
with pytest.raises(Exception) as ctx: |
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.
When running on trio
, previously we would be getting an unexpected "Portal is not running" error here, and misinterpreting it as the Exception()
in the /exc
view. So I made the latter more precise and made sure we're explicitly checking for it.
525215a
to
58defbc
Compare
I'm not sure I'm in favor of relying on event loop magic to correctly propagate exceptions. Here's an idea that I think is a bit easier to follow, thoughts?: diff --git a/starlette/middleware/base.py b/starlette/middleware/base.py
index bf337b8..30ae1ed 100644
--- a/starlette/middleware/base.py
+++ b/starlette/middleware/base.py
@@ -24,21 +24,25 @@ class BaseHTTPMiddleware:
async def call_next(request: Request) -> Response:
send_stream, recv_stream = anyio.create_memory_object_stream()
+ app_exc: typing.Optional[Exception] = None
async def coro() -> None:
async with send_stream:
- await self.app(scope, request.receive, send_stream.send)
+ try:
+ await self.app(scope, request.receive, send_stream.send)
+ except Exception as exc:
+ nonlocal app_exc
+ app_exc = exc
task_group.start_soon(coro)
try:
message = await recv_stream.receive()
except anyio.EndOfStream:
- # HACK: give anyio a chance to surface any app exception first,
- # in order to avoid an `anyio.ExceptionGroup`.
- # See #1255.
- await anyio.lowlevel.checkpoint()
- raise RuntimeError("No response returned.")
+ if app_exc is not None:
+ raise app_exc
+ else:
+ raise RuntimeError("No response returned.")
assert message["type"] == "http.response.start" |
Hey team. Anything I can do to help push this along? I'm running into this issue as well, which is impacting I've tested out the proposal from @uSpike and it works well. Also seems like a pragmatic approach to resolve the issue. When can we get this merged and released? |
Looks like this is ready then — happy to have anyone review and release this. |
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.
Looks good to me. Thanks, @florimondmanca!
Hello, we will be using this fix at our company too, looks good. |
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.
This solution looks much simpler 👍
I was waiting to see if @uSpike wanted to say something, but the code looks pretty straightforward. 😄
Thanks! |
Closes #1255
Motivation
#1157 switched to using
anyio
for async concurrency operations.But now when a view raises an exception, say
MyExc
, and aBaseHTTPMiddleware
is installed, calling the view would make Starlette raise ananyio.ExceptionGroup
, rather thanMyExc
directly.Previously, under
asyncio
, we were callingtask.result()
before raisingRuntimeError("No response sent.")
, so onlyMyExc
was raised, as expected, see here:starlette/starlette/middleware/base.py
Lines 45 to 46 in f5a08d0
This PR switches back to that behavior by adding a
checkpoint
(equivalent toasyncio.sleep(0)
— i.e. "allow switching tasks and checking for cancellation) before raising the "No response sent" exception.cc @uSpike