actualbudget/actual · error

Invalid GoCardless identifier: ${String(id)}

Error message

Invalid GoCardless identifier: ${String(id)}

What it means

sanitizeId validates GoCardless path/query identifiers (institutionId, requisitionId, country, accountId) against SAFE_ID = /^[a-zA-Z0-9_-]+$/. Anything else (empty, containing slashes, dots, spaces, or non-strings) throws Error('Invalid GoCardless identifier: ...'), acting as an injection/SSRF guard before values reach the GoCardless API URL.

Source

Thrown at packages/sync-server/src/app-gocardless/app-gocardless.ts:57

  if (url.protocol !== 'http:' && url.protocol !== 'https:') {
    throw new Error('Invalid Origin header');
  }
  return url.origin;
}

function resolveRedirectHost(req: Request) {
  const { origin } = req.headers;
  const host = req.get('host');
  if (origin === ELECTRON_APP_ORIGIN && host) {
    return `${req.protocol}://${host}`;
  }
  return validateOrigin(origin);
}

const SAFE_ID = /^[a-zA-Z0-9_-]+$/;
function sanitizeId<T extends string = string>(id: unknown): T {
  if (typeof id !== 'string' || !SAFE_ID.test(id)) {
    throw new Error(`Invalid GoCardless identifier: ${String(id)}`);
  }
  return id as T;
}

const LINK_PAGE_HTML = `<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Actual</title>
  </head>
  <body>
    <script>
      window.close();
    </script>

    <p>Please wait...</p>
    <p>
      The window should close automatically. If nothing happened you can close

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Trim and verify the id against /^[a-zA-Z0-9_-]+$/ before sending it
  2. Use ids exactly as returned by the GoCardless institutions/requisitions endpoints (do not pass URLs)
  3. Add String(x).trim() at the client boundary if values may have whitespace
  4. Log the offending value from the error message and correct the caller

Example fix

// before
const id = input.id; // "REL_1234/extra"
await fetch(`/gocardless/get-account/${id}`)
// after
const id = String(input.id).trim();
if (!/^[a-zA-Z0-9_-]+$/.test(id)) throw new Error('bad id');
await fetch(`/gocardless/get-account/${id}`)
Defensive patterns

Strategy: validation

Validate before calling

const SAFE_ID = /^[a-zA-Z0-9_-]+$/;
function assertSafeGoCardlessId(id) {
  if (typeof id !== 'string' || !SAFE_ID.test(id)) {
    throw new Error(`Refusing to send invalid GoCardless id: ${String(id)}`);
  }
  return id.trim();
}
assertSafeGoCardlessId(institutionId);

Type guard

function isSafeId(id: unknown): id is string {
  return typeof id === 'string' && /^[a-zA-Z0-9_-]+$/.test(id);
}

Try / catch

try {
  await gocardlessRoute({ institutionId });
} catch (e) {
  if (e.message.startsWith('Invalid GoCardless identifier')) {
    console.error(`Bad id "${institutionId}" — must match /^[a-zA-Z0-9_-]+$/; use the raw id, not a URL`);
  } else throw e;
}

Prevention

When it happens

Trigger: Passing an institution id like 'SANDBOXFINANCE_SFIN000060 ' (trailing space), a full URL, an empty string, undefined/null, or an id containing characters like ':' or '/' into the GoCardless route handlers.

Common situations: Copy-pasting ids with trailing whitespace or surrounding quotes; accidentally passing a requisition URL instead of its id; a bank picker supplying an untrimmed value; scripts interpolating undefined variables.

Related errors


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/d86a964fee5bb4de. Report an issue: GitHub.