{"record":{"id":"88f7d92410d323d2","repo":"sipeed/picoclaw","slug":"invalid-browser-login-response","errorCode":null,"errorMessage":"Invalid browser login response","messagePattern":"Invalid browser login response","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"web/frontend/src/hooks/use-credentials-page.ts","lineNumber":205,"sourceCode":"\n      const authTab = window.open(\"\", \"_blank\")\n      if (!authTab) {\n        if (!isActionTokenCurrent(actionToken)) {\n          return\n        }\n        setActiveAction(\"\")\n        setError(t(\"credentials.errors.popupBlocked\"))\n        return\n      }\n\n      try {\n        const resp = await loginOAuth({ provider, method: \"browser\" })\n        if (!isActionTokenCurrent(actionToken)) {\n          authTab.close()\n          return\n        }\n        if (!resp.auth_url || !resp.flow_id) {\n          throw new Error(t(\"credentials.errors.invalidBrowserResponse\"))\n        }\n\n        authTab.location.href = resp.auth_url\n\n        setActiveFlow({\n          flow_id: resp.flow_id,\n          provider,\n          method: \"browser\",\n          status: \"pending\",\n          expires_at: resp.expires_at,\n        })\n        setWatchFlowID(resp.flow_id)\n        setWatchMode(\"status\")\n        setPollIntervalMs(2000)\n      } catch (err) {\n        if (!isActionTokenCurrent(actionToken)) {\n          authTab.close()\n          return","sourceCodeStart":187,"sourceCodeEnd":223,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/web/frontend/src/hooks/use-credentials-page.ts#L187-L223","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":"function isBrowserLoginResponse(\n  resp: unknown,\n): resp is { auth_url: string; flow_id: string; expires_at?: string } {\n  if (!resp || typeof resp !== \"object\") return false\n  const r = resp as Record<string, unknown>\n  return typeof r.auth_url === \"string\" && r.auth_url.length > 0 && typeof r.flow_id === \"string\" && r.flow_id.length > 0\n}","tryCatchPattern":"try {\n  const resp = await loginOAuth({ provider, method: \"browser\" })\n  if (!isBrowserLoginResponse(resp)) {\n    throw new Error(t(\"credentials.errors.invalidBrowserResponse\"))\n  }\n  authTab.location.href = resp.auth_url\n} catch (err) {\n  authTab.close()\n  setError(err instanceof Error ? err.message : t(\"credentials.errors.loginFailed\"))\n}","preventionTips":["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"],"tags":["oauth","api-contract","frontend","backend","authentication"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}