hono icon indicating copy to clipboard operation
hono copied to clipboard

ETag changes every time when combine `etag` and `cache` middlewares

Open rwv opened this issue 8 months ago • 2 comments

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:

  1. Load the page
  2. Refresh
  3. Observe that the ETag header 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 DigestStream in 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) => ArrayBuffer signature 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.

rwv avatar Apr 14 '25 16:04 rwv

Hi, any update on this? Happy to provide more context or help with a PR if needed.
Thanks in advance!

rwv avatar May 13 '25 17:05 rwv

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.

usualoma avatar May 15 '25 11:05 usualoma