Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

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

Closed
#6,570 3 comments 0 reactions 0 assignees View on GitHub

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

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

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});
}
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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from lidge-jun/opencodex

All issues in lidge-jun/opencodex

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.