Relevant source files
The following files were used as context for generating this wiki page:
- src/middleware/compress/index.ts
- src/adapter/aws-lambda/handler.ts
- src/middleware/secure-headers/secure-headers.ts
- src/helper/streaming/sse.ts
- src/middleware/etag/index.ts
- src/jsx/streaming.ts
- src/helper/streaming/stream.ts
- src/middleware/cache/index.ts
- src/utils/compress.ts
- src/middleware/etag/digest.ts
- src/helper/ssg/ssg.ts
- src/context.ts
- src/helper/streaming/text.ts
- src/request.ts
- src/helper/proxy/index.ts
- src/adapter/deno/websocket.ts
- src/utils/stream.ts
- src/adapter/lambda-edge/handler.ts
- src/middleware/serve-static/index.ts
- src/middleware/body-limit/index.ts
- src/middleware/jsx-renderer/index.ts
- src/adapter/bun/websocket.ts
- src/middleware/timing/timing.ts
- src/middleware/language/language.ts
- src/client/fetch-result-please.ts
- src/adapter/cloudflare-pages/handler.ts
- src/adapter/cloudflare-workers/websocket.ts
- runtime-tests/lambda/stream-mock.ts
- src/helper/websocket/index.ts
- src/utils/accept.ts
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.
| Option | Type | Default | Description |
|---|---|---|---|
encoding | 'gzip' | 'deflate' | undefined | The compression scheme to allow. If omitted, both schemes are permitted. |
threshold | number | 1024 | The minimum response body size in bytes required to trigger compression. |
contentTypeFilter | RegExp | ((contentType: string) => boolean) | COMPRESSIBLE_CONTENT_TYPE_REGEX | A 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:
-
compressinspects the response and retrieves theAccept-Encodingheader from the request object. Sources: src/middleware/compress/index.ts:103-104 -
selectEncodingreceives the header string and candidate array, invokingparseAcceptto transform the raw header into structured objects. Sources: src/middleware/compress/index.ts:24-31 -
parseAcceptiterates through the header string length, callinggetNextAcceptValueat each position. Sources: src/utils/accept.ts:211-222 -
getNextAcceptValuecallsconsumeWhitespaceto locate type tokens and invokesgetNextParamto parse key-value parameter pairs. Sources: src/utils/accept.ts:168-199 -
getNextParaminitiates parameter scanning by callingconsumeWhitespaceon the remaining substring. Sources: src/utils/accept.ts:66-70 -
consumeWhitespaceinvokesisWhitespacefor each character code encountered during index advancement. Sources: src/utils/accept.ts:10-16 -
isWhitespacechecks 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 } | undefinedSources: 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 Score | Handling Rule |
|---|---|---|
undefined or '' | 1 | Default weight when parameter is omitted or empty |
'NaN' or NaN | 1 | Invalid string or NaN numerical results default to 1 |
Infinity | 1 | Positive infinity normalizes to maximum priority 1 |
-Infinity | 0 | Negative infinity normalizes to minimum priority 0 |
< 0 or > 1 | 1 | Out-of-bounds numeric values default to 1 |
Valid number string (0.0–1.0) | Parsed Number | Converted 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.
compressinitiates the validation check on the incoming request header. Sources: src/middleware/compress/index.ts:103-104selectEncodingreceives the header string and delegates parsing toparseAccept. Sources: src/utils/accept.ts:211-222parseAcceptadvances through the buffer by invokinggetNextAcceptValueat each index offset. Sources: src/utils/accept.ts:221-222getNextAcceptValuescans character codes for delimiters such as semicolons and commas; if a type segment is empty when a separator is reached, it passes control toskipInvalidAcceptValue. Sources: src/utils/accept.ts:179-182skipInvalidAcceptValuetracks 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
Related
On This Page
OverviewMiddleware registration and configuration optionsAccept-Encoding negotiation and parsingParsing mechanisms and whitespace handlingCall-chain execution walkthroughQuality factors and encoding selectionEncoding candidate evaluation and priority calculationQuality factor sanitization rulesHeader sanitation and malformed tokensCall-chain execution walkthroughRecovery logic and token boundary trackingResponse transformation and stream pipingEvaluating response criteria before compressionPiping through CompressionStream and header adjustmentsRelated