{"record":{"id":"08a659a4d4da23c3","repo":"actualbudget/actual","slug":"invalid-origin-header","errorCode":null,"errorMessage":"Invalid Origin header","messagePattern":"Invalid Origin header","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/sync-server/src/app-gocardless/app-gocardless.ts","lineNumber":37,"sourceCode":"  GoCardlessAccountId,\n  GoCardlessInstitutionId,\n  GoCardlessRequisitionId,\n} from './gocardless-node.types';\nimport { goCardlessService } from './services/gocardless-service';\nimport { handleError } from './util/handle-error';\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n  return typeof value === 'object' && value !== null;\n}\n\nconst ELECTRON_APP_ORIGIN = 'app://actual';\n\nfunction validateOrigin(origin: string | undefined) {\n  let url;\n  try {\n    url = new URL(origin ?? '');\n  } catch {\n    throw new Error('Invalid Origin header');\n  }\n  if (url.protocol !== 'http:' && url.protocol !== 'https:') {\n    throw new Error('Invalid Origin header');\n  }\n  return url.origin;\n}\n\nfunction resolveRedirectHost(req: Request) {\n  const { origin } = req.headers;\n  const host = req.get('host');\n  if (origin === ELECTRON_APP_ORIGIN && host) {\n    return `${req.protocol}://${host}`;\n  }\n  return validateOrigin(origin);\n}\n\nconst SAFE_ID = /^[a-zA-Z0-9_-]+$/;\nfunction sanitizeId<T extends string = string>(id: unknown): T {","sourceCodeStart":19,"sourceCodeEnd":55,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/sync-server/src/app-gocardless/app-gocardless.ts#L19-L55","documentation":"validateOrigin parses the request's Origin header with new URL(). If the header is missing/empty or not a parseable absolute URL, it throws Error('Invalid Origin header') before the GoCardless redirect flow can compute a redirect host.","triggerScenarios":"Calling the GoCardless link endpoints without sending an Origin header (curl/scripts, server-side fetches), or sending a malformed Origin (e.g. 'localhost:3001' without scheme, garbage values).","commonSituations":"Testing the endpoint with curl/Postman which omit Origin by default; reverse proxies stripping the Origin header; native clients constructing the Origin manually and forgetting 'https://'","solutions":["Send a proper Origin header on the request (e.g. 'Origin: https://budget.example.com')","Include the scheme (http:// or https://) in the Origin value","Check that your reverse proxy forwards Origin rather than removing it","Note protocol must be http: or https: — any other scheme is rejected"],"exampleFix":"// before\ncurl -X POST https://server/gocardless/create-link\n// after\ncurl -X POST -H 'Origin: https://budget.example.com' https://server/gocardless/create-link","handlingStrategy":"validation","validationCode":"function hasValidOrigin(origin) {\n  if (!origin) return false;\n  try { const u = new URL(origin); return u.protocol === 'http:' || u.protocol === 'https:'; }\n  catch { return false; }\n}\nif (!hasValidOrigin(window.location.origin)) throw new Error('fix Origin before calling');","typeGuard":null,"tryCatchPattern":"try {\n  const res = await fetch(`${server}/gocardless/create-link`, { headers: { Origin: location.origin } });\n} catch (e) {\n  if (e.message === 'Invalid Origin header') {\n    console.error('Origin header missing or malformed; send an absolute http(s) URL');\n  } else throw e;\n}","preventionTips":["Always send an absolute http(s) Origin header when calling GoCardless link endpoints","Verify reverse proxies forward Origin unchanged","Test with curl using -H 'Origin: ...' rather than omitting it"],"tags":["validation","http","gocardless","headers"],"backgroundTag":"invalid-origin-header","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}