Keep the OIDC callback listener alive when an LLM proxy client disconnects - #6229
Merged
Conversation
The token fetch was rooted in the inbound client's request context, so a client disconnect (e.g. an LLM client's ~11s timeout) tore down the OAuth callback server mid-interactive-login and the completed sign-in was discarded. Thread Start's proxy-lifetime context into the handler instead: an impatient client can no longer abort a login, while Ctrl+C still can. Closes #6227
Pin the context contract: cancelling the inbound request's context must not cancel the in-flight token fetch, while cancelling Start's lifetime context must. Guards the fix against re-rooting in r.Context().
The doc claimed interactive is false for thv llm token, but runLLMToken has passed true since lazy setup landed: a cache miss there is expected to sign the user in transparently. Describe what the flag actually gates.
The reparented fetch changes what a queued request experiences: it now waits on a login that outlives its own client. State that the queue collapses onto a single browser flow so the behaviour is not mistaken for a stall.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #6229 +/- ##
=======================================
Coverage 72.46% 72.46%
=======================================
Files 739 739
Lines 76728 76767 +39
=======================================
+ Hits 55598 55631 +33
- Misses 17163 17172 +9
+ Partials 3967 3964 -3 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
JAORMX
approved these changes
Aug 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
thv llm proxyopens the browser for the OIDC login but the callback listener is gone before the user can finish authenticating, sothv llm setup --lazycannot complete a login through the proxy. The token fetch was rooted in the inbound request's context, which bounds the login's lifetime by the calling client's patience — and no HTTP client waits the 30-90s a person needs at an IdP. When the client gives up,net/httpcancelsr.Context(),oauth.Flow.Startreturns atflow.go:255, and its deferredShutdowntears down the callback server mid-login.Startinstead. A client disconnect can no longer abort a login in progress; Ctrl+C (which cancelsStart's ctx) still can.state, so a completed login on the user's original tab was rejected asinvalid state parameter. With the flow no longer dying on disconnect, there is no successor flow to invalidate the open tab.interactiveparameter onllm.NewTokenSource(runLLMTokenhas passedtruesince lazy setup landed), andtokenFetchTimeout, which now explains what a queued request experiences during the initial login.Closes #6227
Type of change
Test plan
go test -race ./pkg/llm/...green (the fulltask testsuite has not been run on this branch)task test-e2e)task lint-fix)Manual: reproduced the original failure with
thv llm setup --lazy+thv llm proxyagainst Okta — browser opened, the calling tool timed out, and the callback landed on a dead listener. With this change the login completes after the client has already disconnected, and the client's retry is served from the cached token.Two new unit tests pin both halves of the contract:
TestHandler_TokenFetchSurvivesClientDisconnect— cancelling the request ctx does not cancel the fetch ctx.TestHandler_TokenFetchCancelsWithStartContext— cancellingStart's ctx does.Changes
pkg/llm/proxy/proxy.gohandlertakes the proxy-lifetime ctx; token fetch derives from it instead ofr.Context(). Expanded thetokenFetchTimeoutcomment.pkg/llm/proxy/proxy_test.gohandlersignature.pkg/llm/tokensource.gointeractiveactually gates.Does this introduce a user-facing change?
Yes. Interactive OIDC login through
thv llm proxynow works when the calling client times out mid-login, which is the normal case forthv llm setup --lazy. Previously the login could not be completed at all.Special notes for reviewers
The behavioural trade is deliberate and worth a look: a handler goroutine's lifetime is no longer bounded by its client's connection, so during the login window a disconnected client's handler stays parked until the token arrives or
tokenFetchTimeout(3 min) elapses. That is what makes the login survivable. The token source serializesToken()internally, so concurrent requests queue behind the one login rather than each starting their own browser flow — one flow, however many requests. This is documented at thetokenFetchTimeoutdeclaration.Follow-ups I deliberately left out of scope:
thv llm tokeninvoked as anapiKeyHelpershows the same symptom from a different cause — the client kills the child process, so the listener dies with it. No amount of context rooting fixes that; it needs either an eager login or a process-independent listener.oauth.Flowbinds:portrather than127.0.0.1:port(flow.go:202) and treats a foreignstateas fatal instead of returning 400 and continuing to wait (flow.go:322-327). Together that is a LAN-reachable way to kill someone's in-progress login. Worth its own PR.StartbeforeServe(warn-and-continue on failure) would move the cold-start browser tab to the moment the user typed the command, out of the concurrent HTTP path entirely, without breaking what--lazypromises.Generated with Claude Code