@@ -87,6 +87,15 @@ type KeyGenerator = (ctx: Context<any>) => string;
8787 */
8888type ShouldCacheFunction = ( ctx : Context < any > , res : Response ) => boolean ;
8989
90+ /**
91+ * Function signature for determining if ETag should be generated
92+ * @callback ShouldGenerateETagFunction
93+ * @param {Context<any> } ctx - The request context
94+ * @param {Response } res - The response
95+ * @returns {boolean } True if ETag should be generated
96+ */
97+ type ShouldGenerateETagFunction = ( ctx : Context < any > , res : Response ) => boolean ;
98+
9099/**
91100 * Cache middleware configuration options
92101 *
@@ -183,6 +192,30 @@ export interface CacheConfig {
183192 * @see getAvailableHashAlgorithms() for available algorithms
184193 */
185194 hashAlgorithm ?: string ;
195+
196+ /**
197+ * Whether to generate ETags for responses
198+ * @default true
199+ */
200+ generateETags ?: boolean ;
201+
202+ /**
203+ * Function to determine if ETag should be generated for a specific response
204+ * @default Smart function that skips large files and certain content types
205+ */
206+ shouldGenerateETag ?: ShouldGenerateETagFunction ;
207+
208+ /**
209+ * Maximum body size for ETag generation (in bytes)
210+ * @default 1048576 (1MB)
211+ */
212+ maxETagBodySize ?: number ;
213+
214+ /**
215+ * Content types to skip ETag generation for
216+ * @default ['image/', 'video/', 'audio/', 'application/octet-stream', 'application/zip', 'application/pdf']
217+ */
218+ skipETagContentTypes ?: string [ ] ;
186219}
187220
188221/**
@@ -548,6 +581,51 @@ function matchesPath(path: string, patterns: (string | RegExp)[]): boolean {
548581 } ) ;
549582}
550583
584+ /**
585+ * Default function to determine if ETag should be generated
586+ * @param {Context<any> } ctx - The request context
587+ * @param {Response } res - The response
588+ * @param {number } maxBodySize - Maximum body size for ETag generation
589+ * @param {string[] } skipContentTypes - Content types to skip
590+ * @returns {boolean } True if ETag should be generated
591+ */
592+ function defaultShouldGenerateETag ( ctx : Context < any > , res : Response , maxBodySize : number , skipContentTypes : string [ ] ) : boolean {
593+ // Skip if already has ETag
594+ if ( res . headers . get ( "etag" ) ) {
595+ return false ;
596+ }
597+
598+ // Check content type
599+ const contentType = res . headers . get ( "content-type" ) ?. toLowerCase ( ) || "" ;
600+ for ( const skipType of skipContentTypes ) {
601+ if ( contentType . includes ( skipType . toLowerCase ( ) ) ) {
602+ return false ;
603+ }
604+ }
605+
606+ // Check content length if available
607+ const contentLength = res . headers . get ( "content-length" ) ;
608+ if ( contentLength ) {
609+ const size = parseInt ( contentLength , 10 ) ;
610+ if ( ! isNaN ( size ) && size > maxBodySize ) {
611+ return false ;
612+ }
613+ }
614+
615+ // Check if it's a range request
616+ if ( res . status === 206 || ctx . req . headers . get ( "range" ) ) {
617+ return false ;
618+ }
619+
620+ // Check if response is streaming
621+ const transferEncoding = res . headers . get ( "transfer-encoding" ) ;
622+ if ( transferEncoding && transferEncoding . includes ( "chunked" ) ) {
623+ return false ;
624+ }
625+
626+ return true ;
627+ }
628+
551629/**
552630 * Cache middleware factory
553631 *
@@ -576,6 +654,22 @@ function matchesPath(path: string, patterns: (string | RegExp)[]): boolean {
576654 * staleWhileRevalidate: true,
577655 * maxStaleAge: 3600 // 1 hour
578656 * }));
657+ *
658+ * // Disable ETags for large files
659+ * app.use(cache({
660+ * generateETags: true,
661+ * maxETagBodySize: 512 * 1024, // 512KB
662+ * skipETagContentTypes: ['image/', 'video/', 'audio/']
663+ * }));
664+ *
665+ * // Custom ETag logic
666+ * app.use(cache({
667+ * shouldGenerateETag: (ctx, res) => {
668+ * // Only generate ETags for JSON responses
669+ * const contentType = res.headers.get('content-type') || '';
670+ * return contentType.includes('application/json');
671+ * }
672+ * }));
579673 * ```
580674 */
581675export function cache < T extends Record < string , unknown > = Record < string , unknown > , B extends Record < string , unknown > = Record < string , unknown > > (
@@ -598,6 +692,10 @@ export function cache<T extends Record<string, unknown> = Record<string, unknown
598692 staleWhileRevalidate : false ,
599693 maxStaleAge : 86400 ,
600694 hashAlgorithm : "blake2b512" ,
695+ generateETags : true ,
696+ shouldGenerateETag : ( ctx : Context < any > , res : Response ) => defaultShouldGenerateETag ( ctx , res , options . maxETagBodySize , options . skipETagContentTypes ) ,
697+ maxETagBodySize : 1048576 , // 1MB
698+ skipETagContentTypes : [ "image/" , "video/" , "audio/" , "application/octet-stream" , "application/zip" , "application/pdf" , "application/x-" , "font/" ] ,
601699 ...config ,
602700 } ;
603701
@@ -635,11 +733,7 @@ export function cache<T extends Record<string, unknown> = Record<string, unknown
635733 const cloned = response . clone ( ) ;
636734 const body = await cloned . text ( ) ;
637735
638- // Generate ETag if not present
639- let etag = response . headers . get ( "etag" ) ;
640- if ( ! etag ) {
641- etag = await generateETag ( body , options . hashAlgorithm ) ;
642- }
736+ const etag = response . headers . get ( "etag" ) || undefined ;
643737
644738 // Extract headers
645739 const headers : Record < string , string > = { } ;
@@ -828,34 +922,39 @@ export function cache<T extends Record<string, unknown> = Record<string, unknown
828922 response . headers . set ( options . cacheHeaderName , "MISS" ) ;
829923 }
830924
831- // Generate and set ETag if not present
832- if ( options . shouldCache ( ctx , response ) && ! response . headers . get ( "etag" ) ) {
833- // Clone response to read body for ETag generation
834- const cloned = response . clone ( ) ;
835- const body = await cloned . text ( ) ;
836- const etag = await generateETag ( body , options . hashAlgorithm ) ;
837-
838- // Create new response with ETag header
839- const headers = new Headers ( response . headers ) ;
840- headers . set ( "etag" , etag ) ;
841-
842- // Store in background (don't block response)
843- const responseWithEtag = new Response ( body , {
844- status : response . status ,
845- statusText : response . statusText ,
846- headers,
847- } ) ;
848-
849- // Cache the response with ETag
850- storeCachedResponse ( cacheKey , responseWithEtag ) . catch ( ( ) => {
851- // Ignore storage errors
852- } ) ;
853-
854- return responseWithEtag ;
855- }
856-
857- // Cache the original response if it already has an ETag
925+ // Check if we should cache this response
858926 if ( options . shouldCache ( ctx , response ) ) {
927+ // Check if we should generate ETag
928+ if ( options . generateETags && options . shouldGenerateETag && options . shouldGenerateETag ( ctx , response ) ) {
929+ // Clone response to read body for ETag generation
930+ const cloned = response . clone ( ) ;
931+ const body = await cloned . text ( ) ;
932+
933+ // Only generate ETag if body is within size limit
934+ if ( body . length <= options . maxETagBodySize ) {
935+ const etag = await generateETag ( body , options . hashAlgorithm ) ;
936+
937+ // Create new response with ETag header
938+ const headers = new Headers ( response . headers ) ;
939+ headers . set ( "etag" , etag ) ;
940+
941+ // Store in background (don't block response)
942+ const responseWithEtag = new Response ( body , {
943+ status : response . status ,
944+ statusText : response . statusText ,
945+ headers,
946+ } ) ;
947+
948+ // Cache the response with ETag
949+ storeCachedResponse ( cacheKey , responseWithEtag ) . catch ( ( ) => {
950+ // Ignore storage errors
951+ } ) ;
952+
953+ return responseWithEtag ;
954+ }
955+ }
956+
957+ // Cache the original response without ETag generation
859958 storeCachedResponse ( cacheKey , response . clone ( ) ) . catch ( ( ) => {
860959 // Ignore storage errors
861960 } ) ;
0 commit comments