{"record":{"id":"1b9117927506c25b","repo":"decolua/9router","slug":"url-searchparams-get-error-description-error","errorCode":null,"errorMessage":"url.searchParams.get(\"error_description\") || errorParam","messagePattern":"url\\.searchParams\\.get\\(\"error_description\"\\) \\|\\| errorParam","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/lib/oauth/utils/server.js","lineNumber":219,"sourceCode":"    const server = http.createServer(async (req, res) => {\n      const url = new URL(req.url, \"http://localhost\");\n\n      if (url.pathname !== \"/callback\" && url.pathname !== \"/auth/callback\") {\n        res.writeHead(404);\n        res.end(\"Not found\");\n        return;\n      }\n\n      const code = url.searchParams.get(\"code\");\n      const state = url.searchParams.get(\"state\");\n      const errorParam = url.searchParams.get(\"error\");\n      const session = state ? pendingExchanges.get(state) : null;\n\n      // Mode A: server-side exchange (session registered)\n      if (session) {\n        try {\n          if (errorParam) {\n            throw new Error(url.searchParams.get(\"error_description\") || errorParam);\n          }\n          if (!code) throw new Error(\"No authorization code received\");\n\n          // Lazy import to avoid circular deps\n          const { exchangeTokens } = await import(\"../providers.js\");\n          const { createProviderConnection } = await import(\"@/models\");\n\n          const tokenData = await exchangeTokens(\n            \"codex\",\n            code,\n            session.redirectUri,\n            session.codeVerifier,\n            state\n          );\n          const connection = await createProviderConnection({\n            provider: \"codex\",\n            authType: \"oauth\",\n            ...tokenData,","sourceCodeStart":201,"sourceCodeEnd":237,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/lib/oauth/utils/server.js#L201-L237","documentation":"In the Codex OAuth callback proxy (Mode A, where a pending exchange session is registered for the state), the provider redirected to /callback with an `error` query parameter. The handler throws the provider's error_description (or the raw error code) so it can be recorded on the session and rendered as a failure page. This is the spec-defined authorization-failure path of the code flow.","triggerScenarios":"startCodexProxy's HTTP handler receives /callback or /auth/callback with a `state` matching a pendingExchanges session and `error` present in the query string (e.g. access_denied, invalid_request).","commonSituations":"User denied consent on the Codex/ChatGPT authorization page; the authorize request was malformed (bad client_id, mismatched redirect_uri on port 1455); session/provider config changed between authorize and callback; upstream OpenAI-side outage yielding server_error; the browser aborted the flow mid-way and the provider reported it back.","solutions":["Read the session.error (shown on the rendered failure page) and address the specific provider error — access_denied means re-run the flow and approve","Re-initiate the Codex OAuth connect flow to get a fresh authorize URL and retry","Verify the redirect URI registered for the provider matches the proxy on 127.0.0.1:1455","Check provider status if the message indicates a server-side error, then retry later","Ensure the state passed to register the pending session is the same one used in the authorize URL"],"exampleFix":"// before: raw thrown message only\nif (errorParam) {\n  throw new Error(url.searchParams.get(\"error_description\") || errorParam);\n}\n// after (caller side): map denial to a friendly message\ntry {\n  await codexOAuthFlow();\n} catch (e) {\n  if (e.message === 'access_denied') {\n    show('Authorization denied — please approve access to continue.');\n  } else {\n    throw e;\n  }\n}","handlingStrategy":"try-catch","validationCode":"// Inspect the callback before/at the proxy.\nconst params = new URL(req.url, 'http://localhost').searchParams;\nif (params.get('error')) {\n  const msg = params.get('error_description') || params.get('error');\n  markSessionFailed(params.get('state'), msg);\n}","typeGuard":"function hasOAuthError(searchParams) {\n  return searchParams instanceof URLSearchParams && searchParams.get('error') !== null;\n}","tryCatchPattern":"const status = getCodexSessionStatus(state);\nif (status && status.status === 'error') {\n  if (/access_denied/.test(status.error || '')) {\n    // user denied consent — prompt a retry\n  } else {\n    throw new Error(status.error);\n  }\n}","preventionTips":["Approve the consent screen instead of denying","Confirm the registered redirect URI matches the 1455 proxy callback","Register the pending exchange session with the same state used in the authorize URL","Retry on provider-side server_error after checking provider status","Test the flow end-to-end after any provider/client config change"],"tags":["oauth","codex","authorization-denied","callback-proxy"],"backgroundTag":"oauth-authorization-error","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}