Skip to content

fix: surface OAuth error_description on token-exchange 4xx#469

Draft
posthog[bot] wants to merge 1 commit into
mainfrom
posthog-code/fix-oauth-400-error-surfacing
Draft

fix: surface OAuth error_description on token-exchange 4xx#469
posthog[bot] wants to merge 1 commit into
mainfrom
posthog-code/fix-oauth-400-error-surfacing

Conversation

@posthog
Copy link
Copy Markdown

@posthog posthog Bot commented May 22, 2026

Problem

A new error tracking issue (fingerprint b8d1191658…) shows users hitting AxiosError: Request failed with status code 400 from the PostHog /oauth/token endpoint during npx @posthog/wizard. The opaque message gives them no path to recovery on the very first step of the wizard.

exchangeCodeForToken (src/utils/oauth.ts) posts the PKCE code exchange and only consumes response.data on success — it never reads the OAuth-standard error / error_description from response.data on a 4xx. The catch in performOAuthFlow then logs error.message, which for axios 400s is just the generic status string.

Likely server-side triggers: replayed/expired authorization code, PKCE code_verifier mismatch, or a redirect_uri mismatch (more plausible now after the backup-ports refactor in #400 picks callbackUrl dynamically per port). The recent CI-mode 401 fix (#432) added similar handling but only for 401s from the LLM gateway, not 400s from /oauth/token.

Changes

  • exchangeCodeForToken now wraps the axios call in try/catch. On any 4xx, it parses response.data with a small OAuthErrorResponseSchema (zod), extracts error + error_description, and rethrows an Error whose message reads e.g. invalid_grant: authorization code expired — please re-run the wizard to start a fresh login. The existing catch in performOAuthFlow then renders that via getUI().log.error(...) without further changes.
  • On failure, the resolved redirect_uri, status, and parsed OAuth fields are written via logToFile so future 400s can be traced to a specific port iteration.
  • The per-port [oauth] attempting callback server on port N log line now also includes the chosen redirect_uri, making port-vs-callback-URL drift visible in the log.

Mirrors the surfacing pattern from #432 but narrowly scoped to the OAuth token endpoint — AuthErrorScreen is left alone since its docstring explicitly scopes it to LLM Gateway 401s.

Test plan

  • pnpm build — succeeds, smoke test passes
  • pnpm testprovision-cli.test.ts flakes in the full parallel run (pre-existing, passes in isolation on main); all other 39 suites pass (646/647)
  • pnpm lint — 0 errors (pre-existing warnings unchanged)
  • Manual repro of a 400 from /oauth/token is hard without server cooperation; covered by code review + the unchanged success path which existing tests exercise

Created with PostHog Code

The token endpoint at `${POSTHOG_OAUTH_URL}/oauth/token` returns standard
OAuth `error` / `error_description` fields on a 4xx, but
`exchangeCodeForToken` only ever consumed `response.data` on success. A 400
(e.g. replayed or expired code, PKCE mismatch, redirect_uri mismatch after
the multi-port refactor in #400) surfaced as the opaque "Request failed with
status code 400" on the very first step of the wizard, leaving users without
a path to recovery.

This catches axios errors in `exchangeCodeForToken`, parses the OAuth error
payload, and rethrows with a clear message that gets rendered by the
existing `performOAuthFlow` catch (e.g. "invalid_grant: authorization code
expired - please re-run the wizard"). Also logs the resolved `redirect_uri`
+ port + server response via `logToFile` on the chosen port and on failure,
so future 400s can be traced to a specific port iteration.

Mirrors the surfacing pattern from #432.

Generated-By: PostHog Code
Task-Id: 311205bc-0f27-42f4-a2e6-0112a4d64855
@github-actions
Copy link
Copy Markdown

🧙 Wizard CI

Run the Wizard CI and test your changes against wizard-workbench example apps by replying with a GitHub comment using one of the following commands:

Test all apps:

  • /wizard-ci all

Test all apps in a directory:

  • /wizard-ci basic-integration
  • /wizard-ci misc
  • /wizard-ci revenue

Test an individual app:

  • /wizard-ci basic-integration/android
  • /wizard-ci basic-integration/angular
  • /wizard-ci basic-integration/astro
Show more apps
  • /wizard-ci basic-integration/django
  • /wizard-ci basic-integration/fastapi
  • /wizard-ci basic-integration/flask
  • /wizard-ci basic-integration/javascript-node
  • /wizard-ci basic-integration/javascript-web
  • /wizard-ci basic-integration/laravel
  • /wizard-ci basic-integration/next-js
  • /wizard-ci basic-integration/nuxt
  • /wizard-ci basic-integration/python
  • /wizard-ci basic-integration/rails
  • /wizard-ci basic-integration/react-native
  • /wizard-ci basic-integration/react-router
  • /wizard-ci basic-integration/sveltekit
  • /wizard-ci basic-integration/swift
  • /wizard-ci basic-integration/tanstack-router
  • /wizard-ci basic-integration/tanstack-start
  • /wizard-ci basic-integration/vue
  • /wizard-ci misc/quack-quack
  • /wizard-ci revenue/stripe

Results will be posted here when complete.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants