{"record":{"id":"33416dcc2982fe22","repo":"coleam00/Archon","slug":"err-message-oauthcallbackportbusyerror","errorCode":null,"errorMessage":"err.message (OAuthCallbackPortBusyError)","messagePattern":"err\\.message \\(OAuthCallbackPortBusyError\\)","errorType":"http","errorClass":"OAuthCallbackPortBusyError","httpStatus":503,"severity":"warning","filePath":"packages/server/src/routes/api.ts","lineNumber":2060,"sourceCode":"      return apiError(\n        c,\n        400,\n        `Provider '${provider}' does not support subscription login. ` +\n          `Subscription providers: ${[...SUBSCRIPTION_PROVIDERS].sort().join(', ')}.`\n      );\n    }\n    try {\n      const start = await startOAuth(web.userId, provider);\n      return c.json(start);\n    } catch (err) {\n      // A leaked callback port from a previous attempt is an expected,\n      // retryable condition — log it at warn under its own event (an\n      // error-level `…_failed` would pollute error dashboards on multi-user\n      // installs) and surface the actionable message as a 503 instead of an\n      // opaque 500 (#1963).\n      if (err instanceof OAuthCallbackPortBusyError) {\n        getLog().warn({ userId: web.userId, provider }, 'auth.provider_oauth_start_port_busy');\n        return apiError(c, 503, err.message);\n      }\n      getLog().error(\n        { err: err as Error, userId: web.userId, provider },\n        'auth.provider_oauth_start_failed'\n      );\n      return apiError(c, 500, 'Failed to start subscription login');\n    }\n  });\n\n  registerOpenApiRoute(providerOAuthPollRoute, async c => {\n    const web = await requireWebUser(c, 'Web authentication required to connect a subscription');\n    if ('error' in web) return web.error;\n    if (!isPerUserProviderKeysEnabled()) {\n      return apiError(c, 404, 'Per-user provider keys are not enabled on this install');\n    }\n    // The `:provider` path segment only keeps the OAuth routes under one prefix\n    // (so they're exempt from the Better Auth catch-all); poll itself keys off\n    // sessionId + userId.","sourceCodeStart":2042,"sourceCodeEnd":2078,"githubUrl":"https://github.com/coleam00/Archon/blob/0773b9745896ef0612e709c80845a0f7db315b19/packages/server/src/routes/api.ts#L2042-L2078","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: fire-and-forget parallel logins\nawait Promise.all([startOAuth('claude'), startOAuth('codex')]);\n// after: sequential with retry on 503\nfor (const provider of ['claude', 'codex']) {\n  await startOAuthWithRetry(provider); // retries once after port frees\n}","handlingStrategy":"retry","validationCode":"// client-side precheck: is the callback port free?\nconst net = require('node:net');\nasync function portFree(port) {\n  return new Promise(res => {\n    const s = net.createServer();\n    s.once('error', () => res(false));\n    s.once('listening', () => s.close(() => res(true)));\n    s.listen(port);\n  });\n}\nif (!(await portFree(cfg.callbackPort))) throw new Error('Callback port busy; retry later');","typeGuard":"function isPortBusy503(res: { status: number; error?: string }): boolean {\n  return res.status === 503; // route maps OAuthCallbackPortBusyError to 503\n}","tryCatchPattern":"try {\n  await startProviderOAuth(provider);\n} catch (err) {\n  if (isPortBusy503(err)) {\n    await sleep(2000);\n    return startProviderOAuth(provider); // single retry, condition is transient\n  }\n  throw err;\n}","preventionTips":["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."],"tags":["oauth","port-conflict","http-503","retryable"],"backgroundTag":"port-already-in-use","analyzedSha":"0773b9745896ef0612e709c80845a0f7db315b19","analyzedAt":"2026-09-01T02:28:07.064Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}