Skip to main content
doc0blade47/honodocumentation home
Sign in

Response Compression

Last updated Oct 12, 2026100% grounded
Relevant source files

The following files were used as context for generating this wiki page:

Overview

Hono provides built-in response compression via the compress middleware, allowing applications to reduce network payload sizes by applying compression algorithms such as gzip and deflate. The middleware intercepts outgoing responses after route execution, evaluates criteria such as content length thresholds, content types, and caching directives, and transforms qualifying response streams using the Web Standard CompressionStream API.

Sources: src/middleware/compress/index.ts:46-121

Middleware registration and configuration options

The compress middleware is configured through the CompressionOptions interface, which defines parameters controlling encoding selection, minimum payload size thresholds, and content type filtering. When invoked via compress(options), it returns a MiddlewareHandler that intercepts execution and inspects response characteristics.

OptionTypeDefaultDescription
encoding'gzip' | 'deflate'undefinedThe compression scheme to allow. If omitted, both schemes are permitted.
thresholdnumber1024The minimum response body size in bytes required to trigger compression.
contentTypeFilterRegExp | ((contentType: string) => boolean)COMPRESSIBLE_CONTENT_TYPE_REGEXA regular expression or predicate function determining whether a given Content-Type header qualifies for compression.

Sources: src/middleware/compress/index.ts:16-22, src/middleware/compress/index.ts:51-54, src/middleware/compress/index.ts:70-74

const app = new Hono()
 
app.use(compress())
 
// Compress only JSON responses
app.use(compress({ contentTypeFilter: /^application\/json/ }))
 
// Compress based on custom Content-Type logic
app.use(compress({ contentTypeFilter: (type) => COMPRESSIBLE_CONTENT_TYPE_REGEX.test(type) || type === "application/x-myformat" }))

Sources: src/middleware/compress/index.ts:57-68

Note

If the encoding option is omitted, the candidate list defaults to ['gzip', 'deflate'], allowing selection based on the client's Accept-Encoding header with gzip prioritized when both are weighted equally.

Sources: src/middleware/compress/index.ts:12, src/middleware/compress/index.ts:52, src/middleware/compress/index.ts:72

Accept-Encoding negotiation and parsing

Parsing mechanisms and whitespace handling

Client requests specify supported compression formats using the Accept-Encoding header. Hono processes this string by iterating through characters, parsing comma-separated accept values, and extracting associated parameter definitions such as quality factors. During parsing, whitespace characters—specifically ASCII space (32), horizontal tab (9), line feed (10), and carriage return (13)—are inspected and skipped using dedicated utility helpers.

const isWhitespace = (char: number): boolean =>
  char === 32 || char === 9 || char === 10 || char === 13
 
const consumeWhitespace = (acceptHeader: string, startIndex: number): number => {
  while (startIndex < acceptHeader.length) {
    if (!isWhitespace(acceptHeader.charCodeAt(startIndex))) {
      break
    }
    startIndex++
  }
  return startIndex
}

Sources: src/utils/accept.ts:7-18

Note

The isWhitespace helper evaluates character code points directly, allowing consumeWhitespace to advance the traversal index past any leading or intervening spaces, tabs, or newlines without allocating temporary substrings.

Sources: src/utils/accept.ts:7-18

Call-chain execution walkthrough

When a response is evaluated for compression, the execution flow traverses through specific parsing and selection steps:

  1. compress inspects the response and retrieves the Accept-Encoding header from the request object. Sources: src/middleware/compress/index.ts:103-104

  2. selectEncoding receives the header string and candidate array, invoking parseAccept to transform the raw header into structured objects. Sources: src/middleware/compress/index.ts:24-31

  3. parseAccept iterates through the header string length, calling getNextAcceptValue at each position. Sources: src/utils/accept.ts:211-222

  4. getNextAcceptValue calls consumeWhitespace to locate type tokens and invokes getNextParam to parse key-value parameter pairs. Sources: src/utils/accept.ts:168-199

  5. getNextParam initiates parameter scanning by calling consumeWhitespace on the remaining substring. Sources: src/utils/accept.ts:66-70

  6. consumeWhitespace invokes isWhitespace for each character code encountered during index advancement. Sources: src/utils/accept.ts:10-16

  7. isWhitespace checks whether the character code matches ASCII space, tab, line feed, or carriage return. Sources: src/utils/accept.ts:7-8

Sources: src/middleware/compress/index.ts:24-31, src/utils/accept.ts:7-18, src/utils/accept.ts:66-70, src/utils/accept.ts:168-199, src/utils/accept.ts:211-222

Quality factors and encoding selection

Encoding candidate evaluation and priority calculation

When selecting an encoding, selectEncoding examines the parsed accept entries against the available compression candidates (gzip and deflate). If the Accept-Encoding header is undefined, the function immediately returns undefined. Otherwise, it invokes parseAccept and inspects any wildcard (*) quality factor (q) value present in the request.

const selectEncoding = (
  header: string | undefined,
  candidates: readonly Encoding[]
): Encoding | undefined => {
  if (header === undefined) {
    return undefined
  }
  const accepts = parseAccept(header)
  const wildcardQ = accepts.find((a) => a.type === '*')?.q
  let best: { encoding: Encoding; q: number } | undefined

Sources: src/middleware/compress/index.ts:24-33

For each candidate encoding, the parser checks for an explicit match against the candidate name (case-insensitively). If an explicit directive is absent, it falls back to the wildcard quality factor or 0. If any candidate yields a quality factor of 1, iteration halts immediately and that encoding is returned. Otherwise, the candidate with the highest positive quality factor is selected as the best encoding.

  for (const enc of candidates) {
    const explicit = accepts.find((a) => a.type.toLowerCase() === enc)
    const q = explicit ? explicit.q : (wildcardQ ?? 0)
    if (q === 1) {
      return enc
    } else if (q > 0 && (!best || q > best.q)) {
      best = { encoding: enc, q }
    }
  }
  return best?.encoding
}

Sources: src/middleware/compress/index.ts:34-44

Note

The evaluation loop short-circuits upon encountering the first candidate with a quality factor equal to 1, prioritizing faster decision-making for fully preferred client encodings without scanning remaining candidates.

Sources: src/middleware/compress/index.ts:37-38

Quality factor sanitization rules

The parseQuality function normalizes raw string quality values extracted from accept parameters into standard numeric scores between 0 and 1. Missing or empty parameter values default to 1. Special string literals and out-of-range numbers are sanitized according to the fallback rules defined below.

Input Parameter (qVal)Resulting ScoreHandling Rule
undefined or ''1Default weight when parameter is omitted or empty
'NaN' or NaN1Invalid string or NaN numerical results default to 1
Infinity1Positive infinity normalizes to maximum priority 1
-Infinity0Negative infinity normalizes to minimum priority 0
< 0 or > 11Out-of-bounds numeric values default to 1
Valid number string (0.0–1.0)Parsed NumberConverted directly via Number(qVal)
const parseQuality = (qVal?: string): number => {
  if (qVal === undefined) {
    return 1
  }
  if (qVal === '') {
    return 1
  }
  if (qVal === 'NaN') {
    return 0
  }
 
  const num = Number(qVal)
  if (num === Infinity) {
    return 1
  }
  if (num === -Infinity) {
    return 0
  }
  if (Number.isNaN(num)) {
    return 1
  }
  if (num < 0 || num > 1) {
    return 1
  }
 
  return num
}

Sources: src/utils/accept.ts:240-266

Warning

Passing an out-of-bounds quality factor greater than 1 or less than 0 does not reject the encoding; instead, the sanitizer falls back to a score of 1, treating the directive as fully preferred.

Sources: src/utils/accept.ts:261-263

Header sanitation and malformed tokens

Call-chain execution walkthrough

When client request headers contain malformed directives or unexpected token structures, the parsing engine relies on dedicated recovery routines to bypass syntax errors without throwing exceptions. The execution flow follows a specific call-chain during error handling: compress invokes selectEncoding, which calls parseAccept, which iterates via getNextAcceptValue, and finally branches into skipInvalidAcceptValue when an empty or broken token is encountered.

  1. compress initiates the validation check on the incoming request header. Sources: src/middleware/compress/index.ts:103-104
  2. selectEncoding receives the header string and delegates parsing to parseAccept. Sources: src/utils/accept.ts:211-222
  3. parseAccept advances through the buffer by invoking getNextAcceptValue at each index offset. Sources: src/utils/accept.ts:221-222
  4. getNextAcceptValue scans character codes for delimiters such as semicolons and commas; if a type segment is empty when a separator is reached, it passes control to skipInvalidAcceptValue. Sources: src/utils/accept.ts:179-182
  5. skipInvalidAcceptValue tracks quotation boundaries and escape characters (\ at code 92, quotes at code 34) until it finds a comma (code 44) or reaches the end of the header string, safely discarding the malformed segment. Sources: src/utils/accept.ts:46-64

Sources: src/middleware/compress/index.ts:103-104, src/utils/accept.ts:46-64, src/utils/accept.ts:179-182, src/utils/accept.ts:211-222

Recovery logic and token boundary tracking

The token scanning mechanism uses character code inspections to handle quoted strings and escape sequences safely. Inside skipInvalidAcceptValue, the inQuotes boolean flag toggles whenever a double quote (", code 34) is encountered outside an escape context. When inQuotes is active, backslash characters (\, code 92) trigger an index increment that consumes the following character as an escaped literal, preventing premature quote termination.

const skipInvalidAcceptValue = (acceptHeader: string, startIndex: number): number => {
  let i = startIndex
  let inQuotes = false
  while (i < acceptHeader.length) {
    const char = acceptHeader.charCodeAt(i)
    if (inQuotes && char === 92) {
      // '\' => escape
      i++
    } else if (char === 34) {
      // '"' => toggle quotes
      inQuotes = !inQuotes
    } else if (!inQuotes && char === 44) {
      // ',' => next accept value
      return i + 1
    }
    i++
  }
  return i
}

Sources: src/utils/accept.ts:46-64

When parameter parsing encounters an unexpected assignment or malformed key structure, helper functions such as skipInvalidParam locate the next delimiter to resume normal iteration. If an equals sign appears where a parameter value is expected, or if parameters lack proper assignment tokens, parsing skips forward to the next semicolon or comma.

Caution

Unescaped backslashes inside quoted accept parameters can offset quote toggling logic; the parser handles escape sequences explicitly by advancing past backslash characters during quoted string traversal.

Sources: src/utils/accept.ts:50-53

Response transformation and stream piping

Evaluating response criteria before compression

Before transforming a response body, the compress middleware inspects response headers and request methods to ensure safety and eligibility. Compression is bypassed if the response already contains a Content-Encoding or Transfer-Encoding header, if the request method is HEAD, if the content length falls below the configured threshold, or if shouldTransform detects a Cache-Control: no-transform directive. Furthermore, shouldCompress checks the response Content-Type against the contentTypeFilter option or the default COMPRESSIBLE_CONTENT_TYPE_REGEX pattern.

const contentLength = ctx.res.headers.get('Content-Length')
 
// Check if response should be compressed
if (
  ctx.res.headers.has('Content-Encoding') || // already encoded
  ctx.res.headers.has('Transfer-Encoding') || // already encoded or chunked
  ctx.req.method === 'HEAD' || // HEAD request
  (contentLength && Number(contentLength) < threshold) || // content-length below threshold
  !shouldCompress(ctx.res) || // not compressible type
  !shouldTransform(ctx.res) // cache-control: no-transform
) {
  return
}

Sources: src/middleware/compress/index.ts:89-101

Piping through CompressionStream and header adjustments

Once eligibility is confirmed and a valid encoding is negotiated, the middleware instantiates a web standard CompressionStream with the selected encoding type (gzip or deflate). The original response body is piped through the compression stream via ctx.res.body.pipeThrough(stream), and a new Response object replaces ctx.res. The Content-Length header is removed because compressed stream output changes the payload size, while Content-Encoding is updated to match the chosen algorithm. Additionally, if a strong ETag header is present, it is converted into a weak ETag prefixed with W/ since the compressed representation is not byte-identical to the original source.

// Compress the response
const stream = new CompressionStream(encoding)
ctx.res = new Response(ctx.res.body.pipeThrough(stream), ctx.res)
ctx.res.headers.delete('Content-Length')
ctx.res.headers.set('Content-Encoding', encoding)
 
// Convert strong ETag to weak ETag since compressed content is not byte-identical
const etag = ctx.res.headers.get('ETag')
if (etag && !etag.startsWith('W/')) {
  ctx.res.headers.set('ETag', `W/${etag}`)
}

Sources: src/middleware/compress/index.ts:109-119

Warning

Strong ETags must be converted to weak ETags (W/) following compression because on-the-fly transformations alter byte-level payload hashes, which would otherwise invalidate conditional request validations against uncompressed caches.

Sources: src/middleware/compress/index.ts:115-119

Was this page helpful?