{"record":{"id":"867bda0df9126c9e","repo":"actualbudget/actual","slug":"handleenablebankingerror-response-status-response","errorCode":null,"errorMessage":"handleEnableBankingError(response.status, responseBody)","messagePattern":"handleEnableBankingError\\(response\\.status, responseBody\\)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/sync-server/src/app-enablebanking/services/enablebanking-service.ts","lineNumber":180,"sourceCode":"      throw new EnableBankingError(\n        'TIMED_OUT',\n        'TIMED_OUT',\n        'Request timed out',\n      );\n    }\n    throw error;\n  } finally {\n    clearTimeout(timer);\n  }\n\n  if (!response.ok) {\n    let responseBody: unknown;\n    try {\n      responseBody = await response.json();\n    } catch {\n      responseBody = await response.text().catch(() => 'unknown');\n    }\n    throw handleEnableBankingError(response.status, responseBody);\n  }\n\n  // eslint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- generic API wrapper, type is validated by caller\n  return (await response.json()) as T;\n}\n\n// --- Normalization functions ---\n\n// SEPA / ISO 20022 structured remittance prefixes (e.g. `EREF+invoice-42`).\n// They are metadata for clearing systems, not user-facing text, so we strip\n// them from the front of each remittance line. The list is an allowlist of\n// known prefixes rather than a catch-all `[A-Z]{3,}\\+` so we don't accidentally\n// strip merchant tokens like `BMW+` or `USB+` that legitimately start a\n// description.\nconst SEPA_PREFIX_RE =\n  /^(?:EREF|KREF|MREF|CRED|DBTR|CDTR|SVWZ|SVCL|PURP|RTRN|REJT|REFE|SDVA|INDA|NTAV|ULTC|ULTD|ULTB|ABWA|ABWE|IBAN|BIC|COAM|OAMT|REMI|SQTP|ROC)\\+/;\n\nfunction stripSepaPrefix(s: string): string {","sourceCodeStart":162,"sourceCodeEnd":198,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/sync-server/src/app-enablebanking/services/enablebanking-service.ts#L162-L198","documentation":"The Enable Banking API wrapper request() throws handleEnableBankingError(response.status, responseBody) whenever the upstream HTTP response is not ok. The raw status plus parsed JSON/text body are converted into a normalized EnableBankingError, so callers get a consistent error type instead of raw fetch responses. Raised in request() and surfaced through validateCredentials, getApplication, getAspsps, startAuth, createSession, and getSession.","triggerScenarios":"Any Enable Banking REST call returning 4xx/5xx: invalid or expired JWT (401), unknown ASPSP id (404), malformed session request (400), bank-side rejection during startAuth/createSession, or upstream 5xx outage. Also non-JSON error bodies are tolerated and passed through.","commonSituations":"Expired Enable Banking application credentials; wrong applicationId/secretKey secrets configured; rate limits or daily fetch caps enforced by ASPSPs (when PSU headers are absent); bank API downtime returning 502/503.","solutions":["Inspect the thrown EnableBankingError's type/status and detail from the response body","For 401/403, check enablebanking_applicationId and enablebanking_secretKey secrets and that the JWT is freshly generated","For 4xx on createSession/startAuth, validate the aspsp id and PSU payload sent to the API","For 5xx, retry with backoff — likely a bank or Enable Banking outage","If rate-limited, ensure PSU headers are forwarded so requests count as user-triggered"],"exampleFix":"// before\nconst aspsp = await getAspsps(unknownId); // throws raw 404 normalized error\n// after\ntry {\n  const aspsp = await getAspsps(id);\n} catch (e) {\n  if (e instanceof EnableBankingError && e.status === 404) {\n    return null; // unknown ASPSP id\n  }\n  throw e;\n}","handlingStrategy":"try-catch","validationCode":"// Verify Enable Banking secrets are configured before any API call\nconst applicationId = secretsService.get(SecretName.enablebanking_applicationId);\nconst secretKey = secretsService.get(SecretName.enablebanking_secretKey);\nif (!applicationId || !secretKey) {\n  throw new Error('Enable Banking not configured'); // avoids a guaranteed 401 round-trip\n}","typeGuard":"function isEnableBankingHttpError(e: unknown): e is EnableBankingError & { status: number } {\n  return e instanceof EnableBankingError && typeof e.status === 'number' && e.status >= 400;\n}","tryCatchPattern":"try {\n  return await request<T>(method, path, body);\n} catch (e) {\n  if (isEnableBankingHttpError(e)) {\n    if (e.status === 401 || e.status === 403) return refreshCredentialsAndRetry();\n    if (e.status >= 500) return retryWithBackoff(() => request<T>(method, path, body));\n  }\n  throw e;\n}","preventionTips":["Match on EnableBankingError status/type codes instead of parsing raw response bodies","Refresh the JWT per request (getAuthorizationHeader already does) — never cache long-lived bearer tokens","Forward PSU headers for user-triggered fetches to avoid ASPSP background-fetch rate limits","Add exponential backoff only for 5xx/timeouts; treat 4xx as non-retryable configuration or input errors","Monitor Enable Banking status/bank outages before blaming your integration"],"tags":["http","enablebanking","api","third-party"],"backgroundTag":"upstream-http-error","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}