ETag changes every time when combine `etag` and `cache` middlewares
What version of Hono are you using?
4.7.6
What runtime/platform is your app running on? (with version if possible)
Cloudflare Workers
What steps can reproduce the bug?
Repository: https://github.com/rwv/hone-etag-large-payload-test
Live demo: https://hono-etag-large-payload-test.rwv.dev/
Steps:
- Load the page
- Refresh
- Observe that the
ETagheader changes each time
What is the expected behavior?
For identical response bodies, the ETag should remain stable across requests, regardless of how the body is chunked.
What do you see instead?
When combining the etag() and cache() middleware on Cloudflare Workers, the ETag value changes on every request—even when the response body remains the same. This breaks 304 behavior and prevents effective caching.
Additional information
Suspected Cause
On Cloudflare Workers, responses returned from the cache expose .body as a ReadableStream. The chunk structure of this stream is not consistent across requests.
This breaks the assumption made in #3604:
“It is unlikely that the same architecture (server) will have different data divisions.”
The above is speculative, based on observed behavior.
Suggestion
PR #3832 introduced the generateDigest(body: Uint8Array) option, which is useful but not sufficient in this case.
generateETag?: (res: ClonedResponse or stream?) => string | Promise<string>
This would:
- Native use of
DigestStreamin Cloudflare Workers for consistent hashing - Better semantic clarity: digesting a stream should take a stream, not a fully merged
Uint8Array - Avoids confusion: the current
(body: Uint8Array) => ArrayBuffersignature suggests that Hono reads the full body first, which it doesn’t. The chunked-digest logic is hidden from users and may cause incorrect assumptions.
Hi, any update on this? Happy to provide more context or help with a PR if needed.
Thanks in advance!
Hi @rwv, thank you for creating the issue.
SubtleCrypto does not support incremental digest generation, so Hono's cache middleware is currently designed as it is.
In general, responses that are divided into multiple chunks are often better generated in advance at an appropriate time before the etag middleware.
In your application's case, generating the ETag before the cache middleware would be advantageous as it would allow the generated ETag value to be cached along with the response.
diff --git i/src/index.ts w/src/index.ts
index 9a97ad7..4b3d9d1 100644
--- i/src/index.ts
+++ w/src/index.ts
@@ -12,6 +12,19 @@ app.use(
cacheControl: "max-age=3600",
})
);
+app.use("*", async (c, next) => {
+ const body = c.res.clone().body;
+ if (body) {
+ const digestStream = new crypto.DigestStream("SHA-256");
+ body.pipeTo(digestStream);
+ const digest = await digestStream.digest;
+ const hexString = [...new Uint8Array(digest)]
+ .map((b) => b.toString(16).padStart(2, "0"))
+ .join("");
+ c.res.headers.set("ETag", `W/"${hexString}"`);
+ }
+ next();
+});
// Generate a payload of the given size
function generatePayload(size: number) {
Given this situation, we do not anticipate changes to the etag middleware API until SubtleCrypto supports incremental generation.