{"record":{"id":"3fda3dc3ffe98f0d","repo":"lbjlaq/Antigravity-Manager","slug":"accounts-add-oauth-error-no-url","errorCode":null,"errorMessage":"accounts.add.oauth.error_no_url","messagePattern":"accounts\\.add\\.oauth\\.error_no_url","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/components/accounts/AddAccountDialog.tsx","lineNumber":287,"sourceCode":"            // 不自动关闭,让用户看到结果\n        } else {\n            // 全部失败\n            setStatus('error');\n            setMessage(t('accounts.add.token.batch_fail'));\n        }\n    };\n\n    const handleOAuthWeb = async () => {\n        try {\n            setStatus('loading');\n            setMessage(t('accounts.add.oauth.btn_start') + '...');\n\n            // 1. 获取 URL (指向 /auth/callback)\n            const res = await invoke<any>('prepare_oauth_url');\n            const url = typeof res === 'string' ? res : res.url;\n\n            if (!url) {\n                throw new Error(t('accounts.add.oauth.error_no_url', 'OAuth URLを取得できませんでした'));\n            }\n\n            setOauthUrl(url); // 确保链接在 UI 中可见，方便用户手动复制\n\n            // 2. 打开新标签页 (响应用户反馈：Web 端直接使用新标签体验更好)\n            const popup = window.open(url, '_blank');\n\n            if (!popup) {\n                setStatus('error');\n                setMessage(t('accounts.add.oauth.popup_blocked', 'ポップアップがブロックされました'));\n                return;\n            }\n\n            // 3. 监听消息\n            const handleMessage = async (event: MessageEvent) => {\n                // 安全检查: 如果定义了 ORIGIN 校验更好，这里暂时检查 data type\n                if (event.data?.type === 'oauth-success') {\n                    popup.close();","sourceCodeStart":269,"sourceCodeEnd":305,"githubUrl":"https://github.com/lbjlaq/Antigravity-Manager/blob/a2e3c454237d6d6ef423dfe20505b1ac62803c7c/src/components/accounts/AddAccountDialog.tsx#L269-L305","documentation":"handleOAuthWeb() in src/components/accounts/AddAccountDialog.tsx:287 throws this after invoke('prepare_oauth_url') resolves but yields no usable URL — neither a non-empty string nor a res.url property. In web mode that command maps to GET /api/auth/url (request.ts:114); in Tauri it is a native command. The guard exists because window.open(undefined) would silently open about:blank, so the dialog fails fast and keeps the manual-copy UI honest.","triggerScenarios":"Backend responds 200 with an empty string, null, or an object without url (e.g. { ok: false } or an error envelope that request.ts passed through); OAuth client credentials (client_id/secret) not configured server-side so the authorize URL cannot be built; a proxy returning an HTML error page that request.ts:261-266 coerces to a truthy string but the code path returned a parsed object instead.","commonSituations":"First-time setup where the OAuth provider (Z.ai/Google) client is not configured before the user clicks 'Start OAuth'; web deployment where GET /api/auth/url is misrouted or returns an empty body with 204 (request.ts:252-254 returns null → url undefined); backend version drift changing the response shape from a bare URL string to { url } or vice versa while frontend only tolerates two of the shapes.","solutions":["Hit GET /api/auth/url (or invoke prepare_oauth_url in the desktop shell) directly and inspect the payload; configure the missing OAuth client credentials if it returns empty.","Tighten the response handling: accept string | { url }, validate it starts with https?://, and log the raw response when invalid so the real shape is visible.","If a 204/empty body is legitimate, have the backend return a structured error (e.g. { error: 'oauth_not_configured' }) and surface that instead of the generic no-url error.","Check list_oauth_clients / get_active_oauth_client output to confirm an OAuth client is actually configured before opening the dialog."],"exampleFix":"// before\nconst res = await invoke<any>('prepare_oauth_url');\nconst url = typeof res === 'string' ? res : res.url;\nif (!url) {\n    throw new Error(t('accounts.add.oauth.error_no_url', 'OAuth URLを取得できませんでした'));\n}\n\n// after\nconst res = await invoke<string | { url?: string } | null>('prepare_oauth_url');\nconst url = typeof res === 'string' ? res : res?.url;\nif (typeof url !== 'string' || !/^https?:\\/\\//.test(url)) {\n    console.error('prepare_oauth_url returned unexpected payload:', res);\n    throw new Error(t('accounts.add.oauth.error_no_url', 'OAuth URLを取得できませんでした'));\n}","handlingStrategy":"type-guard","validationCode":"// Validate the response shape before using it\nconst res = await invoke<unknown>('prepare_oauth_url');\nconst candidate = typeof res === 'string' ? res : (res as { url?: unknown })?.url;\nif (typeof candidate !== 'string' || !/^https?:\\/\\//.test(candidate)) {\n  throw new Error('OAuth provider not configured — empty authorize URL');\n}","typeGuard":"type OAuthUrlResponse = string | { url: string };\nconst isUsableOAuthUrl = (r: unknown): r is OAuthUrlResponse =>\n  (typeof r === 'string' && /^https?:\\/\\//.test(r)) ||\n  (typeof r === 'object' && r !== null &&\n    typeof (r as { url?: unknown }).url === 'string' &&\n    /^https?:\\/\\//.test((r as { url: string }).url));","tryCatchPattern":"try {\n  const res = await invoke('prepare_oauth_url');\n  if (!isUsableOAuthUrl(res)) {\n    setOauthUrl(null);\n    setStatus('error');\n    setMessage(t('accounts.add.oauth.error_no_url'));\n    return; // keep dialog open so the user can retry after fixing config\n  }\n  window.open(extractUrl(res), '_blank');\n} catch (e) {\n  setStatus('error');\n  setMessage(e instanceof Error ? e.message : String(e));\n}","preventionTips":["Configure and verify the OAuth client (via list_oauth_clients / get_active_oauth_client) before rendering the OAuth button.","Make the backend return a structured error for 'not configured' instead of an empty 200 so the frontend can show a precise message.","Validate the URL with a regex (^https?://) — truthiness accepts whitespace or malformed strings.","Always log the raw prepare_oauth_url payload when validation fails so response-shape drift is diagnosed in one step."],"tags":["oauth","tauri","web-mode","response-validation","popup"],"backgroundTag":"oauth-authorize-url-missing","analyzedSha":"a2e3c454237d6d6ef423dfe20505b1ac62803c7c","analyzedAt":"2026-08-16T19:44:46.389Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}