Relevant source files
The following files were used as context for generating this wiki page:
- src/middleware/etag/index.ts
- src/middleware/secure-headers/secure-headers.ts
- src/middleware/cache/index.ts
- src/middleware/etag/digest.ts
- src/context.ts
- src/helper/streaming/sse.ts
- src/adapter/aws-lambda/handler.ts
- src/middleware/serve-static/index.ts
- src/utils/crypto.ts
- src/middleware/bearer-auth/index.ts
- src/middleware/compress/index.ts
- src/request.ts
- src/middleware/basic-auth/index.ts
- src/middleware/timing/timing.ts
- src/jsx/dom/intrinsic-element/components.ts
- src/middleware/method-override/index.ts
- src/helper/ssg/ssg.ts
- src/helper/proxy/index.ts
- src/middleware/jwk/keys.test.json
- src/adapter/lambda-edge/handler.ts
- src/middleware/language/language.ts
- src/helper/ssg/middleware.ts
- src/middleware/cors/index.ts
- src/adapter/cloudflare-workers/serve-static.ts
- src/utils/compress.ts
- src/middleware/request-id/request-id.ts
- src/utils/headers.ts
- src/utils/buffer.ts
- src/middleware/powered-by/index.ts
Overview
Hono provides robust ETag and Cache middleware features that optimize HTTP request-response lifecycles through automated cryptographic hashing, conditional validation, and Web Standard Cache API integration. By computing SHA-1 or custom digests from response streams, the ETag middleware evaluates If-None-Match headers against strong or weak tags to short-circuit redundant transmissions with 304 Not Modified responses while retaining specified headers. Concurrently, the cache middleware interfaces with runtime storage via caches.open and cache.match, enforcing status code constraints, vary directive checks, and Cache-Control directives to manage cache population and conditional execution.
Sources: src/middleware/etag/index.ts:21-36, src/middleware/etag/index.ts:79-123, src/middleware/cache/index.ts:13-35, src/middleware/cache/index.ts:64-180
ETag generation and hashing algorithms
Public API and Generator Initialization
The ETag middleware accepts an ETagOptions configuration object defining parameters that control tag formatting, retained headers on validation bypass, and custom hashing logic. When no custom generator is provided via options.generateDigest, the initializer falls back to the native crypto.subtle API using the SHA-1 algorithm.
| Option | Type | Default | Purpose |
|---|---|---|---|
retainedHeaders | string[] | RETAINED_304_HEADERS | Headers preserved on 304 Not Modified responses. |
weak | boolean | false | Prepends W/ to generated ETags for weak validation. |
generateDigest | (body: Uint8Array) => ArrayBuffer | Promise<ArrayBuffer> | crypto.subtle (SHA-1) | Custom digest generation function for response bodies. |
Sources: src/middleware/etag/index.ts:9-13, src/middleware/etag/index.ts:38-54, src/middleware/etag/index.ts:79-82
Digest Calculation and Hex Encoding
The generateDigest function reads response bodies iteratively via a ReadableStream reader, merging incoming chunks and invoking the digest generator. Once the stream is fully consumed, the resulting ArrayBuffer digest is converted into a lowercase hexadecimal string by mapping each byte to a zero-padded two-character hex value.
export const generateDigest = async (
stream: ReadableStream<Uint8Array<ArrayBuffer>> | null,
generator: (body: Uint8Array<ArrayBuffer>) => ArrayBuffer | Promise<ArrayBuffer>
): Promise<string | null> => {
if (!stream) {
return null
}
let result: ArrayBuffer | undefined = undefined
const reader = stream.getReader()
for (;;) {
const { value, done } = await reader.read()
if (done) {
break
}
result = await generator(mergeBuffers(result, value))
}
if (!result) {
return null
}
return Array.prototype.map
.call(new Uint8Array(result), (x) => x.toString(16).padStart(2, '0'))
.join('')
}Sources: src/middleware/etag/digest.ts:16-43
Note
If a response carries an existing ETag header from upstream handlers, the ETag middleware bypasses automatic digest calculation entirely and uses the pre-existing tag value.
Sources: src/middleware/etag/index.ts:90-95
Conditional validation and weak tags
Execution Walkthrough and Sequence Analysis
Conditional validation in the ETag middleware evaluates incoming If-None-Match request headers against computed or pre-existing entity tags. When a request includes an If-None-Match header, the middleware inspects the value after downstream handlers execute and the response ETag is resolved. The validation check executes through a strict function call chain: etag handler invokes etagMatches, which in turn applies stripWeak to parse and normalize tags.
The execution sequence follows this precise path:
etag— The main middleware function extracts theIf-None-Matchrequest header and awaits downstream execution before reading theETagresponse header or generating a fresh digest. Sources: src/middleware/etag/index.ts:84-105etagMatches— Evaluates whetherifNoneMatchis non-null, splits the header value by commas and optional whitespace (/,\s*/), and checks if any token matches the target ETag. Sources: src/middleware/etag/index.ts:32-36stripWeak— Strips the weak validation prefixW/from the beginning of a tag string using a regular expression replacement (tag.replace(/^W\//, '')), allowing weak and strong tags to be compared equivalently during validation. Sources: src/middleware/etag/index.ts:30-30
304 Response Generation and Retained Headers
When etagMatches returns true, the middleware short-circuits the response by constructing a new 304 Not Modified response instance. This response discards the response body while preserving specific metadata headers defined by retainedHeaders. By default, the middleware references RETAINED_304_HEADERS to determine which response headers survive the validation bypass.
| Header Constant | Value / Identifier | Purpose |
|---|---|---|
RETAINED_304_HEADERS | 'cache-control' | Retains caching directives on 304 responses. |
RETAINED_304_HEADERS | 'content-location' | Retains resource location metadata. |
RETAINED_304_HEADERS | 'date' | Retains server response timestamp. |
RETAINED_304_HEADERS | 'etag' | Retains the entity tag for validation tracking. |
RETAINED_304_HEADERS | 'expires' | Retains expiration time limits. |
RETAINED_304_HEADERS | 'vary' | Retains cache negotiation criteria. |
Sources: src/middleware/etag/index.ts:21-28, src/middleware/etag/index.ts:107-119
Note
When iterating over response headers during a 304 short-circuit, header keys are converted to lowercase via key.toLowerCase() before checking against the retainedHeaders index array, ensuring case-insensitive header preservation.
Sources: src/middleware/etag/index.ts:115-119
Cache middleware and storage adapter
Cache Storage Initialization and Availability Check
The cache middleware integrates with the Web Standard Cache API via globalThis.caches. Upon initialization, the cache function verifies if globalThis.caches is defined in the current runtime. If the storage adapter is unavailable, the middleware checks the onCacheNotAvailable option: when set to false, it silently suppresses logging; when provided as a custom function, it invokes that handler; otherwise, it logs a default message to the console and returns a pass-through middleware handler that immediately invokes next().
if (!globalThis.caches) {
if (options.onCacheNotAvailable === false) {
// suppress log
} else if (options.onCacheNotAvailable) {
options.onCacheNotAvailable()
} else {
console.log('Cache Middleware is not enabled because caches is not defined.')
}
return async (_c, next) => await next()
}Sources: src/middleware/cache/index.ts:73-82
When globalThis.caches is present, the middleware resolves the cache store name. The cacheName option accepts either a static string or a dynamic function (c: Context) => Promise<string> | string. The resolved store name is passed to caches.open(cacheName) to obtain the cache instance.
const cacheName =
typeof options.cacheName === 'function' ? await options.cacheName(c) : options.cacheName
const cache = await caches.open(cacheName)Sources: src/middleware/cache/index.ts:155-157
Request Matching and Key Generation Lifecycle
Before executing downstream handlers, the middleware intercepts incoming requests. Requests are filtered by method and authorization headers: non-GET requests or requests containing an Authorization header bypass the cache middleware entirely and proceed directly to next().
if (c.req.method !== 'GET' || c.req.raw.headers.has('Authorization')) {
await next()
return
}Sources: src/middleware/cache/index.ts:139-142
For valid GET requests, the cache key is constructed starting from the request URL (c.req.url). If a custom keyGenerator option is supplied, it executes to replace the base key string. When vary directives are configured, the middleware appends each specified header name and its URI-encoded request header value to the cache key using a double-colon delimiter format: ::directive=encodedValue.
let key = c.req.url
if (options.keyGenerator) {
key = await options.keyGenerator(c)
}
if (varyDirectives) {
for (const directive of varyDirectives) {
const value = c.req.raw.headers.get(directive) ?? ''
key += `::${directive}=${encodeURIComponent(value)}`
}
}Sources: src/middleware/cache/index.ts:144-153
Once the cache key is assembled, the middleware queries the storage adapter via cache.match(key). If a matching response is located, a new Response is instantiated from the cached response body and headers, short-circuiting further request execution and returning immediately.
const response = await cache.match(key)
if (response) {
return new Response(response.body, response)
}Sources: src/middleware/cache/index.ts:158-161
Warning
Requests bearing an Authorization header automatically bypass the cache lookup and storage lifecycle, preventing authenticated user data from being improperly stored in shared cache tiers.
Sources: src/middleware/cache/index.ts:139-142
Writing to the Cache Store
When a cache miss occurs, the middleware awaits downstream execution (await next()). It then verifies whether the resulting response status code is present in the allowed cacheableStatusCodes set (which defaults to [200]). If the status code is ineligible, the middleware returns without caching.
await next()
if (!cacheableStatusCodes.has(c.res.status)) {
return
}Sources: src/middleware/cache/index.ts:163-166
Eligible responses undergo header augmentation and skip-cache evaluations. If checks pass, the response is cloned via c.res.clone(). The write operation to the cache store is governed by the wait boolean option:
- When
waitis set totrue, Hono awaits the resolution ofcache.put(key, res)before finishing the middleware handler execution (a requirement for runtimes like Deno). - When
waitisfalse(the default), the promise returned bycache.put(key, res)is passed toc.executionCtx.waitUntil()to allow background storage without blocking the response delivery.
const res = c.res.clone()
if (options.wait) {
await cache.put(key, res)
} else {
c.executionCtx.waitUntil(cache.put(key, res))
}Sources: src/middleware/cache/index.ts:174-179
Note
The wait option defaults to false. Explicitly setting wait: true is mandatory in Deno and other runtime environments that require pending promises to resolve within the request scope before termination.
Sources: src/middleware/cache/index.ts:44-44, src/middleware/cache/index.ts:84-86
Cache control and skipping heuristics
Heuristics and Skip Inspection
The cache middleware evaluates several inspection rules before deciding whether an eligible response should be stored in the cache. These heuristics examine the Cache-Control header, Set-Cookie headers, and Vary directives to prevent writing private or sensitive data.
const shouldSkipCacheControl = (cacheControl: string | null): boolean =>
!!cacheControl && /(?:^|,\s*)(?:private|no-(?:store|cache))(?:\s*(?:=|,|$))/i.test(cacheControl)Sources: src/middleware/cache/index.ts:15-16
The shouldSkipCache function consolidates these checks, returning true if any skipping criterion is satisfied:
const shouldSkipCache = (
res: Response,
optionsVaryDirectives: Set<string> | undefined,
responseVary: string[]
): boolean =>
(responseVary.length &&
(!optionsVaryDirectives || responseVary.some((name) => !optionsVaryDirectives.has(name)))) ||
shouldSkipCacheControl(res.headers.get('Cache-Control')) ||
res.headers.has('Set-Cookie')Sources: src/middleware/cache/index.ts:27-35
Warning
Responses containing a Set-Cookie header or a Cache-Control directive matching private, no-store, or no-cache are automatically excluded from cache writes to prevent leaking user sessions or private records.
Sources: src/middleware/cache/index.ts:32-35
Skip Inspection Call Chain
When determining whether to skip caching a response after downstream execution, Hono invokes a dedicated evaluation sequence.
Sources: src/middleware/cache/index.ts:15-17, src/middleware/cache/index.ts:27-35, src/middleware/cache/index.ts:170-172
cacheinvokesshouldSkipCachepassing the response and vary directives.shouldSkipCachechecks vary conditions and callsshouldSkipCacheControl.shouldSkipCacheControltests theCache-Controlheader string against the regex pattern forprivate,no-store, orno-cache.
Sources: src/middleware/cache/index.ts:15-17, src/middleware/cache/index.ts:27-35, src/middleware/cache/index.ts:170-172
Header Augmentation Options
The middleware can inject configured Cache-Control and Vary headers into outgoing responses prior to storage evaluation via the internal addHeader function.
const addHeader = (c: Context, responseVary: string[]) => {
if (cacheControlDirectives) {
const existingDirectives =
c.res.headers
.get('Cache-Control')
?.split(',')
.map((d) => d.trim().split('=', 1)[0]) ?? []
for (const directive of cacheControlDirectives) {
let [name, value] = directive.trim().split('=', 2)
name = name.toLowerCase()
if (!existingDirectives.includes(name)) {
c.header('Cache-Control', `${name}${value ? `=${value}` : ''}`, { append: true })
}
}
}
if (varyDirectives) {
if (responseVary.length === 0) {
c.header('Vary', Array.from(varyDirectives).join(', '))
} else {
const merged = new Set(varyDirectives)
for (const directive of responseVary) {
merged.add(directive)
}
if (merged.has('*')) {
c.header('Vary', '*')
} else {
c.header('Vary', Array.from(merged).join(', '))
}
}
}
}Sources: src/middleware/cache/index.ts:105-136
Note
If varyDirectives includes the wildcard token *, the middleware throws an error upon initialization because RFC 7231 Section 7.1.4 disallows effective caching when * is specified in the Vary header.
Sources: src/middleware/cache/index.ts:93-99
Buffer management for digest hashing
Chunk Concatenation and Memory Merging
The generateDigest utility in src/middleware/etag/digest.ts processes streamed response payloads by reading chunks iteratively from a ReadableStream<Uint8Array<ArrayBuffer>>. When building the complete hash input across multiple chunks, the internal mergeBuffers function combines an existing ArrayBuffer with an incoming Uint8Array<ArrayBuffer> chunk. If buffer1 is undefined, it returns buffer2 directly; otherwise, it allocates a new ArrayBuffer matching the combined byte lengths, writes the initial buffer at offset 0, and copies the incoming chunk at offset buffer1.byteLength.
const mergeBuffers = (
buffer1: ArrayBuffer | undefined,
buffer2: Uint8Array<ArrayBuffer>
): Uint8Array<ArrayBuffer> => {
if (!buffer1) {
return buffer2
}
const merged = new Uint8Array<ArrayBuffer>(
new ArrayBuffer(buffer1.byteLength + buffer2.byteLength)
)
merged.set(new Uint8Array(buffer1), 0)
merged.set(buffer2, buffer1.byteLength)
return merged
}Sources: src/middleware/etag/digest.ts:1-14
Note
Successive chunks from the stream reader are accumulated iteratively via mergeBuffers, feeding the result into the custom or default generator function on every read cycle until the stream signals completion.
Sources: src/middleware/etag/digest.ts:26-34
Stream Reader Loop and Hex Encoding
Once all stream chunks are processed and hashed, the resulting ArrayBuffer is converted into a lowercase hexadecimal string by mapping over its bytes with toString(16).padStart(2, '0') and joining them. If the input stream is null or yields no result, the generator returns null.
export const generateDigest = async (
stream: ReadableStream<Uint8Array<ArrayBuffer>> | null,
generator: (body: Uint8Array<ArrayBuffer>) => ArrayBuffer | Promise<ArrayBuffer>
): Promise<string | null> => {
if (!stream) {
return null
}
let result: ArrayBuffer | undefined = undefined
const reader = stream.getReader()
for (;;) {
const { value, done } = await reader.read()
if (done) {
break
}
result = await generator(mergeBuffers(result, value))
}
if (!result) {
return null
}
return Array.prototype.map
.call(new Uint8Array(result), (x) => x.toString(16).padStart(2, '0'))
.join('')
}Sources: src/middleware/etag/digest.ts:16-43
Related
On This Page
OverviewETag generation and hashing algorithmsPublic API and Generator InitializationDigest Calculation and Hex EncodingConditional validation and weak tagsExecution Walkthrough and Sequence Analysis304 Response Generation and Retained HeadersCache middleware and storage adapterCache Storage Initialization and Availability CheckRequest Matching and Key Generation LifecycleWriting to the Cache StoreCache control and skipping heuristicsHeuristics and Skip InspectionSkip Inspection Call ChainHeader Augmentation OptionsBuffer management for digest hashingChunk Concatenation and Memory MergingStream Reader Loop and Hex EncodingRelated