fix(oauth): abort hung grant refresh token fetches - #112
Conversation
refreshGrant posted to tokenUrl with no AbortSignal. A hung provider left grant refresh and proxy grant selection pending. Reuse fetchTimeoutSignal (30s) and map abort/timeout to grant_refresh_failed. Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
|
🦞👀 Pull request received. I will update this pull request when review starts. |
|
Codex review: needs maintainer review before merge. Reviewed August 16, 2026, 7:32 AM ET / 11:32 UTC. ClawSweeper reviewWhat this changesThis PR adds a 30-second timeout to OAuth grant-refresh token requests so a hung provider endpoint fails cleanly instead of leaving refresh work pending. Merge readinessKeep open: current main still leaves the grant-refresh token POST unbounded, while this focused PR converts a provider stall into the established 502 failure path with credible real-behavior proof. Priority: P2 Review scores
Verification
How this fits togetherThe Worker refreshes stored OAuth grants through provider token endpoints, then saves the updated grant for admin actions and proxy grant selection. This change bounds that outbound request and returns the existing refresh error when the provider does not respond. flowchart LR
A[Stored OAuth grant] --> B[Grant refresh worker]
B --> C[Provider token request]
C --> D{Response before timeout?}
D -->|Yes| E[Validate and store grant]
D -->|No| F[Grant refresh failure]
F --> G[Admin and proxy receive 502]
Before merge
Agent review detailsSecurityNone. Review metrics
Merge-risk optionsMaintainer options:
Technical reviewBest possible solution: Land the bounded failure behavior with the focused regression coverage, retaining the existing grant_refresh_failed response and no persisted grant update on timeout. Do we have a high-confidence way to reproduce the issue? Yes—current main awaits the refresh POST without a signal, and a local HTTP server that accepts a connection without responding exercises that path; the contributor supplied an after-fix production-owner trace using this setup. Is this the best way to solve the issue? Yes—the timeout is applied at the one unbounded production refresh boundary and preserves the existing error contract, making it narrower than adding provider-specific behavior. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against ca27bc44227f. LabelsLabel justifications:
EvidenceWhat I checked:
Likely related people:
Rating scale
Overall follows the weaker of proof and patch quality. Workflow
HistoryReview history (1 earlier review cycle)
|
# Conflicts: # shared/fetch-timeout.ts # worker/test/fetch-timeout.test.mjs
What Problem This Solves
refreshGrantinworker/providers.tsposts the OAuth refresh token totokenUrlwith noAbortSignal. If the provider token endpoint accepts the TCP connection and never responds, admin grant refresh and proxy grant selection stay pending. The same hang class is already fixed for dashboard fetches and the OAuth callback token POST in #111. This change covers the remaining grant-refresh POST.Evidence
Live Node against a TCP server that accepts the connection and never writes an HTTP response. The shared helper aborts in ~80ms when given an 80ms budget. Production
refreshStoredGrant()uses the default 30s budget, returnsgrant_refresh_failed(502), and does not persist a new grant.Patched call site on this branch:
A hung or aborted token POST now takes the existing
grant_refresh_failedpath instead of leaving the Worker fetch pending.Real behavior proof
AbortSignal.timeout. A hungtokenUrlfinishes asgrant_refresh_failed(502) and does not write a new grant to KV./tmp/oc-impl-clawrouter-refreshonfix/refresh-grant-fetch-timeout. Livenodeagainst a local hanging HTTP server (accept, no response body).node:httpserver that never writes a response. CalledfetchTimeoutSignal(undefined, 80)onPOST /oauth/token, then called productionrefreshStoredGrant()with that hungtokenUrlso the default 30s budget is the one compiled intoworker/providers.ts.TimeoutErrorat 91ms. ProductionrefreshStoredGrant()recordedAbortSignal.timeout(30000)and finished at 30003ms withgrant_refresh_failed/ 502. KV was not updated.tokenUrl. Dashboard and OAuth callback token POSTs (covered by fix: abort hung dashboard and OAuth token fetches #111). Browser click of the access-console Refresh action.Summary
fetchTimeoutSignalinshared/fetch-timeout.ts(30s default). Copied independently because fix: abort hung dashboard and OAuth token fetches #111 is not onmainyet. Same helper as that sibling PR.refreshGranttoken POST uses the helper and maps abort or network failure togrant_refresh_failedfetchlanded in openclaw/clawrouter#55 (61cba08, 2026-06-22) and has been present for 54 daysAbortSignal.timeout(10_000)