{"record":{"id":"35bf6671148ff483","repo":"actualbudget/actual","slug":"duplicate-headers-encountered-for-key-key","errorCode":null,"errorMessage":"Duplicate headers encountered for key ${key}","messagePattern":"Duplicate headers encountered for key (.+?)","errorType":"http","errorClass":null,"httpStatus":400,"severity":"error","filePath":"packages/sync-server/src/app-sync.ts","lineNumber":88,"sourceCode":"function generateGroupId(): GroupId {\n  const id = uuidv4();\n  if (!isValidGroupId(id)) {\n    throw new TypeError('UUID format no longer matches expected format');\n  }\n  return id;\n}\n\nfunction extractSingleHeader(\n  req: Request,\n  res: Response,\n  key: string,\n): string | null {\n  const value = req.headers[key];\n  if (!value) {\n    return null;\n  }\n  if (typeof value !== 'string') {\n    res.status(400).send('Duplicate headers encountered for key ' + key);\n    return null;\n  }\n  return value;\n}\n\nconst verifyFileExists = (\n  fileId: unknown,\n  filesService: FilesService,\n  res: Response,\n  errorObject: string | Record<string, unknown>,\n) => {\n  if (typeof fileId !== 'string' || !isValidFileId(fileId)) {\n    res.status(400).send('invalid fileId');\n    return;\n  }\n\n  try {\n    return filesService.get(fileId);","sourceCodeStart":70,"sourceCodeEnd":106,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/sync-server/src/app-sync.ts#L70-L106","documentation":"extractSingleHeader reads a single request header by key; express delivers repeated headers as an array. When req.headers[key] is not a string (an array of duplicates), the helper responds 400 with 'Duplicate headers encountered for key ' + key and returns null.","triggerScenarios":"An HTTP request to a sync route (X-ACTUAL-*) that contains the same custom header twice, e.g. from a misconfigured proxy adding the header when the client already sets it.","commonSituations":"Reverse proxies (nginx/traefik/Cloudflare workers) appending auth or encryption-key headers; HTTP/2 lowercase/duplicate header injection; test harnesses merging header maps with duplicates.","solutions":["Inspect the outgoing request and remove the duplicated header so each X-ACTUAL-* key appears once.","Fix the proxy/middleware that adds the header (e.g. nginx `proxy_set_header` on a header the client already sends).","In the client, set headers via a single map assignment instead of appending multiple times."],"exampleFix":"// before: duplicate X-ACTUAL-FILE-ID\nheaders.append('X-ACTUAL-FILE-ID', fileId); // fetch Headers.append allows duplicates\n// after\nheaders.set('X-ACTUAL-FILE-ID', fileId); // replaces, guaranteeing a single value","handlingStrategy":"validation","validationCode":"const names = ['X-ACTUAL-FILE-ID','X-ACTUAL-GROUP-ID','X-ACTUAL-KEY-ID','X-ACTUAL-ENCRYPT-META','X-ACTUAL-FORMAT-VERSION'];\nfor (const n of names) {\n  const v = req.headers[n.toLowerCase()];\n  if (Array.isArray(v)) throw new Error('duplicate header: ' + n);\n}","typeGuard":"const isSingleHeader = (v) => typeof v === 'string';","tryCatchPattern":"try {\n  const res = await sendSyncRequest(headers);\n  if (res.status === 400 && (await res.text()).startsWith('Duplicate headers encountered')) throw new Error('remove duplicated X-ACTUAL-* headers');\n  return res;\n} catch (e) { throw e; }","preventionTips":["Use Headers.set (not append) when building request headers","Audit reverse-proxy configs for headers added on top of client-supplied ones","Log outgoing headers once when debugging sync setups"],"tags":["http-headers","http-400","sync","proxy"],"backgroundTag":"duplicate-http-header","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}