Skip to content

Surface non-Connect handler exceptions to user#219

Open
anuraaga wants to merge 4 commits intoconnectrpc:mainfrom
anuraaga:reraise-exception-asgi
Open

Surface non-Connect handler exceptions to user#219
anuraaga wants to merge 4 commits intoconnectrpc:mainfrom
anuraaga:reraise-exception-asgi

Conversation

@anuraaga
Copy link
Copy Markdown
Collaborator

@anuraaga anuraaga commented Apr 17, 2026

Fixes #217

Ended up folding WSGI in too. Could confirm servers render the exceptions

❯ uv run pyvoy example.eliza_service:app
pyvoy listening on 127.0.0.1:8000
Exception in ASGI application
Traceback (most recent call last):
  File "/Users/anuraag/git/connect-python/.venv/lib/python3.14/site-packages/starlette/applications.py", line 107, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/Users/anuraag/git/connect-python/.venv/lib/python3.14/site-packages/starlette/middleware/errors.py", line 186, in __call__
    raise exc
  File "/Users/anuraag/git/connect-python/.venv/lib/python3.14/site-packages/starlette/middleware/errors.py", line 164, in __call__
    await self.app(scope, receive, _send)
  File "/Users/anuraag/git/connect-python/.venv/lib/python3.14/site-packages/starlette/middleware/exceptions.py", line 63, in __call__
    await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
  File "/Users/anuraag/git/connect-python/.venv/lib/python3.14/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
    raise exc
  File "/Users/anuraag/git/connect-python/.venv/lib/python3.14/site-packages/starlette/_exception_handler.py", line 42, in wrapped_app
    await app(scope, receive, sender)
  File "/Users/anuraag/git/connect-python/.venv/lib/python3.14/site-packages/starlette/routing.py", line 716, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/Users/anuraag/git/connect-python/.venv/lib/python3.14/site-packages/starlette/routing.py", line 736, in app
    await route.handle(scope, receive, send)
  File "/Users/anuraag/git/connect-python/.venv/lib/python3.14/site-packages/starlette/routing.py", line 462, in handle
    await self.app(scope, receive, send)
  File "/Users/anuraag/git/connect-python/src/connectrpc/_server_async.py", line 224, in __call__
    return await self._handle_unary_connect(
  File "/Users/anuraag/git/connect-python/src/connectrpc/_server_async.py", line 264, in _handle_unary_connect
    response_data = await endpoint.function(request, ctx)
  File "/Users/anuraag/git/connect-python/example/example/eliza_service.py", line 36, in say
    raise TypeError("FOO")
TypeError: FOO
^CShutting down pyvoy...

❯ uv run gunicorn  example.eliza_service_sync:app
[2026-04-17 12:19:27 +0900] [23456] [INFO] Starting gunicorn 25.3.0
[2026-04-17 12:19:27 +0900] [23456] [INFO] Listening at: http://127.0.0.1:8000 (23456)
[2026-04-17 12:19:27 +0900] [23456] [INFO] Using worker: sync
[2026-04-17 12:19:27 +0900] [23457] [INFO] Booting worker with pid: 23457
[2026-04-17 12:19:27 +0900] [23456] [INFO] Control socket listening at /Users/anuraag/.gunicorn/gunicorn.ctl
Exception in WSGI application
Traceback (most recent call last):
  File "/Users/anuraag/git/connect-python/src/connectrpc/_server_sync.py", line 249, in __call__
    return self._handle_unary(
           ~~~~~~~~~~~~~~~~~~^
        environ, start_response, http_method, endpoint, ctx, headers
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "/Users/anuraag/git/connect-python/src/connectrpc/_server_sync.py", line 277, in _handle_unary
    response = endpoint.function(request, ctx)
  File "/Users/anuraag/git/connect-python/example/example/eliza_service_sync.py", line 35, in say
    raise TypeError("Foo")
TypeError: Foo

@anuraaga anuraaga requested a review from a team April 17, 2026 02:38
@anuraaga anuraaga force-pushed the reraise-exception-asgi branch from fa73a27 to 73b011d Compare April 17, 2026 02:40
Signed-off-by: Anuraag Agrawal <anuraaga@gmail.com>
Signed-off-by: Anuraag Agrawal <anuraaga@gmail.com>
@anuraaga anuraaga force-pushed the reraise-exception-asgi branch from 73b011d to 485b5d9 Compare April 17, 2026 03:02
Signed-off-by: Anuraag Agrawal <anuraaga@gmail.com>
@anuraaga anuraaga changed the title Reraise non-connect exceptions in ASGI Surface non-Connect handler exceptions to user Apr 17, 2026
"more_trailers": False,
}
)
if error and not isinstance(error, ConnectError):
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

We only raise HTTPExceptions before negotiating a response so don't worry here

return await self._handle_error(e, ctx, send)
await self._handle_error(e, ctx, send)
if not isinstance(e, (ConnectError, HTTPException)):
raise
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This should be raise e , isn't it?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

When inside an except block, raise is actually enough to reraise the active exception. This is exercised in the test_async_unhandled_exception_reraised test

Copy link
Copy Markdown
Member

@stefanvanburen stefanvanburen left a comment

Choose a reason for hiding this comment

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

looks good, just some nits / questions

if isinstance(exc, (ConnectError, HTTPException)):
return
errors: ErrorStream = environ["wsgi.errors"]
errors.write(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: not sure, but should we errors.flush() after this?: https://peps.python.org/pep-3333/#input-and-error-streams. Still understanding WSGI's idioms 😅. I'm assuming we ought to as we're the "portable application"...?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good diligence. I'm leaning towards leaving it out though since I can't imagine a server not writing error messages directly, but also the following For example, to minimize intermingling of data from multiple processes writing to the same error log. doesn't even make sense to me. Preventing intermingling requires passing a chunk of data in a single write call, flush doesn't seem to matter. So I interpret it as a PEP bug, which happens some times

Comment thread test/test_errors.py Outdated
class RaisingHaberdasher(Haberdasher):
def make_similar_hats(
self, request: Size, ctx: RequestContext
) -> AsyncIterator[Hat]:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: should this be NoReturn typed? (also not async def)

Comment thread test/test_errors.py Outdated
class RaisingHaberdasher(Haberdasher):
def make_similar_hats(
self, request: Size, ctx: RequestContext
) -> AsyncIterator[Hat]:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

same nit as above

Comment thread pyproject.toml
"Typing :: Typed",
]
dependencies = ["protobuf>=5.28", "pyqwest>=0.4.1"]
dependencies = ["protobuf>=5.28", "pyqwest>=0.5.1"]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

assuming we need this for https://github.com/curioswitch/pyqwest/releases/tag/v0.5.1:

  • WSGI testing transport registers and exposes wsgi.errors to allow testing handlers that use it

Assuming the transparent wheel change in https://github.com/curioswitch/pyqwest/releases/tag/v0.5.0 doesn't need to be called out on our end?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I don't think so - we never really called out the native aspect clearly before I think, at least in compatibility terms. The perf aspect is probably too much detail on the consumer side (it's pretty small)

Copy link
Copy Markdown
Member

@stefanvanburen stefanvanburen left a comment

Choose a reason for hiding this comment

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

lgtm after force-push w/ DCO

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.

Try to reraise exception from server handlers or otherwise allow logging them

3 participants