denoland/deno · error · Error

ERR_HTTP_TRAILER_INVALID

ERR_HTTP_TRAILER_INVALID

Error message

Trailers are invalid with this transfer encoding

What it means

OutgoingMessage._storeHeader rejects a message that declares a Trailer header while its transfer encoding is not chunked (this.chunkedEncoding !== true). HTTP trailers are only defined for chunked bodies - a non-chunked message ends at the first empty line after headers, so a trailer could never be delivered - hence ERR_HTTP_TRAILER_INVALID at header-assembly time.

Solutions

  1. Remove the explicit content-length header so an HTTP/1.1 response can use chunked encoding with trailers.
  2. Drop the Trailer header and send the metadata as a normal header or in the body when you must keep content-length.
  3. Ensure the client connection is HTTP/1.1 (chunked is not negotiated for 1.0 responses).

Example fix

// before
res.setHeader('Trailer', 'X-Checksum');
res.setHeader('content-length', body.length); // chunking off -> throws
res.end(body);

// after
res.setHeader('Trailer', 'X-Checksum');
res.removeHeader('content-length'); // chunked encoding -> trailers legal
res.write(body);
res.addTrailers({ 'X-Checksum': sum });
res.end();
Defensive patterns

Strategy: validation

Validate before calling

// trailers are only legal with chunked encoding: no content-length and HTTP/1.1
const canTrailer = res.req?.httpVersionMajor === 1 &&
  res.req?.httpVersionMinor >= 1 &&
  !res.hasHeader('content-length');
if (canTrailer) res.setHeader('Trailer', 'X-Checksum');

Try / catch

try {
  res.setHeader('Trailer', 'X-Foo');
  res.writeHead(200);
} catch (err) {
  if (err.code === 'ERR_HTTP_TRAILER_INVALID') {
    res.removeHeader('Trailer');
    res.writeHead(200); // retry without trailers
  } else throw err;
}

Prevention

When it happens

Trigger: res.setHeader('Trailer', 'X-Foo') combined with anything that disables chunking: an explicit content-length header, HTTP/1.0 client (server forces _last and no chunked), or writeHead with content-length. Also adding 'Trailer' to an array/object headers argument on a non-chunked request.

Common situations: Adding trailing metadata (checksums, timing headers) without realizing the response got a fixed content-length (e.g. serving a file with known size); proxying HTTP/1.0 clients; frameworks that auto-set content-length for small bodies, silently disabling chunked encoding.

Related errors


AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20). Data as JSON: /api/errors/744304aa481a09dc. Report an issue: GitHub.

Appendix: source

Thrown at ext/node/polyfills/_http_outgoing.ts:1061

          this.chunkedEncoding = true;
        } else {
          // We should only be able to get here if both Content-Length and
          // Transfer-Encoding are removed by the user.
          // See: test/parallel/test-http-remove-header-stays-removed.js
          debug("Both Content-Length and Transfer-Encoding are removed");

          // We can't keep alive in this case, because with no header info the body
          // is defined as all data until the connection is closed.
          this._last = true;
        }
      }

      // Test non-chunked message does not have trailer header set,
      // message will be terminated by the first empty line after the
      // header fields, regardless of the header fields present in the
      // message, and thus cannot contain a message body or 'trailers'.
      if (this.chunkedEncoding !== true && state.trailer) {
        throw new ERR_HTTP_TRAILER_INVALID();
      }

      const { header } = state;
      this._header = header + "\r\n";

      // Wait until the first body chunk, or close(), is sent to flush,
      // UNLESS we're sending Expect: 100-continue.
      if (state.expect) this._send("");
    },
    writable: true,
    enumerable: true,
    configurable: true,
  },
  _storeHeaderEntry: {
    __proto__: null,
    value: function _storeHeaderEntry(
      state: any,
      field: string,

View on GitHub (pinned to 9ad36f7a2c)