diff --git a/src/middleware/etag/digest.ts b/src/middleware/etag/digest.ts index 5af9cacc9..336590b8c 100644 --- a/src/middleware/etag/digest.ts +++ b/src/middleware/etag/digest.ts @@ -9,7 +9,8 @@ const mergeBuffers = (buffer1: ArrayBuffer | undefined, buffer2: Uint8Array): Ui } export const generateDigest = async ( - stream: ReadableStream | null + stream: ReadableStream | null, + generator: (body: Uint8Array) => ArrayBuffer | Promise ): Promise => { if (!stream || !crypto || !crypto.subtle) { return null @@ -24,12 +25,7 @@ export const generateDigest = async ( break } - result = await crypto.subtle.digest( - { - name: 'SHA-1', - }, - mergeBuffers(result, value) - ) + result = await generator(mergeBuffers(result, value)) } if (!result) { diff --git a/src/middleware/etag/index.test.ts b/src/middleware/etag/index.test.ts index a1c67c608..296c74bb9 100644 --- a/src/middleware/etag/index.test.ts +++ b/src/middleware/etag/index.test.ts @@ -20,6 +20,39 @@ describe('Etag Middleware', () => { expect(res.headers.get('ETag')).toBe('"4515561204e8269cb4468d5b39288d8f2482dcfe"') }) + it('Should return etag header with another algorithm', async () => { + const app = new Hono() + app.use( + '/etag/*', + etag({ + generateDigest: (body) => + crypto.subtle.digest( + { + name: 'SHA-256', + }, + body + ), + }) + ) + app.get('/etag/abc', (c) => { + return c.text('Hono is cool') + }) + app.get('/etag/def', (c) => { + return c.json({ message: 'Hono is cool' }) + }) + let res = await app.request('http://localhost/etag/abc') + expect(res.headers.get('ETag')).not.toBeFalsy() + expect(res.headers.get('ETag')).toBe( + '"ee7e84f92c4f54fec768123ac23003a6eb8437db95bcfbfc35db477af1ccb49e"' + ) + + res = await app.request('http://localhost/etag/def') + expect(res.headers.get('ETag')).not.toBeFalsy() + expect(res.headers.get('ETag')).toBe( + '"6ae7438c67f07b60b2ab069dbce206b00d3528c690840a77e0222d37398a8547"' + ) + }) + it('Should return etag header - binary', async () => { const app = new Hono() app.use('/etag/*', etag()) diff --git a/src/middleware/etag/index.ts b/src/middleware/etag/index.ts index f8555583c..b8fb4ecaf 100644 --- a/src/middleware/etag/index.ts +++ b/src/middleware/etag/index.ts @@ -9,6 +9,7 @@ import { generateDigest } from './digest' type ETagOptions = { retainedHeaders?: string[] weak?: boolean + generateDigest?: (body: Uint8Array) => ArrayBuffer | Promise } /** @@ -38,6 +39,9 @@ function etagMatches(etag: string, ifNoneMatch: string | null) { * @param {ETagOptions} [options] - The options for the ETag middleware. * @param {boolean} [options.weak=false] - Define using or not using a weak validation. If true is set, then `W/` is added to the prefix of the value. * @param {string[]} [options.retainedHeaders=RETAINED_304_HEADERS] - The headers that you want to retain in the 304 Response. + * @param {function(Uint8Array): ArrayBuffer | Promise} [options.generateDigest] - + * A custom digest generation function. By default, it uses 'SHA-1' + * This function is called with the response body as a `Uint8Array` and should return a hash as an `ArrayBuffer` or a Promise of one. * @returns {MiddlewareHandler} The middleware handler function. * * @example @@ -53,6 +57,15 @@ function etagMatches(etag: string, ifNoneMatch: string | null) { export const etag = (options?: ETagOptions): MiddlewareHandler => { const retainedHeaders = options?.retainedHeaders ?? RETAINED_304_HEADERS const weak = options?.weak ?? false + const generator = + options?.generateDigest ?? + ((body: Uint8Array) => + crypto.subtle.digest( + { + name: 'SHA-1', + }, + body + )) return async function etag(c, next) { const ifNoneMatch = c.req.header('If-None-Match') ?? null @@ -63,7 +76,7 @@ export const etag = (options?: ETagOptions): MiddlewareHandler => { let etag = res.headers.get('ETag') if (!etag) { - const hash = await generateDigest(res.clone().body) + const hash = await generateDigest(res.clone().body, generator) if (hash === null) { return } @@ -71,7 +84,6 @@ export const etag = (options?: ETagOptions): MiddlewareHandler => { } if (etagMatches(etag, ifNoneMatch)) { - await c.res.blob() // Force using body c.res = new Response(null, { status: 304, statusText: 'Not Modified',