Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

[Bug]: Anthropic OAuth DNS failure leaves a durable intent and permanently requires re-login

Chiusa
#6,570 3 commenti 0 reazioni 0 assegnatari Vedi su GitHub

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

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

account-pool bug

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/opencodex 2.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", credential source: "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:

  1. Writes the durable intent before calling def.refresh.
  2. Sets refreshMayHaveReachedProvider = true before entering the adapter.
  3. The adapter in src/oauth/anthropic.ts propagates transport errors without structured evidence of whether the request could have reached the server.
  4. An ENOTFOUND error is nonterminal, but fails definitivelyAnswered(error), which accepts only an AnthropicTokenError with HTTP status. The intent remains.
  5. On the next call, the same-generation pending-intent branch marks the account as needing reauthentication before any network call.
  6. 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

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di lidge-jun/opencodex

Tutte le issue di lidge-jun/opencodex

Issue simili

Altre issue su TypeScript

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.