sipeed/picoclaw · error

Invalid browser login response

Error message

Invalid browser login response

What it means

Thrown in startBrowserOAuth (web/frontend/src/hooks/use-credentials-page.ts:205, message from i18n key credentials.errors.invalidBrowserResponse) after a successful POST /api/oauth/login with method "browser" whose response body lacks auth_url or flow_id. This is a backend-contract violation: the launcher backend replied 2xx but did not return the fields the browser flow requires. The action tab opened with window.open("") is closed in the catch block and the message is shown inline.

Source

Thrown at web/frontend/src/hooks/use-credentials-page.ts:205

      const authTab = window.open("", "_blank")
      if (!authTab) {
        if (!isActionTokenCurrent(actionToken)) {
          return
        }
        setActiveAction("")
        setError(t("credentials.errors.popupBlocked"))
        return
      }

      try {
        const resp = await loginOAuth({ provider, method: "browser" })
        if (!isActionTokenCurrent(actionToken)) {
          authTab.close()
          return
        }
        if (!resp.auth_url || !resp.flow_id) {
          throw new Error(t("credentials.errors.invalidBrowserResponse"))
        }

        authTab.location.href = resp.auth_url

        setActiveFlow({
          flow_id: resp.flow_id,
          provider,
          method: "browser",
          status: "pending",
          expires_at: resp.expires_at,
        })
        setWatchFlowID(resp.flow_id)
        setWatchMode("status")
        setPollIntervalMs(2000)
      } catch (err) {
        if (!isActionTokenCurrent(actionToken)) {
          authTab.close()
          return

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Verify the provider is fully configured server-side (client id/secret present in launcher config) before offering browser login
  2. Confirm GET /api/oauth/providers lists the provider and the backend version supports browser flow, then retry
  3. Bypass the proxy or fix its routing so POST /api/oauth/login reaches the launcher API untouched
  4. If you control the backend, ensure the browser-flow response always includes auth_url and flow_id on success instead of an empty 200
Defensive patterns

Strategy: try-catch

Type guard

function isBrowserLoginResponse(
  resp: unknown,
): resp is { auth_url: string; flow_id: string; expires_at?: string } {
  if (!resp || typeof resp !== "object") return false
  const r = resp as Record<string, unknown>
  return typeof r.auth_url === "string" && r.auth_url.length > 0 && typeof r.flow_id === "string" && r.flow_id.length > 0
}

Try / catch

try {
  const resp = await loginOAuth({ provider, method: "browser" })
  if (!isBrowserLoginResponse(resp)) {
    throw new Error(t("credentials.errors.invalidBrowserResponse"))
  }
  authTab.location.href = resp.auth_url
} catch (err) {
  authTab.close()
  setError(err instanceof Error ? err.message : t("credentials.errors.loginFailed"))
}

Prevention

When it happens

Trigger: Clicking a provider's browser-login button when the backend's OAuth client for that provider is not configured (no client id/secret), when the backend build predates browser flow support, or when a proxy/gateway strips fields from the JSON response. The check is resp.auth_url || resp.flow_id falsy.

Common situations: Self-hosted launcher without provider credentials set in its config; backend/frontend version skew after a partial upgrade; a reverse proxy returning its own 200 HTML page instead of the API JSON; provider removed server-side but still rendered in the UI list.

Related errors


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/88f7d92410d323d2. Report an issue: GitHub.