[wrangler] fix: re-read refresh_token from disk to avoid 401 from sibling-process rotation#13910
Merged
petebacondarwin merged 2 commits intoMay 14, 2026
Conversation
🦋 Changeset detectedLatest commit: 2011b3c The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Contributor
|
Codeowners approval required for this PR:
Show detailed file reviewers |
create-cloudflare
@cloudflare/kv-asset-handler
miniflare
@cloudflare/pages-shared
@cloudflare/unenv-preset
@cloudflare/vite-plugin
@cloudflare/vitest-pool-workers
@cloudflare/workers-editor-shared
@cloudflare/workers-utils
wrangler
commit: |
petebacondarwin
approved these changes
May 13, 2026
workers-devprod
approved these changes
May 13, 2026
workers-devprod
left a comment
Contributor
There was a problem hiding this comment.
Codeowners reviews satisfied
Member
|
mh.... something off happened here, would you like some help rebasing this PR @timoconnellaus? 🙂 |
…ling rotation
OAuth refresh tokens are single-use. When a sibling wrangler process refreshes,
it rotates the token server-side and writes the new value to the shared global
config file. A long-lived process (typically `wrangler dev`) that loaded its
refresh_token at startup then sends the stale in-memory value on its next
refresh and gets `401 Unauthorized` from `/oauth2/token`, falling through to
interactive login and timing out unattended.
`refreshToken` now calls `reinitialiseAuthTokens()` before exchanging so it
picks up whatever the latest write put on disk. The previously empty
`catch {}` in the same function also now logs the underlying error at debug
level so future refresh failures are diagnosable.
Repro test added to `user.test.ts` (fails without this change, passes with it).
…rom sibling rotation
324414e to
2011b3c
Compare
petebacondarwin
approved these changes
May 14, 2026
Merged
5 tasks
petebacondarwin
added a commit
that referenced
this pull request
May 19, 2026
…riority Wrangler previously read its OAuth state from the user auth config file eagerly at module-import time, before `.env` files have been loaded. The in-memory state therefore always held the OAuth tokens even when the user only wanted to authenticate via `CLOUDFLARE_API_TOKEN`. If that stored OAuth token happened to be expired, Wrangler would try to refresh it (and fail), aborting the command with 'Failed to fetch auth token: 400 Bad Request' / 'Not logged in.' — even though a valid API token was in scope. Read the auth config file on demand from every site that needs it. `isAccessTokenExpired()` short-circuits to `false` when env-based credentials are present, so the OAuth refresh endpoint is no longer called when env auth is going to be used. Sibling-process refresh-token rotation is also handled naturally because every check reads the current file contents. Drive-by cleanups: - Drop the dead in-memory mutations in `exchangeRefreshTokenForAccessToken` and `exchangeAuthCodeForAccessToken` — they were redundant with the `writeAuthConfigFile` call that immediately follows. - Drop the dead inner `!localState.accessToken && localState.refreshToken` branch in `logout()` plus its accidental double-revoke. - Remove the now-redundant `reinitialiseAuthTokens()` call from the top of `refreshToken()` (introduced in #13910 for sibling rotation; now naturally subsumed). - Drop the in-process selected-account cache in favour of the existing file-based `wrangler-account.json` cache, which is naturally per-cwd. - Remove the exported `reinitialiseAuthTokens()` function — there is no module-level cache left to invalidate. Fixes #13744.
penalosa
pushed a commit
that referenced
this pull request
May 28, 2026
…ling-process rotation (#13910) Co-authored-by: Pete Bacon Darwin <pbacondarwin@cloudflare.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.
Summary
refreshTokeninpackages/wrangler/src/user/user.tsuses the refresh token cached in module-levellocalState, which is populated once at process startup and never re-read. OAuth refresh tokens are single-use: when a sibling wrangler process (another repo, another shell, or a parallel script) refreshes first, it rotates the token server-side and writes the new value to the shared global config file (~/Library/Preferences/.wrangler/config/default.tomlon macOS, equivalents elsewhere). The long-lived process — typicallywrangler dev— then sends its stale in-memory token on the next refresh and gets401 Unauthorizedfromhttps://dash.cloudflare.com/oauth2/token, falling through to a fresh interactive OAuth login.This matches every
Failed to fetch auth token: 401 Unauthorizedreport I can find — and reproduces deterministically in a unit test.Evidence from a real failure
Two wrangler processes on the same machine, started 60 minutes apart in different repos, both sent the same
refresh_token=07RHxMHc…:Fix
Two changes inside
refreshToken()(packages/wrangler/src/user/user.ts):reinitialiseAuthTokens()before exchanging. Re-readsdefault.tomlso the refresh request uses whatever value is currently on disk, not whatever was on disk when this process started. Cheap, idiomatic, eliminates the race for the realistic case where the sibling write lands seconds-to-hours before this refresh.catch {}with alogger.debugcall. The current swallowed error is exactly why this bug was hard to diagnose —WRANGLER_LOG=debugwill now surface the failure mode.A proper
flock-style fix ondefault.tomlis nicer but materially harder; this gets 99%+ of cases.Test plan
packages/wrangler/src/__tests__/user.test.ts(should re-read refresh_token from disk before refreshing in case a sibling process rotated it) — fails onmain, passes with this changepnpm test -F wrangler src/__tests__/user.test.ts— 51/51 passpnpm test -F wrangler src/__tests__/deploy/core.test.ts(other refresh-touching tests) — 48/48 passpnpm check:type -F wranglercleanoxlint --deny-warnings --type-awarecleanoxfmt --checkclean🤖 Generated with Claude Code