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 by the Session constructor when the session-level requestHeaders config contains a Host key, matched case-insensitively. Because connect()/new Session() is cheap and lazy (no I/O until the first query), this check runs eagerly at construction so the mistake surfaces before any request — fetch would otherwise silently drop the forbidden Host header on every call.

Source

Thrown at serverless/javascript/src/session.ts:99

 * A database session that manages the connection state and baton.
 * 
 * Each session maintains its own connection state and can execute SQL statements
 * independently without interfering with other sessions.
 */
export class Session {
  private config: SessionConfig;
  private baton: string | null = null;
  private baseUrl: string;
  // Cached autocommit status from the server's last `get_autocommit` answer.
  // A fresh connection is in autocommit (not in a transaction).
  private autocommit: boolean = true;

  constructor(config: SessionConfig) {
    for (const name of Object.keys(config.requestHeaders ?? {})) {
      // `Host` is a forbidden fetch header and would be silently dropped —
      // reject it up front so the caller learns the override never takes effect.
      if (name.toLowerCase() === 'host') {
        throw new DatabaseError("overwriting the 'Host' header is not supported");
      }
    }
    this.config = config;
    this.baseUrl = normalizeUrl(config.url);
  }

  private httpContext(queryOptions?: QueryOptions): HttpContext {
    // Per-query headers are merged over the session-level ones, so a query
    // can override a header the session sets (and both override the
    // standard headers).
    let requestHeaders = this.config.requestHeaders;
    if (queryOptions?.requestHeaders) {
      requestHeaders = { ...requestHeaders, ...queryOptions.requestHeaders };
    }
    return {
      url: this.baseUrl,
      authToken: this.config.authToken,
      remoteEncryptionKey: this.config.remoteEncryptionKey,

View on GitHub (pinned to bad083fafb)

Solutions

  1. Remove Host from requestHeaders and change the connection url to route to the intended host
  2. When forwarding headers, whitelist only the keys you need (authorization, x-*) instead of spreading all of them
  3. Remember per-query QueryOptions.requestHeaders carries the same restriction — apply the same filtering there

Example fix

// before
const db = connect({ url, requestHeaders: { Host: "tenant-7.internal" } });

// after
const db = connect({ url: "https://tenant-7.internal", requestHeaders: { "x-tenant-id": "7" } });
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"),
  );

const db = connect({ url, authToken, requestHeaders: stripHost(forwardedHeaders) });

Prevention

When it happens

Trigger: connect({ url, authToken, requestHeaders: { Host: 'tenant.example.com' } }); forwarding an inbound request's full header set (which includes host) into the client config; lowercase 'host' or uppercase 'HOST' keys, all rejected identically.

Common situations: Multi-tenant routing attempts via the Host header; shared header constants that bundle host with real overrides; copy-pasted proxy configuration blocks adapted into requestHeaders.

Related errors


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