[Bug]: Anthropic OAuth DNS failure leaves a durable intent and permanently requires re-login
I maintainer di solito rispondono entro 1 giorno
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Idoneità per principianti
- 68/100
- Tipo di issue
- Bug
- Chiarezza
- Specificata chiaramente
- Stato di attività
- Attiva
- Stack tecnologico
- bun, typescript
- Ambito
- authentication, backend
Direzione di ricerca
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.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
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});
}
- Lingua principale
- TypeScript
- Stelle
- 16.9k
- Fork
- 1.3k
- Merge medio
- 7h 50m
- PR unite (30g)
- 645
Preparare l'ambiente
Questo progetto non fornisce container di sviluppo, Dockerfile né guida per i contributori, quindi l'ambiente è a tuo carico: parti dal suo README e consulta la nostra guida al primo contributo per i passaggi generali.
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di lidge-jun/opencodex
-
[Bug]: Business Premium seat plan self_serve_business_prolite is counted at baseline capacity weight 1 instead of 5Forse già presa Una pull request collegata a questa issue è aperta o già unita. Apertaaccount-pool bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
lidge-jun/opencodex#7440 · 2 commenti ·
I maintainer di solito rispondono entro 1 giorno
-
[Docs]: translate the #7395 OpenGateway and Mistral provider-guide sectionsForse già presa @MannXo l’ha presa 1 giorno fa. Apertadocumentation
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
lidge-jun/opencodex#7405 · 3 commenti ·
I maintainer di solito rispondono entro 1 giorno
-
[Bug]: resolveCodexRuntime test depends on the host's installed CodexForse già presa Una pull request collegata a questa issue è aperta o già unita. Apertabug cli
Difficoltà 2/5 1-3 ore Idoneità per principianti 74/100
lidge-jun/opencodex#7403 · 2 commenti ·
I maintainer di solito rispondono entro 1 giorno
-
account-pool enhancement needs-design proxy
Difficoltà 2/5 1-3 ore Idoneità per principianti 85/100
lidge-jun/opencodex#6737 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
bug streaming
Difficoltà 4/5 3-5 giorni Idoneità per principianti 45/100
lidge-jun/opencodex#7462 · 4 commenti ·
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di lidge-jun/opencodex
Issue simili
-
frontend
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 86/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 75/100
ranxianglei/billion-context#4172 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 75/100
awslabs/threat-composer#362 ·
I maintainer di solito rispondono entro 1 giorno
-
Area/Cloud Needs Triage Type/Bug
Difficoltà 1/5 1-3 ore Idoneità per principianti 86/100
wso2/product-integrator#2847 ·
I maintainer di solito rispondono entro 1 giorno