@@ -77,45 +77,49 @@ async def sleep_with_background_task(
7777def fastapi_app (fastapi_router : APIRouter ) -> FastAPI :
7878 app = FastAPI ()
7979 app .include_router (fastapi_router )
80- app .add_middleware (RequestCancellationMiddleware )
80+
81+ app .add_middleware (RequestCancellationMiddleware ) # Middleware under test
8182 return app
8283
8384
8485@pytest .fixture
8586def uvicorn_server (fastapi_app : FastAPI ) -> Iterator [URL ]:
86- random_port = unused_port ()
87+ server_port = unused_port ()
8788 with log_context (
8889 logging .INFO ,
89- msg = f"with uvicorn server on 127.0.0.1:{ random_port } " ,
90+ msg = f"with uvicorn server on 127.0.0.1:{ server_port } " ,
9091 ) as ctx :
92+
9193 config = uvicorn .Config (
9294 fastapi_app ,
9395 host = "127.0.0.1" ,
94- port = random_port ,
96+ port = server_port ,
9597 log_level = "error" ,
98+ loop = "uvloop" ,
9699 )
97100 server = uvicorn .Server (config )
98101
99102 thread = Thread (target = server .run )
100103 thread .daemon = True
101104 thread .start ()
102105
106+ server_url = f"http://127.0.0.1:{ server_port } "
107+
103108 @retry (wait = wait_fixed (0.1 ), stop = stop_after_delay (10 ), reraise = True )
104109 def wait_for_server_ready () -> None :
105- with httpx .Client () as client :
106- response = client .get (f"http://127.0.1:{ random_port } /" )
107- assert (
108- response .is_success
109- ), f"Server did not start successfully: { response .status_code } { response .text } "
110+ response = httpx .get (f"{ server_url } /" )
111+ assert (
112+ response .is_success
113+ ), f"Server did not start successfully: { response .status_code } { response .text } "
110114
111115 wait_for_server_ready ()
112116
113117 ctx .logger .info (
114118 "server ready at: %s" ,
115- f"http://127.0.0.1: { random_port } " ,
119+ server_url ,
116120 )
117121
118- yield URL (f"http://127.0.0.1: { random_port } " )
122+ yield URL (server_url )
119123
120124 server .should_exit = True
121125 thread .join (timeout = 10 )
@@ -126,40 +130,49 @@ async def test_server_cancels_when_client_disconnects(
126130 server_done_event : asyncio .Event ,
127131 server_cancelled_mock : AsyncMock ,
128132):
133+ # Implementation of RequestCancellationMiddleware is under test here
134+
129135 async with httpx .AsyncClient (base_url = f"{ uvicorn_server } " ) as client :
130- # check standard call still complete as expected
136+ # 1. check standard call still complete as expected
131137 with log_context (logging .INFO , msg = "client calling endpoint" ):
132138 response = await client .get ("/sleep" , params = {"sleep_time" : 0.1 })
139+
133140 assert response .status_code == 200
134141 assert response .json () == {"message" : "Slept for 0.1 seconds" }
142+
135143 async with asyncio .timeout (10 ):
136144 await server_done_event .wait ()
137145 server_done_event .clear ()
138146
139- # check slow call get cancelled
147+ # 2. check slow call get cancelled
140148 with log_context (
141149 logging .INFO , msg = "client calling endpoint for cancellation"
142150 ) as ctx :
143151 with pytest .raises (httpx .ReadTimeout ):
144- response = await client .get (
145- "/sleep" , params = {"sleep_time" : 10 }, timeout = 0.1
152+ await client .get (
153+ "/sleep" ,
154+ params = {"sleep_time" : 10 },
155+ timeout = 0.1 , # <--- this will enforce the client to disconnect from the server !
146156 )
147157 ctx .logger .info ("client disconnected from server" )
148158
159+ # request should have been cancelled after the ReadTimoeut!
149160 async with asyncio .timeout (5 ):
150161 await server_done_event .wait ()
151162 server_cancelled_mock .assert_called_once ()
152163 server_cancelled_mock .reset_mock ()
153164 server_done_event .clear ()
154165
166+ # 3. check background tasks get cancelled as well sadly
155167 # NOTE: shows that FastAPI BackgroundTasks get cancelled too!
156- # check background tasks get cancelled as well sadly
157168 with log_context (logging .INFO , msg = "client calling endpoint for cancellation" ):
158169 response = await client .get (
159170 "/sleep-with-background-task" ,
160171 params = {"sleep_time" : 2 },
161172 )
162173 assert response .status_code == 200
174+
175+ # request should have been cancelled after the ReadTimoeut!
163176 async with asyncio .timeout (5 ):
164177 await server_done_event .wait ()
165178 server_cancelled_mock .assert_called_once ()
0 commit comments