{"record":{"id":"b8b8f54a7a87c0d4","repo":"jlcodes99/cockpit-tools","slug":"403-forbidden","errorCode":null,"errorMessage":"403 Forbidden","messagePattern":"403 Forbidden","errorType":"exception","errorClass":null,"httpStatus":403,"severity":"error","filePath":"src/stores/useAccountStore.ts","lineNumber":416,"sourceCode":"                    }\n                }\n                return {\n                    currentAccount:\n                        nextByTarget[target]?.id === accountId\n                            ? updatedAccount\n                            : state.currentAccount?.id === accountId\n                              ? updatedAccount\n                              : state.currentAccount,\n                    currentAccountsByTarget: nextByTarget,\n                };\n            });\n\n            // 如果后端返回了配额错误信息，需要抛出异常让 UI 捕获并显示为失败（红叉）\n            if (updatedAccount.quota_error) {\n                throw new Error(updatedAccount.quota_error.message);\n            }\n            if (updatedAccount.quota?.is_forbidden) {\n                throw new Error(\"403 Forbidden\");\n            }\n        } catch (e) {\n            // Token 级别失败（如 invalid_grant 会改变 disabled 状态）：全量刷新确保数据正确\n            // 如果是我们自己 throw 的配额错误，因为状态已经局部更新，不再需要全量刷新\n            const isQuotaError = e instanceof Error && (\n                get().accounts.find(a => a.id === accountId)?.quota_error?.message === e.message ||\n                e.message === \"403 Forbidden\"\n            );\n            if (!isQuotaError) {\n                await get().fetchAccounts();\n            }\n            throw e;\n        } finally {\n            await get().fetchCurrentAccount(target);\n        }\n    },\n\n    refreshAllQuotas: async (trigger) => {","sourceCodeStart":398,"sourceCodeEnd":434,"githubUrl":"https://github.com/jlcodes99/cockpit-tools/blob/1ed8b77992d62ca81fabf744deb0839ad361d5bf/src/stores/useAccountStore.ts#L398-L434","documentation":"The account store's quota refresh flow detects that the backend marked the account's quota as forbidden (quota.is_forbidden) and throws '403 Forbidden' so the UI can render the refresh as a failure (red cross). It is a deliberate UI-signal exception, not an HTTP-level error, though it usually mirrors a real upstream 403 from the provider.","triggerScenarios":"Refreshing an account's quota when the backend's updated account payload has quota.is_forbidden === true (e.g. the upstream API rejected the token with 403), or when a quota_error message is present (that branch throws first).","commonSituations":"Account token revoked or banned upstream; account disabled by the provider; IP/region blocked by the provider; stale credentials after password change.","solutions":["Catch this error in the refresh caller and surface the account as failed/forbidden in the UI","Re-authenticate the account (update its token / re-login) so upstream 403 stops occurring","Check the account's quota_error field for the more specific upstream reason","Delete and re-import the account if credentials are unrecoverable"],"exampleFix":"// before\nawait refreshQuota(accountId); // throws '403 Forbidden' uncaught\n// after\ntry {\n  await refreshQuota(accountId);\n} catch (e) {\n  markAccountFailed(accountId, (e as Error).message); // show red cross in UI\n}","handlingStrategy":"try-catch","validationCode":"const acct = get().accounts.find(a => a.id === accountId);\nconst forbidden = acct?.quota?.is_forbidden === true || !!acct?.quota_error;","typeGuard":"function isQuotaForbidden(a?: { quota?: { is_forbidden?: boolean } | null }) {\n  return a?.quota?.is_forbidden === true;\n}","tryCatchPattern":"try {\n  await refreshQuota(accountId);\n} catch (e) {\n  if ((e as Error).message === '403 Forbidden') {\n    // mark account forbidden in UI\n  }\n}","preventionTips":["Check quota.is_forbidden before triggering refreshes","Surface quota_error messages proactively in the UI","Re-authenticate accounts whose tokens show repeated upstream failures"],"tags":["http","forbidden","account-status","quota"],"backgroundTag":"http-403-forbidden","analyzedSha":"1ed8b77992d62ca81fabf744deb0839ad361d5bf","analyzedAt":"2026-09-05T09:51:41.178Z","contentChangedAt":"2026-09-05T09:51:41.178Z","schemaVersion":2},"datasetVersion":"2026-09-12T12:17:11.808Z"}