auth: bind refreshing token source to a background context, not the request ctx#988
Open
toabctl wants to merge 2 commits into
Open
auth: bind refreshing token source to a background context, not the request ctx#988toabctl wants to merge 2 commits into
toabctl wants to merge 2 commits into
Conversation
…equest ctx
AuthorizationCodeHandler.exchangeAuthorizationCode built the long-lived
token source with cfg.TokenSource(clientCtx, token), where clientCtx
derives from the per-request context passed to Authorize(). golang.org/x/
oauth2 captures that context inside tokenRefresher and reuses it for every
subsequent refresh round-trip (oauth2.go: Config.TokenSource ->
tokenRefresher{ctx}; token.go: ContextClient(ctx).Do(req.WithContext(ctx))).
Authorize() is typically called from a request- or connect-scoped context
(e.g. the transport's setMCPHeaders/initialize path), which is cancelled
once that operation completes. The cached access token works until it
expires; the first refresh after expiry then fails immediately with
"context canceled" before any HTTP request is sent, leaving the connection
permanently unable to refresh. setMCPHeaders only re-runs Authorize() for an
invalid_grant oauth2.RetrieveError, so a context-cancellation error is not
recovered and the connection is wedged until the client reconnects.
A token source outlives the request that created it, so bind its refreshes
to context.Background() (still carrying the configured HTTP client via
oauth2.HTTPClient) instead of the request context. The one-shot token
exchange continues to use the request context.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mdhannanmiah110-png
approved these changes
Jun 3, 2026
mdhannanmiah110-png
approved these changes
Jun 3, 2026
Adds opt-in refresh support to the fake authorization server (a configurable
access-token TTL and a refresh_token grant; both default off so existing
tests are unchanged) and a test that authorizes under a cancellable context,
cancels it, then forces a refresh. Without the fix it reproduces the
production failure exactly:
Post ".../token": context canceled
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Problem
AuthorizationCodeHandler.exchangeAuthorizationCodebuilds the long-lived token source from the request context passed toAuthorize():golang.org/x/oauth2captures that context and reuses it for every future refresh:oauth2.go:Config.TokenSource(ctx, …)storesctxintokenRefresher{ctx}tokenRefresher.Token()(no ctx arg) callsretrieveToken(tf.ctx, …)internal/token.go:ContextClient(ctx).Do(req.WithContext(ctx))Authorize()is normally invoked from a request- or connect-scoped context — e.g. the streamable client transport calls it duringsetMCPHeaders/initialize, and that context is cancelled once the operation completes. The cached access token works until expiry; the first refresh after expiry then fails instantly withcontext canceled, before any HTTP request is sent. The connection can no longer refresh.setMCPHeadersonly re-invokesAuthorize()for aninvalid_grantoauth2.RetrieveError(#917). Acontext canceledis not aRetrieveError, so it is never recovered — the connection is wedged until the client reconnects.Reproduce
StreamableClientTransportwith anAuthorizationCodeHandlerto a server whoseinitializereturns 401 (soAuthorize()runs during connect, under the connect context).context.WithTimeout(parent, …)+defer cancel()bounding the interactive flow).Post "<token endpoint>": context canceledanddur≈0— the refresh never hits the network.Fix
A token source outlives the request that created it, so bind its refreshes to
context.Background()(still carrying the configured HTTP client viaoauth2.HTTPClient). The one-shot token exchange keeps using the request context.authpackage tests pass. Behaviour is otherwise unchanged: the same HTTP client value is carried; only the cancellation parent differs.🤖 Generated with Claude Code