[Bug]: Anthropic OAuth DNS failure leaves a durable intent and permanently requires re-login
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 68/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- bun, typescript
- Domain
- authentication, backend
Research direction
Start with src/oauth/index.ts and src/oauth/anthropic.ts, then run the provided Bun reproduction against the package or repository; it exercises the real refresh path with an isolated auth store. Compare DNS ENOTFOUND with the HTTP 503 and ambiguous-timeout controls. Done means a recovered endpoint can refresh after the pre-connection DNS failure while the replay guard remains for ambiguous outcomes.
Written by the indexing model from the issue text.
Description
Summary
An Anthropic OAuth refresh that fails before connecting to the token endpoint with getaddrinfo ENOTFOUND api.anthropic.com leaves a durable refresh intent. The next refresh attempt marks the account needsReauth: true and throws OAuthLoginRequiredError without calling the token endpoint again, even when the mocked network has recovered.
This turns a DNS outage into a persistent manual-relogin requirement. It is distinct from an ambiguous timeout after a request might have reached Anthropic: the existing replay guard should remain for those ambiguous outcomes.
Environment
- Client: OpenCodex dashboard / OAuth account resolver
- Area: Authentication and account pool
- Installed package:
@bitkyc08/opencodex2.76.0 - OS: macOS 27.0.1, arm64
- Reproduced with packaged Bun 1.4.0 and global Bun 1.4.2
- Provider: Anthropic,
authMode: "oauth", credentialsource: "oauth"; no model inference is needed
Observed live symptom and limits of attribution
The user reports recurrent Anthropic disconnections. Read-only inspection found:
GET /api/oauth/accounts?provider=anthropic -> HTTP 200
account count: 1
needsReauth: true
paused: false
credential source: oauth
refresh token present: true
access credential expired: true
pending auth.refresh.anthropic.<redacted>.lock.json: present
pending intent generation matches current credential generation: true
The service log also contains repeated TypeError: getaddrinfo ENOTFOUND api.anthropic.com errors from the quota endpoint (/api/oauth/usage) and timeout errors. The log does not record enough information to attribute the outstanding token refresh attempt conclusively to DNS failure rather than an ambiguous timeout or another interrupted attempt. No claim is made that the real saved refresh token is still accepted by Anthropic.
The deterministic reproduction below independently confirms the DNS failure handling defect. The live account and its replay guard were not deleted or modified, and its refresh token was not manually replayed.
Reproduction
Save the script below as anthropic-refresh-repro.mjs and run:
bun anthropic-refresh-repro.mjs /path/to/opencodex
The argument is the installed npm package directory or repository root containing src/. The script runs the real refreshAnthropicAccountWithLock and refreshAnthropicToken implementations against an isolated temporary auth store with fake credentials. Only the HTTP transport is stubbed; no account or external network is needed.
It tests three scenarios, each followed by a simulated recovered token endpoint:
| First failure | Fetch calls across two attempts | Second result | needsReauth | Intent remains |
|---|---|---|---|---|
| Explicit HTTP 503 | 2 | Success | false | false |
| DNS ENOTFOUND | 1 | OAuthLoginRequiredError | true | true |
| Ambiguous timeout (control) | 1 | OAuthLoginRequiredError | true | true |
The script exits 1 at BUG: DNS failure before connecting must not permanently require re-login. Repeated runs produced the same result in under a second.
Code path
In installed 2.76.0, src/oauth/index.ts, refreshAnthropicAccountWithLock:
- Writes the durable intent before calling
def.refresh. - Sets
refreshMayHaveReachedProvider = truebefore entering the adapter. - The adapter in
src/oauth/anthropic.tspropagates transport errors without structured evidence of whether the request could have reached the server. - An ENOTFOUND error is nonterminal, but fails
definitivelyAnswered(error), which accepts only anAnthropicTokenErrorwith HTTP status. The intent remains. - On the next call, the same-generation pending-intent branch marks the account as needing reauthentication before any network call.
- The token guardian subsequently skips accounts marked
needsReauth; the flag and intent also survive process restart.
The same broad dispatch classification and pending-intent branches are present in the inspected upstream source at 06841165 (package version 2.77.0). The runnable reproduction was executed on installed 2.76.0, not on a full checkout of 2.77.0.
Expected behavior / suggested fix scope
Preserve structured evidence of a definitive pre-connection failure in the Anthropic token transport. Clear the corresponding attempt intent through the existing generation-safe cleanup path and allow retry/backoff for that narrow class of failure.
Do not clear intents for every network error: a timeout, dropped connection after dispatch, unreadable successful response, or persistence failure can conceal an already-rotated refresh token. Those cases still need the replay guard. Proxy, redirects, and transport retries must be considered before asserting that no request reached the token endpoint.
Also consider recording a sanitized refresh failure category and distinguishing an unknown refresh outcome from an explicitly rejected grant in the account-health UI. Currently the live state cannot explain which of those caused the reconnect requirement.
Related issue
Issue #209 describes a similar user-visible symptom on an older release. This report concerns the durable-intent / pre-connection failure path in 2.76.0, with a runnable reproduction and an HTTP-503 control.
No secrets, actual account identifiers, email addresses, user paths, or original credential files are included.
Reproduction script
// Run with Bun; argument: installed opencodex package directory or repository root.
// Uses only fake credentials in a temporary directory. No external network calls.
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
import assert from 'node:assert/strict';
const root = resolve(process.argv[2] ?? '.');
const scratch = mkdtempSync(join(tmpdir(), 'ocx-anthropic-repro-'));
process.env.OPENCODEX_HOME = scratch;
const scenarios = ['http503', 'dns-enotfound', 'ambiguous-timeout'];
const credential = {access:'fake-access',refresh:'fake-refresh',expires:Date.now()-60_000,source:'oauth'};
writeFileSync(join(scratch,'auth.json'),JSON.stringify({anthropic:{activeAccountId:scenarios[0],accounts:scenarios.map(id=>({id,credential}))}}),{mode:0o600});
const realFetch = globalThis.fetch;
try {
const {refreshAnthropicAccountWithLock} = await import(pathToFileURL(join(root,'src/oauth/index.ts')).href);
const {refreshAnthropicToken} = await import(pathToFileURL(join(root,'src/oauth/anthropic.ts')).href);
const {getAccountCredentialWithStatus,readOAuthRefreshIntent} = await import(pathToFileURL(join(root,'src/oauth/store.ts')).href);
const results=[];
for(const scenario of scenarios){
let calls=0;
globalThis.fetch=async url=>{
assert.equal(String(url),'https://api.anthropic.com/v1/oauth/token');
calls++;
if(calls===1){
if(scenario==='http503')return new Response('{"error":"temporarily_unavailable"}',{status:503});
if(scenario==='dns-enotfound')throw Object.assign(new TypeError('getaddrinfo ENOTFOUND api.anthropic.com'),{code:'ENOTFOUND',syscall:'getaddrinfo',hostname:'api.anthropic.com'});
throw new DOMException('The operation timed out.','TimeoutError');
}
return Response.json({access_token:'fake-new-access',refresh_token:'fake-new-refresh',expires_in:3600});
};
const errors=[];
for(let attempt=0;attempt<2;attempt++){
try{await refreshAnthropicAccountWithLock('anthropic',scenario,{refresh:refreshAnthropicToken},credential);errors.push(null);}
catch(e){errors.push(e.name);}
}
const row=getAccountCredentialWithStatus('anthropic',scenario);
results.push({scenario,fetchCalls:calls,errors,needsReauth:row.needsReauth,pendingIntent:!!readOAuthRefreshIntent('anthropic',scenario),recovered:row.credential.access==='fake-new-access'});
}
console.log(JSON.stringify(results,null,2));
assert.equal(results[0].recovered,true,'Control: explicit HTTP 503 should recover');
assert.equal(results[2].pendingIntent,true,'Ambiguous timeout must preserve replay guard');
assert.equal(results[1].recovered,true,'BUG: DNS failure before connecting must not permanently require re-login');
} finally {
globalThis.fetch=realFetch;
rmSync(scratch,{recursive:true,force:true});
}
- Dominant language
- TypeScript
- Stars
- 16.9k
- Forks
- 1.3k
- Avg merge
- 7h 38m
- Merged PRs (30d)
- 659
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from lidge-jun/opencodex
-
[Bug]: Business Premium seat plan self_serve_business_prolite is counted at baseline capacity weight 1 instead of 5Possibly taken A pull request linked to this issue is open or already merged. Openaccount-pool bug
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
lidge-jun/opencodex#7440 · 2 comments ·
Maintainers usually reply within 1 day
-
[Docs]: translate the #7395 OpenGateway and Mistral provider-guide sectionsPossibly taken @MannXo claimed this 1 day ago. Opendocumentation
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
lidge-jun/opencodex#7405 · 3 comments ·
Maintainers usually reply within 1 day
-
bug cli
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
lidge-jun/opencodex#7403 · 2 comments ·
Maintainers usually reply within 1 day
-
account-pool enhancement needs-design proxy
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
lidge-jun/opencodex#6737 · 1 comment ·
Maintainers usually reply within 1 day
-
enhancement proxy
Difficulty 4/5 1-2 days Newbie friendliness 55/100
lidge-jun/opencodex#7443 · 3 comments ·
Maintainers usually reply within 1 day
All issues in lidge-jun/opencodex
Similar issues
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 67/100
mobile-next/mobile-mcp#526 ·
Maintainers usually reply within 1 day
-
e2e-failure ready-to-code
Difficulty 1/5 Under an hour Newbie friendliness 73/100
redhat-developer/rhdh-plugin-export-overlays#4470 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
anthropics/anthropic-sdk-typescript#1273 ·
Maintainers usually reply within 10 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
CopilotKit/OpenBot#815 ·
Maintainers usually reply within 1 day