coleam00/Archon · warning · OAuthCallbackPortBusyError

err.message (OAuthCallbackPortBusyError)

Error message

err.message (OAuthCallbackPortBusyError)

What it means

When starting a provider OAuth subscription login, the local callback HTTP listener could not bind its port because another process (often a previous incomplete OAuth attempt) already holds it. The server recognizes OAuthCallbackPortBusyError as a retryable condition and surfaces the underlying err.message verbatim with HTTP 503, plus a warn-level log under auth.provider_oauth_start_port_busy, instead of an opaque 500.

Source

Thrown at packages/server/src/routes/api.ts:2060

      return apiError(
        c,
        400,
        `Provider '${provider}' does not support subscription login. ` +
          `Subscription providers: ${[...SUBSCRIPTION_PROVIDERS].sort().join(', ')}.`
      );
    }
    try {
      const start = await startOAuth(web.userId, provider);
      return c.json(start);
    } catch (err) {
      // A leaked callback port from a previous attempt is an expected,
      // retryable condition — log it at warn under its own event (an
      // error-level `…_failed` would pollute error dashboards on multi-user
      // installs) and surface the actionable message as a 503 instead of an
      // opaque 500 (#1963).
      if (err instanceof OAuthCallbackPortBusyError) {
        getLog().warn({ userId: web.userId, provider }, 'auth.provider_oauth_start_port_busy');
        return apiError(c, 503, err.message);
      }
      getLog().error(
        { err: err as Error, userId: web.userId, provider },
        'auth.provider_oauth_start_failed'
      );
      return apiError(c, 500, 'Failed to start subscription login');
    }
  });

  registerOpenApiRoute(providerOAuthPollRoute, async c => {
    const web = await requireWebUser(c, 'Web authentication required to connect a subscription');
    if ('error' in web) return web.error;
    if (!isPerUserProviderKeysEnabled()) {
      return apiError(c, 404, 'Per-user provider keys are not enabled on this install');
    }
    // The `:provider` path segment only keeps the OAuth routes under one prefix
    // (so they're exempt from the Better Auth catch-all); poll itself keys off
    // sessionId + userId.

View on GitHub (pinned to 0773b97458)

Solutions

  1. Wait a few seconds and retry the OAuth start call — the server explicitly treats this as retryable (503).
  2. Find and stop the process holding the callback port (lsof -i :PORT / ss -ltnp) from the earlier OAuth attempt.
  3. Ensure each OAuth start completes or is cancelled before starting another; avoid firing parallel provider logins from scripts.
  4. If this recurs on every attempt, restart the server to release leaked listeners and investigate callback-server cleanup.

Example fix

// before: fire-and-forget parallel logins
await Promise.all([startOAuth('claude'), startOAuth('codex')]);
// after: sequential with retry on 503
for (const provider of ['claude', 'codex']) {
  await startOAuthWithRetry(provider); // retries once after port frees
}
Defensive patterns

Strategy: retry

Validate before calling

// client-side precheck: is the callback port free?
const net = require('node:net');
async function portFree(port) {
  return new Promise(res => {
    const s = net.createServer();
    s.once('error', () => res(false));
    s.once('listening', () => s.close(() => res(true)));
    s.listen(port);
  });
}
if (!(await portFree(cfg.callbackPort))) throw new Error('Callback port busy; retry later');

Type guard

function isPortBusy503(res: { status: number; error?: string }): boolean {
  return res.status === 503; // route maps OAuthCallbackPortBusyError to 503
}

Try / catch

try {
  await startProviderOAuth(provider);
} catch (err) {
  if (isPortBusy503(err)) {
    await sleep(2000);
    return startProviderOAuth(provider); // single retry, condition is transient
  }
  throw err;
}

Prevention

When it happens

Trigger: POSTing the provider OAuth start route (subscription login) while another OAuth flow on the same install already occupies the callback port; two users or two browser tabs starting provider login concurrently on a multi-user install; a stale crashed process still holding the callback listener port.

Common situations: A previous OAuth attempt was abandoned mid-flow so its callback server never shut down; parallel automation/scripts logging in for several providers at once; a dev server restarted without the old process fully exiting.

Related errors


AI-assisted analysis of coleam00/Archon@0773b97458 (2026-09-01). Data as JSON: /api/errors/33416dcc2982fe22. Report an issue: GitHub.