Files
SnowLuma/.pnpm-store/v11/files/38/b9b98b02285c5a6d29eb3a8e1776f8520e1f92d0d9c10820992edf370af77c02990cc82f726ea95b72ed9c3a992026aedf2feeabb25b662efba3b188cca6d8
T
TanTan 2a69b23b47
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
docs: 仓库链接指向本镜像仓库
2026-11-13 09:27:07 +08:00

78 lines
2.9 KiB
Plaintext

import { generateDigest } from "./digest.js";
//#region src/middleware/etag/index.ts
/**
* Default headers to pass through on 304 responses. From the spec:
* > The response must not contain a body and must include the headers that
* > would have been sent in an equivalent 200 OK response: Cache-Control,
* > Content-Location, Date, ETag, Expires, and Vary.
*/
const RETAINED_304_HEADERS = [
"cache-control",
"content-location",
"date",
"etag",
"expires",
"vary"
];
const stripWeak = (tag) => tag.replace(/^W\//, "");
function etagMatches(etag, ifNoneMatch) {
return ifNoneMatch != null && ifNoneMatch.split(",").some((t) => stripWeak(t.trim()) === stripWeak(etag));
}
function initializeGenerator(generator) {
if (!generator) {
if (crypto && crypto.subtle) generator = (body) => crypto.subtle.digest({ name: "SHA-1" }, body);
}
return generator;
}
/**
* ETag Middleware for Hono.
*
* @see {@link https://hono.dev/docs/middleware/builtin/etag}
*
* @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
* ```ts
* const app = new Hono()
*
* app.use('/etag/*', etag())
* app.get('/etag/abc', (c) => {
* return c.text('Hono is hot')
* })
* ```
*/
const etag = (options) => {
const retainedHeaders = new Set((options?.retainedHeaders ?? RETAINED_304_HEADERS).map((header) => header.toLowerCase()));
const weak = options?.weak ?? false;
const generator = initializeGenerator(options?.generateDigest);
return async function etag(c, next) {
const ifNoneMatch = c.req.header("If-None-Match") ?? null;
await next();
if (!(c.req.method === "GET" || c.req.method === "HEAD" || c.req.method === "QUERY") || !c.res.ok) return;
const res = c.res;
let etag = res.headers.get("ETag");
if (!etag) {
if (!generator) return;
const hash = await generateDigest(res.clone().body, generator);
if (hash === null) return;
etag = weak ? `W/"${hash}"` : `"${hash}"`;
}
if (ifNoneMatch === "*" || etagMatches(etag, ifNoneMatch)) {
c.res = new Response(null, {
status: 304,
statusText: "Not Modified",
headers: { ETag: etag }
});
for (const key of Array.from(c.res.headers.keys())) if (!retainedHeaders.has(key.toLowerCase())) c.res.headers.delete(key);
} else c.res.headers.set("ETag", etag);
};
};
//#endregion
export { RETAINED_304_HEADERS, etag };