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
- Wait a few seconds and retry the OAuth start call — the server explicitly treats this as retryable (503).
- Find and stop the process holding the callback port (lsof -i :PORT / ss -ltnp) from the earlier OAuth attempt.
- Ensure each OAuth start completes or is cancelled before starting another; avoid firing parallel provider logins from scripts.
- 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
- Serialize OAuth logins; never start two provider flows concurrently on one install.
- Always complete or cancel an in-flight OAuth flow before starting another.
- Monitor for stale processes holding the callback port after crashes and restart the server if found.
- Treat 503 on this route as retryable, and cap retries to avoid tight loops.
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
- A previous '${provider}' login attempt is still holding the
- Provider '${provider}' does not support subscription login.
- Vendor '${vendor}' (Pi backend) has no env-based OAuth deliv
- OAuth state mismatch.
- Missing authorization code.
AI-assisted analysis of coleam00/Archon@0773b97458 (2026-09-01).
Data as JSON: /api/errors/33416dcc2982fe22.
Report an issue: GitHub.