| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
PR SummaryMedium Risk Overview The /execute handler passes the Starlette Request into ContextWebSocket.execute(). While streaming, _wait_for_result polls request.is_disconnected() on keepalive ticks and before forwarding output; on disconnect it raises _ClientDisconnected, triggers a shielded kernel interrupt, and exits cleanly so the lock is released. Older behavior via CancelledError / GeneratorExit is still handled. Reviewed by Cursor Bugbot for commit cd53fc6. Bugbot is set up for automated code reviews on this repo. Configure here. |
Sorry, something went wrong.
There was a problem hiding this comment.
Here are some automated review suggestions for this pull request.
Reviewed commit: cf6255b891
ℹ️ About Codex in GitHubCodex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Sorry, something went wrong.
| # after polling request.is_disconnected() (Starlette >= 1.0). | ||
| try: | ||
| async for item in self._wait_for_result(message_id): | ||
| async for item in self._wait_for_result(message_id, request=request): |
There was a problem hiding this comment.
Preserve the FastAPI request for disconnect polling
For any execution that produces no output for KEEPALIVE_INTERVAL (for example time.sleep(10)), the local request has already been rebound above to the JSON websocket payload string, so this call passes a str into _wait_for_result. On the first timeout _wait_for_result calls await request.is_disconnected(), which raises AttributeError and aborts the stream instead of continuing or interrupting on an actual disconnect. Keep the FastAPI Request in a separate variable from the Jupyter execute payload.
Useful? React with 👍 / 👎.
Sorry, something went wrong.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit cf6255b. Configure here.
Sorry, something went wrong.
| # When we detected the disconnect ourselves, stop streaming | ||
| # gracefully. Cancellation/GeneratorExit must propagate. | ||
| if isinstance(e, _ClientDisconnected): | ||
| return |
There was a problem hiding this comment.
Medium Severity
The streaming disconnect handler only catches CancelledError, GeneratorExit, and internal _ClientDisconnected. On Starlette ≥ 1.0 a dead client can also surface as connection errors (including Starlette’s ClientDisconnect) while flushing a yielded chunk, so those paths skip interrupt() even though the finally block drops the execution entry.
Reviewed by Cursor Bugbot for commit cf6255b. Configure here.
Sorry, something went wrong.
| await execution.queue.put(UnexpectedEndOfExecution()) | ||
|
|
||
| # Stream the results. | ||
| # If the client disconnects (Starlette cancels the task), we | ||
| # interrupt the kernel so the next execution isn't blocked (#213). | ||
| # If the client disconnects, we interrupt the kernel so the next | ||
| # execution isn't blocked (#213). Depending on the Starlette | ||
| # version this surfaces either as cancellation of this generator | ||
| # (older Starlette) or as a _ClientDisconnected we raise ourselves | ||
| # after polling request.is_disconnected() (Starlette >= 1.0). | ||
| try: | ||
| async for item in self._wait_for_result(message_id): | ||
| async for item in self._wait_for_result(message_id, request=request): |
There was a problem hiding this comment.
🔴 Critical: The new request: Optional[Request] = None parameter on execute() is shadowed by the local assignment at line 415 — request = self._get_execute_request(message_id, complete_code, False) — which returns a JSON str. When self._wait_for_result(message_id, request=request) then runs, request.is_disconnected() on the string raises AttributeError on the first 5s keepalive tick. AttributeError isn't in execute()'s except (CancelledError, GeneratorExit, _ClientDisconnected) tuple, so it propagates, the kernel is never interrupted, and the very test_subsequent_execution_works_after_client_timeout test this PR exists to fix will still fail. Fix: rename the local payload variable (e.g. exec_payload).
Extended reasoning...The PR adds a new request: Optional[Request] = None parameter to ContextWebSocket.execute() (messaging.py:341) so that _wait_for_result can poll request.is_disconnected() and raise _ClientDisconnected to interrupt the kernel. The problem is that inside execute()'s retry loop at lines 411-415, the local variable name request is reused for the JSON payload to send over the WebSocket:
request = self._get_execute_request(message_id, complete_code, False)
await self._ws.send(request)_get_execute_request is annotated -> str and returns json.dumps(...). This rebinds the local name request to a string, shadowing the Request parameter for the remainder of the function.
At line 421, async for item in self._wait_for_result(message_id, request=request): then passes the JSON string (not the original Starlette Request) into _wait_for_result. Inside _wait_for_result (lines 295-296):
if request is not None and await request.is_disconnected():
raise _ClientDisconnected()A non-empty string is truthy, so Python evaluates request.is_disconnected — strings have no such attribute, so Python raises AttributeError: 'str' object has no attribute 'is_disconnected' before await even runs.
execute()'s exception handler only catches (asyncio.CancelledError, GeneratorExit, _ClientDisconnected) (line 423). AttributeError is not in that tuple, so it escapes the generator and propagates up through StreamingListJsonResponse, breaking the streaming response entirely. The kernel-interrupt path on line 427 never runs.
The KEEPALIVE_INTERVAL is 5 seconds, so any execution that takes longer than 5 seconds will trigger the first timeout and immediately crash with AttributeError. This means:
Rename the local payload variable so the Request parameter is preserved. Smallest patch is at lines 411-416:
exec_payload = self._get_execute_request(message_id, complete_code, False)
await self._ws.send(exec_payload)(_cleanup_env_vars and change_current_directory also use a local named request for the JSON payload, but they don't take a Request parameter, so the shadowing only matters in execute().)
Sorry, something went wrong.
Bumping FastAPI 0.111.0 -> 0.136.3 pulls in Starlette 1.2.1, which broke the #213 disconnect->interrupt behavior. Starlette >= 1.0 takes a new StreamingResponse path for ASGI spec_version >= 2.4 (advertised by uvicorn 0.30.1): it no longer runs listen_for_disconnect concurrently and no longer cancels the response body iterator on http.disconnect. The interrupt relied on that cancellation, so an abandoned execution was never interrupted and the next execution blocked behind it and timed out. This was the only failing test in both SDKs on the Renovate bump (#207): - js: tests/interrupt.test.ts > subsequent execution works after client timeout - python: test_async_interrupt.py::test_subsequent_execution_works_after_client_timeout Detect the disconnect explicitly: thread the Request into execute(), and on each keepalive tick poll request.is_disconnected(). When it flips, raise an internal _ClientDisconnected, interrupt the kernel, and stop streaming. The old cancellation path is still handled for older Starlette. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| Back | FazBrowse Home | New Git URL |
What
Supersedes the Renovate bump #207 (fastapi 0.111.0 → 0.136.3), which fails CI, by including the dependency bump plus the fix it requires.
The problem
PR #207 only bumps fastapi==0.111.0 → fastapi==0.136.3 in template/server/requirements.txt, yet both SDK jobs fail — on the same test:
Both fail with a TimeoutError/TimeoutException on the second execution.
Root cause
This is the #213 behavior: when a client disconnects mid-execution, the server interrupts the kernel so the next execution isn't blocked. The interrupt relied on Starlette cancelling the streaming response body iterator on http.disconnect.
Bumping FastAPI 0.111.0 → 0.136.3 pulls Starlette 0.37.2 → 1.2.1. Starlette ≥ 1.0 added a new StreamingResponse.__call__ path for ASGI spec_version >= (2, 4) (which uvicorn 0.30.1 advertises):
It no longer runs listen_for_disconnect concurrently and no longer cancels the body iterator. So the execute() generator never receives CancelledError/GeneratorExit, the kernel is never interrupted, and the abandoned time.sleep(300) blocks the next execution until it times out.
Verified empirically against starlette==1.2.1 + uvicorn==0.30.1: on client disconnect the body generator is not cancelled, but request.is_disconnected() does flip to True.
The fix
Detect the disconnect explicitly instead of relying on Starlette cancellation:
End-to-end reproduction (uvicorn 0.30.1 + starlette 1.2.1, mirroring the real queue/keepalive/StreamingListJsonResponse structure) confirms the kernel is interrupted within ~1–2s of disconnect.
Notes
🤖 Generated with Claude Code