actualbudget/actual · error

Duplicate headers encountered for key ${key}

Error message

Duplicate headers encountered for key ${key}

What it means

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.

Source

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

function generateGroupId(): GroupId {
  const id = uuidv4();
  if (!isValidGroupId(id)) {
    throw new TypeError('UUID format no longer matches expected format');
  }
  return id;
}

function extractSingleHeader(
  req: Request,
  res: Response,
  key: string,
): string | null {
  const value = req.headers[key];
  if (!value) {
    return null;
  }
  if (typeof value !== 'string') {
    res.status(400).send('Duplicate headers encountered for key ' + key);
    return null;
  }
  return value;
}

const verifyFileExists = (
  fileId: unknown,
  filesService: FilesService,
  res: Response,
  errorObject: string | Record<string, unknown>,
) => {
  if (typeof fileId !== 'string' || !isValidFileId(fileId)) {
    res.status(400).send('invalid fileId');
    return;
  }

  try {
    return filesService.get(fileId);

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Inspect the outgoing request and remove the duplicated header so each X-ACTUAL-* key appears once.
  2. Fix the proxy/middleware that adds the header (e.g. nginx `proxy_set_header` on a header the client already sends).
  3. In the client, set headers via a single map assignment instead of appending multiple times.

Example fix

// before: duplicate X-ACTUAL-FILE-ID
headers.append('X-ACTUAL-FILE-ID', fileId); // fetch Headers.append allows duplicates
// after
headers.set('X-ACTUAL-FILE-ID', fileId); // replaces, guaranteeing a single value
Defensive patterns

Strategy: validation

Validate before calling

const names = ['X-ACTUAL-FILE-ID','X-ACTUAL-GROUP-ID','X-ACTUAL-KEY-ID','X-ACTUAL-ENCRYPT-META','X-ACTUAL-FORMAT-VERSION'];
for (const n of names) {
  const v = req.headers[n.toLowerCase()];
  if (Array.isArray(v)) throw new Error('duplicate header: ' + n);
}

Type guard

const isSingleHeader = (v) => typeof v === 'string';

Try / catch

try {
  const res = await sendSyncRequest(headers);
  if (res.status === 400 && (await res.text()).startsWith('Duplicate headers encountered')) throw new Error('remove duplicated X-ACTUAL-* headers');
  return res;
} catch (e) { throw e; }

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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