Skip to content

Commit

Permalink
feat(etag): allow for custom hashing methods to be used to etag (#3832)
Browse files Browse the repository at this point in the history
* feat(etag): allow for custom hashing methods to be used to etag

* chore: add jsdoc

* fix jsdoc

* cleanup

* fix: remove unneed code

* fix arg name

* fix
  • Loading branch information
EdamAme-x authored Feb 6, 2025
1 parent 6837649 commit 438983b
Show file tree
Hide file tree
Showing 3 changed files with 50 additions and 9 deletions.
10 changes: 3 additions & 7 deletions src/middleware/etag/digest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ const mergeBuffers = (buffer1: ArrayBuffer | undefined, buffer2: Uint8Array): Ui
}

export const generateDigest = async (
stream: ReadableStream<Uint8Array> | null
stream: ReadableStream<Uint8Array> | null,
generator: (body: Uint8Array) => ArrayBuffer | Promise<ArrayBuffer>
): Promise<string | null> => {
if (!stream || !crypto || !crypto.subtle) {
return null
Expand All @@ -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) {
Expand Down
33 changes: 33 additions & 0 deletions src/middleware/etag/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
16 changes: 14 additions & 2 deletions src/middleware/etag/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { generateDigest } from './digest'
type ETagOptions = {
retainedHeaders?: string[]
weak?: boolean
generateDigest?: (body: Uint8Array) => ArrayBuffer | Promise<ArrayBuffer>
}

/**
Expand Down Expand Up @@ -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<ArrayBuffer>} [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
Expand All @@ -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
Expand All @@ -63,15 +76,14 @@ 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
}
etag = weak ? `W/"${hash}"` : `"${hash}"`
}

if (etagMatches(etag, ifNoneMatch)) {
await c.res.blob() // Force using body
c.res = new Response(null, {
status: 304,
statusText: 'Not Modified',
Expand Down

0 comments on commit 438983b

Please sign in to comment.