{"record":{"id":"3007bb31e3f9005f","repo":"actualbudget/actual","slug":"response-reason-response-error-code","errorCode":null,"errorMessage":"response.reason || response.error_code","messagePattern":"response\\.reason \\|\\| response\\.error_code","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/desktop-client/src/components/banksync/useBuiltInBankSyncProviders.ts","lineNumber":99,"sourceCode":"  isFileOwner: boolean,\n): 'general' | 'file-owner' | null {\n  if (syncServerStatus !== 'online') {\n    return null;\n  }\n\n  if (isAdmin) {\n    return null;\n  }\n\n  return isFileOwner ? 'file-owner' : 'general';\n}\n\nasync function ensureSuccessResponse(\n  response: SecretSetResponse,\n  fallbackMessage: string,\n) {\n  if (response?.error_code) {\n    throw new Error(response.reason || response.error_code);\n  }\n\n  if (response?.error) {\n    throw new Error(response.reason || response.error || fallbackMessage);\n  }\n}\n\nexport function useBuiltInBankSyncProviders({\n  upgradingAccountId,\n}: UseBuiltInBankSyncProvidersOptions = {}) {\n  const { t } = useTranslation();\n  const dispatch = useDispatch();\n  const syncServerStatus = useSyncServerStatus();\n  const { cloudFileId, isAdmin, isFileOwner } = useCurrentAccess();\n  const canConfigureProviders = isAdmin;\n\n  const [isGoCardlessSetupComplete, setIsGoCardlessSetupComplete] = useState<\n    boolean | null","sourceCodeStart":81,"sourceCodeEnd":117,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/desktop-client/src/components/banksync/useBuiltInBankSyncProviders.ts#L81-L117","documentation":"`ensureSuccessResponse` validates the sync-server's response after setting bank-sync provider secrets (GoCardless, SimpleFin, PluggyAI, EnableBanking, Akahu). If the response carries an `error_code`, it throws with `response.reason || response.error_code` — preferring the human-readable reason but falling back to the raw error code string. This surfaces provider-configuration rejections from the sync server (e.g. missing/invalid API keys) to the UI.","triggerScenarios":"Any reset/save handler (`onGoCardlessReset`, `onSimpleFinReset`, `onPluggyAiReset`, `onEnableBankingReset`, `onAkahuReset`) submits credentials via the sync-server and the server responds with `error_code` set — e.g. secret storage failed, the provider rejected the credentials, or the request was malformed/unauthorized.","commonSituations":"Entering an expired or wrong GoCardless secret ID/token; PluggyAI clientId/secret rejected; EnableBanking or Akahu credentials invalid; sync-server cannot persist secrets (storage/permission problem); running without the required env vars on a self-hosted server.","solutions":["Read the thrown message — it is the `reason` or `error_code` from the sync server; map known codes (e.g. invalid-credentials) to the offending field.","Re-enter the provider credentials carefully (no trailing spaces, correct environment — sandbox vs production keys).","Check sync-server logs and its provider env configuration (e.g. ACTUAL_GOCARDLESS_* variables) for the underlying cause.","Restart/upgrade the sync-server if the code indicates an internal storage failure, then retry."],"exampleFix":"// before\nconst res = await send('secret-set', { name: 'gocardless_secret_id', value: value.trim() });\nawait ensureSuccessResponse(res, 'Failed to set GoCardless secret');\n// after\nconst value = input.trim();\nif (!value) { setError('Secret ID is required'); return; }\nconst res = await send('secret-set', { name: 'gocardless_secret_id', value });\ntry {\n  await ensureSuccessResponse(res, 'Failed to set GoCardless secret');\n} catch (e) {\n  setError(e.message); // surfaces e.g. 'invalid-credentials' from server\n}","handlingStrategy":"try-catch","validationCode":"// Before submitting credentials\nconst trimmed = value.trim();\nif (!trimmed) { setError('Value is required'); return; }\nif (provider === 'gocardless' && !/^test_|^live_/.test(trimmed)) {\n  setError('GoCardless secret IDs start with test_ or live_');\n  return;\n}","typeGuard":"function isSecretSetError(\n  res: SecretSetResponse,\n): res is SecretSetResponse & { error_code: string } {\n  return typeof res?.error_code === 'string' && res.error_code.length > 0;\n}","tryCatchPattern":"try {\n  await ensureSuccessResponse(res, 'Failed to set secret');\n} catch (e) {\n  const msg = e instanceof Error ? e.message : String(e);\n  if (/credential|invalid|unauthorized/i.test(msg)) {\n    setError('Credentials rejected — verify the keys for your provider environment (sandbox vs production).');\n  } else {\n    setError(msg);\n  }\n}","preventionTips":["Trim and sanity-check provider keys against their documented format before saving.","Confirm you are using sandbox keys only with the sandbox environment and vice versa.","Check that required sync-server env vars for the provider are set on self-hosted installs.","Verify you have admin rights on the sync server before configuring bank-sync providers."],"tags":["bank-sync","api-key","sync-server","provider-credentials"],"backgroundTag":"provider-credentials-rejected","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}