@@ -1243,3 +1243,62 @@ async def send(message: Message) -> None:
12431243 assert len (events ) == 2
12441244 assert events [0 ]["type" ] == "http.response.start"
12451245 assert events [1 ]["type" ] == "http.response.pathsend"
1246+
1247+
1248+ def test_error_context_propagation (test_client_factory : TestClientFactory ) -> None :
1249+ class PassthroughMiddleware (BaseHTTPMiddleware ):
1250+ async def dispatch (
1251+ self ,
1252+ request : Request ,
1253+ call_next : RequestResponseEndpoint ,
1254+ ) -> Response :
1255+ return await call_next (request )
1256+
1257+ def exception_without_context (request : Request ) -> None :
1258+ raise Exception ("Exception" )
1259+
1260+ def exception_with_context (request : Request ) -> None :
1261+ try :
1262+ raise Exception ("Inner exception" )
1263+ except Exception :
1264+ raise Exception ("Outer exception" )
1265+
1266+ def exception_with_cause (request : Request ) -> None :
1267+ try :
1268+ raise Exception ("Inner exception" )
1269+ except Exception as e :
1270+ raise Exception ("Outer exception" ) from e
1271+
1272+ app = Starlette (
1273+ routes = [
1274+ Route ("/exception-without-context" , endpoint = exception_without_context ),
1275+ Route ("/exception-with-context" , endpoint = exception_with_context ),
1276+ Route ("/exception-with-cause" , endpoint = exception_with_cause ),
1277+ ],
1278+ middleware = [Middleware (PassthroughMiddleware )],
1279+ )
1280+ client = test_client_factory (app )
1281+
1282+ # For exceptions without context the context is filled with the `anyio.EndOfStream`
1283+ # but it is suppressed therefore not propagated to traceback.
1284+ with pytest .raises (Exception ) as ctx :
1285+ client .get ("/exception-without-context" )
1286+ assert str (ctx .value ) == "Exception"
1287+ assert ctx .value .__cause__ is None
1288+ assert ctx .value .__context__ is not None
1289+ assert ctx .value .__suppress_context__ is True
1290+
1291+ # For exceptions with context the context is propagated as a cause to avoid
1292+ # `anyio.EndOfStream` error from overwriting it.
1293+ with pytest .raises (Exception ) as ctx :
1294+ client .get ("/exception-with-context" )
1295+ assert str (ctx .value ) == "Outer exception"
1296+ assert ctx .value .__cause__ is not None
1297+ assert str (ctx .value .__cause__ ) == "Inner exception"
1298+
1299+ # For exceptions with cause check that it gets correctly propagated.
1300+ with pytest .raises (Exception ) as ctx :
1301+ client .get ("/exception-with-cause" )
1302+ assert str (ctx .value ) == "Outer exception"
1303+ assert ctx .value .__cause__ is not None
1304+ assert str (ctx .value .__cause__ ) == "Inner exception"
0 commit comments