Skip to content

Commit 373f9f1

Browse files
committed
WSGI
Signed-off-by: Anuraag Agrawal <anuraaga@gmail.com>
1 parent 485b5d9 commit 373f9f1

4 files changed

Lines changed: 107 additions & 67 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ classifiers = [
2828
"Topic :: Software Development :: Libraries :: Python Modules",
2929
"Typing :: Typed",
3030
]
31-
dependencies = ["protobuf>=5.28", "pyqwest>=0.4.1"]
31+
dependencies = ["protobuf>=5.28", "pyqwest>=0.5.1"]
3232

3333
[project.urls]
3434
Documentation = "https://connectrpc.com/docs/python/getting-started/"

src/connectrpc/_server_sync.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import base64
44
import functools
5+
import traceback
56
from abc import ABC, abstractmethod
67
from dataclasses import replace
78
from http import HTTPStatus
@@ -46,9 +47,9 @@
4647
from .compression import Compression
4748

4849
if sys.version_info >= (3, 11):
49-
from wsgiref.types import StartResponse, WSGIEnvironment
50+
from wsgiref.types import ErrorStream, StartResponse, WSGIEnvironment
5051
else:
51-
from _typeshed.wsgi import StartResponse, WSGIEnvironment
52+
from _typeshed.wsgi import ErrorStream, StartResponse, WSGIEnvironment
5253
else:
5354
StartResponse = "wsgiref.types.StartResponse"
5455
WSGIEnvironment = "wsgiref.types.WSGIEnvironment"
@@ -251,6 +252,7 @@ def __call__(
251252

252253
except Exception as e:
253254
_drain_request_body(environ)
255+
_maybe_log_exception(environ, e)
254256
return self._handle_error(e, ctx, start_response)
255257

256258
def _handle_unary(
@@ -502,6 +504,7 @@ def _handle_stream(
502504
# response message will be handled by _response_stream, so here we have a
503505
# full error-only response.
504506
_drain_request_body(environ)
507+
_maybe_log_exception(environ, e)
505508
_send_stream_response_headers(
506509
start_response, protocol, codec, resp_compression.name(), ctx
507510
)
@@ -668,3 +671,12 @@ def _drain_request_body(environ: WSGIEnvironment) -> None:
668671
# server that doesn't do so, so we go ahead and do it ourselves.
669672
for _ in _read_body(environ):
670673
pass
674+
675+
676+
def _maybe_log_exception(environ: WSGIEnvironment, exc: Exception) -> None:
677+
if isinstance(exc, (ConnectError, HTTPException)):
678+
return
679+
errors: ErrorStream = environ["wsgi.errors"]
680+
errors.write(
681+
f"Exception in WSGI application\n{''.join(traceback.format_exception(type(exc), exc, exc.__traceback__))}"
682+
)

test/test_errors.py

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -491,9 +491,7 @@ async def make_hat(self, request, ctx) -> NoReturn:
491491
with pytest.raises(ConnectError, match="We're broken"):
492492
await client.make_hat(request=Size(inches=10))
493493

494-
# Workaround https://github.com/curioswitch/pyqwest/pull/148
495-
# TODO: Remove after fix is released
496-
assert getattr(transport, "_app_exception", None) is None
494+
assert transport.app_exception is None
497495

498496

499497
@pytest.mark.asyncio
@@ -515,9 +513,7 @@ def make_similar_hats(
515513
async for _ in client.make_similar_hats(request=Size(inches=10)):
516514
pass
517515

518-
# Workaround https://github.com/curioswitch/pyqwest/pull/148
519-
# TODO: Remove after fix is released
520-
assert getattr(transport, "_app_exception", None) is None
516+
assert transport.app_exception is None
521517

522518

523519
@pytest.mark.asyncio
@@ -536,6 +532,50 @@ async def make_hat(self, request, ctx) -> NoReturn:
536532
with pytest.raises(ConnectError, match="Internal Server Error"):
537533
await client.make_hat(request=Size(inches=10))
538534

539-
# Workaround https://github.com/curioswitch/pyqwest/pull/148
540-
# TODO: Remove after fix is released
541-
assert getattr(transport, "_app_exception", None) is None
535+
assert transport.app_exception is None
536+
537+
538+
def test_sync_unhandled_exception_logged() -> None:
539+
class RaisingHaberdasher(HaberdasherSync):
540+
def make_hat(self, request, ctx) -> NoReturn:
541+
raise TypeError("Something went wrong")
542+
543+
app = HaberdasherWSGIApplication(RaisingHaberdasher())
544+
transport = WSGITransport(app)
545+
http_client = SyncClient(transport)
546+
547+
with (
548+
HaberdasherClientSync(
549+
"http://localhost", timeout_ms=200, http_client=http_client
550+
) as client,
551+
pytest.raises(ConnectError, match="Something went wrong"),
552+
):
553+
client.make_hat(request=Size(inches=10))
554+
555+
logged_error = transport.error_stream.getvalue()
556+
assert "Exception in WSGI application" in logged_error
557+
assert "TypeError: Something went wrong" in logged_error
558+
assert "Traceback" in logged_error
559+
560+
561+
def test_sync_unhandled_exception_logged_stream() -> None:
562+
class RaisingHaberdasher(HaberdasherSync):
563+
def make_similar_hats(self, request, ctx) -> NoReturn:
564+
raise TypeError("Something went wrong")
565+
566+
app = HaberdasherWSGIApplication(RaisingHaberdasher())
567+
transport = WSGITransport(app)
568+
http_client = SyncClient(transport)
569+
570+
with (
571+
HaberdasherClientSync(
572+
"http://localhost", timeout_ms=200, http_client=http_client
573+
) as client,
574+
pytest.raises(ConnectError, match="Something went wrong"),
575+
):
576+
next(client.make_similar_hats(request=Size(inches=10)))
577+
578+
logged_error = transport.error_stream.getvalue()
579+
assert "Exception in WSGI application" in logged_error
580+
assert "TypeError: Something went wrong" in logged_error
581+
assert "Traceback" in logged_error

0 commit comments

Comments
 (0)