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()
returnView on GitHub (pinned to 49183d7e8d)
Solutions
- Verify the provider is fully configured server-side (client id/secret present in launcher config) before offering browser login
- Confirm GET /api/oauth/providers lists the provider and the backend version supports browser flow, then retry
- Bypass the proxy or fix its routing so POST /api/oauth/login reaches the launcher API untouched
- 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
- Gate the browser-login button on GET /api/oauth/providers advertising the provider with browser support
- Keep backend and frontend versions in lockstep so the login response contract never skews
- In e2e tests, assert the /api/oauth/login browser response contains auth_url and flow_id
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
- Invalid device code response
- Request failed with status ${res.status}
- ${label} must be a JSON object.
- ${label}.${key} must be a string.
- Failed to load config
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/88f7d92410d323d2.
Report an issue: GitHub.