tursodatabase/turso · error · DatabaseError

overwriting the 'Host' header is not supported

Error message

overwriting the 'Host' header is not supported

What it means

DatabaseError thrown while building request headers when a per-query requestHeaders object (QueryOptions.requestHeaders) contains a Host key, matched case-insensitively. fetch() classifies Host as a forbidden header and would silently drop the override, so the driver throws to make the no-op visible instead of letting routing logic quietly not work.

Source

Thrown at serverless/javascript/src/protocol.ts:243

   */
  requestHeaders?: Record<string, string>;
}

function buildHeaders(ctx: HttpContext): Record<string, string> {
  const headers: Record<string, string> = {
    'Content-Type': 'application/json',
  };
  if (ctx.authToken) {
    headers['Authorization'] = `Bearer ${ctx.authToken}`;
  }
  if (ctx.remoteEncryptionKey) {
    headers[ENCRYPTION_KEY_HEADER] = ctx.remoteEncryptionKey;
  }
  for (const [name, value] of Object.entries(ctx.requestHeaders ?? {})) {
    // `Host` is a forbidden fetch header and would be silently dropped —
    // throw instead so the caller learns the override never takes effect.
    if (name.toLowerCase() === 'host') {
      throw new DatabaseError("overwriting the 'Host' header is not supported");
    }
    headers[name] = value;
  }
  return headers;
}

function buildFetchOptions(ctx: HttpContext, body: string, signal?: AbortSignal): RequestInit {
  return {
    method: 'POST',
    headers: buildHeaders(ctx),
    body,
    signal,
  };
}

/** Per-query options. Override the session-level defaults for a single call. */
export interface QueryOptions {
  /** Per-query timeout in milliseconds. Overrides defaultQueryTimeout for this call. */

View on GitHub (pinned to bad083fafb)

Solutions

  1. Route by changing the connection's url (connect({ url })), never by overriding Host
  2. Strip the Host key before passing headers: filter out keys whose lowercase name is 'host'
  3. Use custom x-* headers for routing metadata — those pass through fine

Example fix

// before
await db.all(sql, { requestHeaders: { ...incomingHeaders } }); // incomingHeaders contains Host

// after
const headers = Object.fromEntries(
  Object.entries(incomingHeaders).filter(([k]) => k.toLowerCase() !== "host"),
);
await db.all(sql, { requestHeaders: headers });
Defensive patterns

Strategy: validation

Validate before calling

const stripHost = (h: Record<string, string> = {}): Record<string, string> =>
  Object.fromEntries(
    Object.entries(h).filter(([k]) => k.toLowerCase() !== "host"),
  );

await db.all(sql, { requestHeaders: stripHost(incomingHeaders) });

Prevention

When it happens

Trigger: Passing { requestHeaders: { Host: 'db.example.com' } } as queryOptions to run/get/all/exec/batch/pragma or statement methods; spreading an incoming request's full header set (which includes host) into queryOptions; case variants 'host'/'HOST' which are rejected the same way.

Common situations: Proxy or multi-tenant routing setups that try to select a backend via the Host header; middleware that forwards original client headers to the database request; header constants shared across services that include host.

Related errors


AI-assisted analysis of tursodatabase/turso@bad083fafb (2026-08-16). Data as JSON: /api/errors/69908f2af503c4b4. Report an issue: GitHub.