@@ -97,24 +97,27 @@ function processHeadersForCacheKey(headers: HeadersInit | undefined): Record<str
9797
9898 if ( headers instanceof Headers ) {
9999 headers . forEach ( ( value , key ) => {
100+ const lowerKey = key . toLowerCase ( ) ;
100101 // Skip trace context headers
101- if ( key . toLowerCase ( ) !== 'traceparent' && key . toLowerCase ( ) !== 'tracestate' ) {
102- headerObj [ key ] = value ;
102+ if ( lowerKey !== 'traceparent' && lowerKey !== 'tracestate' ) {
103+ headerObj [ lowerKey ] = value ;
103104 }
104105 } ) ;
105106 } else if ( Array . isArray ( headers ) ) {
106107 headers . forEach ( ( [ key , value ] ) => {
108+ const lowerKey = key . toLowerCase ( ) ;
107109 // Skip trace context headers
108- if ( key . toLowerCase ( ) !== 'traceparent' && key . toLowerCase ( ) !== 'tracestate' ) {
109- headerObj [ key ] = value ;
110+ if ( lowerKey !== 'traceparent' && lowerKey !== 'tracestate' ) {
111+ headerObj [ lowerKey ] = value ;
110112 }
111113 } ) ;
112114 } else {
113115 // Plain object
114116 Object . entries ( headers ) . forEach ( ( [ key , value ] ) => {
117+ const lowerKey = key . toLowerCase ( ) ;
115118 // Skip trace context headers
116- if ( key . toLowerCase ( ) !== 'traceparent' && key . toLowerCase ( ) !== 'tracestate' ) {
117- headerObj [ key ] = value ;
119+ if ( lowerKey !== 'traceparent' && lowerKey !== 'tracestate' ) {
120+ headerObj [ lowerKey ] = value as string ;
118121 }
119122 } ) ;
120123 }
@@ -137,8 +140,8 @@ async function sha256(message: string): Promise<string> {
137140
138141 // Node.js runtime
139142 // @ts -ignore - crypto module is available in Node.js
140- const nodeCrypto = await import ( 'crypto' ) ;
141- return nodeCrypto . createHash ( 'sha256' ) . update ( message ) . digest ( 'hex' ) ;
143+ const { createHash } = await import ( 'crypto' ) ;
144+ return createHash ( 'sha256' ) . update ( message ) . digest ( 'hex' ) ;
142145}
143146
144147/**
@@ -147,14 +150,15 @@ async function sha256(message: string): Promise<string> {
147150async function generateCacheKey (
148151 input : RequestInfo | URL ,
149152 init ?: RequestInit ,
150- fetchCacheKeyPrefix ?: string
153+ fetchCacheKeyPrefix ?: string ,
154+ preprocessedBodyChunks ?: any [ ]
151155) : Promise < string > {
152156 // Extract URL and create Request object for consistent processing
153157 const url = typeof input === 'string' ? input : input instanceof URL ? input . toString ( ) : input . url ;
154158 const request = new Request ( url , init ) ;
155159
156160 // Process body
157- const { chunks : bodyChunks } = await processBodyForCacheKey ( init ?. body ) ;
161+ const bodyChunks = preprocessedBodyChunks ?? ( await processBodyForCacheKey ( init ?. body ) ) . chunks ;
158162
159163 // Process headers (removing trace context headers)
160164 const headers = processHeadersForCacheKey ( init ?. headers ) ;
@@ -211,10 +215,21 @@ function needsRevalidation(entry: CacheEntry): boolean {
211215async function responseToCache ( response : Response , options ?: CachedFetchOptions ) : Promise < CacheEntry > {
212216 const headers : Record < string , string > = { } ;
213217 response . headers . forEach ( ( value , key ) => {
214- headers [ key ] = value ;
218+ headers [ key . toLowerCase ( ) ] = value ;
215219 } ) ;
216220
217- const data = await response . text ( ) ;
221+ const contentType = response . headers . get ( 'content-type' ) || '' ;
222+ const shouldTreatAsText = / ^ ( t e x t \/ | a p p l i c a t i o n \/ ( j s o n | j a v a s c r i p t | x m l | x - w w w - f o r m - u r l e n c o d e d ) | i m a g e \/ s v g \+ x m l ) / i. test ( contentType ) ;
223+ let data : string ;
224+ let isBinary = false ;
225+ if ( shouldTreatAsText ) {
226+ data = await response . text ( ) ;
227+ } else {
228+ const buffer = await response . arrayBuffer ( ) ;
229+ const bytes = new Uint8Array ( buffer ) ;
230+ data = toBase64 ( bytes ) ;
231+ isBinary = true ;
232+ }
218233 const now = Date . now ( ) ;
219234
220235 // Calculate revalidation and expiry times
@@ -249,17 +264,32 @@ async function responseToCache(response: Response, options?: CachedFetchOptions)
249264 revalidateAfter,
250265 expiresAt,
251266 tags : options ?. next ?. tags ,
267+ isBinary,
268+ contentType : contentType || undefined ,
252269 } ;
253270}
254271
255272/**
256273 * Convert a cache entry back to a Response object
257274 */
258275function cacheToResponse ( entry : CacheEntry ) : Response {
259- return new Response ( entry . data , {
276+ const headers = new Headers ( entry . headers ) ;
277+ headers . delete ( 'content-length' ) ;
278+ if ( entry . contentType && ! headers . get ( 'content-type' ) ) {
279+ headers . set ( 'content-type' , entry . contentType ) ;
280+ }
281+ let body : BodyInit | null = null ;
282+ if ( ( entry as any ) . isBinary ) {
283+ const bytes = fromBase64 ( entry . data as string ) ;
284+ const ab = bytes . buffer . slice ( bytes . byteOffset , bytes . byteOffset + bytes . byteLength ) as unknown as ArrayBuffer ;
285+ body = ab ;
286+ } else {
287+ body = entry . data as string ;
288+ }
289+ return new Response ( body , {
260290 status : entry . status ,
261291 statusText : entry . statusText ,
262- headers : entry . headers ,
292+ headers,
263293 } ) ;
264294}
265295
@@ -284,6 +314,42 @@ function cleanFetchOptions(options?: CachedFetchOptions): RequestInit | undefine
284314 return cleanOptions ;
285315}
286316
317+ function toBase64 ( bytes : Uint8Array ) : string {
318+ // @ts -ignore - Buffer may be available in Node runtime
319+ if ( typeof Buffer !== 'undefined' ) {
320+ // @ts -ignore
321+ return Buffer . from ( bytes ) . toString ( 'base64' ) ;
322+ }
323+ let binary = '' ;
324+ const chunkSize = 0x8000 ;
325+ for ( let i = 0 ; i < bytes . length ; i += chunkSize ) {
326+ const chunk = bytes . subarray ( i , i + chunkSize ) ;
327+ binary += String . fromCharCode ( ...chunk ) ;
328+ }
329+ // @ts -ignore - btoa available in Edge/Web runtimes
330+ return btoa ( binary ) ;
331+ }
332+
333+ function fromBase64 ( b64 : string ) : Uint8Array {
334+ // @ts -ignore - Buffer may be available in Node runtime
335+ if ( typeof Buffer !== 'undefined' ) {
336+ // @ts -ignore
337+ return new Uint8Array ( Buffer . from ( b64 , 'base64' ) ) ;
338+ }
339+ // @ts -ignore - atob available in Edge/Web runtimes
340+ const binary = atob ( b64 ) ;
341+ const len = binary . length ;
342+ const bytes = new Uint8Array ( len ) ;
343+ for ( let i = 0 ; i < len ; i ++ ) bytes [ i ] = binary . charCodeAt ( i ) ;
344+ return bytes ;
345+ }
346+
347+ function computeTTL ( expiresAt ?: number ) : number {
348+ if ( ! expiresAt ) return 86400 ;
349+ const ttl = Math . floor ( ( expiresAt - Date . now ( ) ) / 1000 ) ;
350+ return Math . max ( 60 , ttl ) ;
351+ }
352+
287353/**
288354 * A fetch wrapper that uses Vercel Runtime Cache for caching
289355 * Mimics Next.js Data Cache API for use in edge middleware
@@ -293,19 +359,25 @@ export async function cachedFetch(
293359 init ?: CachedFetchOptions
294360) : Promise < Response > {
295361 const url = typeof input === 'string' ? input : input instanceof URL ? input . toString ( ) : input . url ;
296- const method = init ?. method || 'GET' ;
362+ const cleanOptions = cleanFetchOptions ( init ) || { } ;
363+ const method = ( cleanOptions . method ? String ( cleanOptions . method ) : 'GET' ) . toUpperCase ( ) ;
364+ cleanOptions . method = method ;
297365
298366 // Determine cache behavior
299367 const cacheOption = init ?. cache || 'auto no cache' ;
300368 const revalidate = init ?. next ?. revalidate ;
301369
302370 // Skip cache for no-store or revalidate: 0
303371 if ( cacheOption === 'no-store' || revalidate === 0 ) {
304- return fetch ( input , cleanFetchOptions ( init ) ) ;
372+ return fetch ( input , cleanOptions ) ;
305373 }
306374
307375 // Generate cache key
308- const cacheKey = await generateCacheKey ( input , cleanFetchOptions ( init ) , init ?. next ?. fetchCacheKeyPrefix ) ;
376+ const { chunks : bodyChunks , ogBody } = await processBodyForCacheKey ( init ?. body ) ;
377+ if ( ogBody !== undefined ) {
378+ cleanOptions . body = ogBody ;
379+ }
380+ const cacheKey = await generateCacheKey ( input , cleanOptions , init ?. next ?. fetchCacheKeyPrefix , bodyChunks ) ;
309381
310382 // Get Vercel Runtime Cache instance
311383 const cache = getCache ( ) ;
@@ -315,19 +387,23 @@ export async function cachedFetch(
315387 if ( cacheOption === 'force-cache' || cacheOption === 'auto no cache' ) {
316388 const cachedEntry = await cache . get ( cacheKey ) as CacheEntry | undefined ;
317389
318- if ( cachedEntry && ! isCacheEntryExpired ( cachedEntry ) ) {
390+ if (
391+ cachedEntry &&
392+ typeof cachedEntry . status === 'number' &&
393+ cachedEntry . data !== undefined &&
394+ cachedEntry . headers &&
395+ ! isCacheEntryExpired ( cachedEntry )
396+ ) {
319397 // Check if we need to revalidate in the background
320398 if ( needsRevalidation ( cachedEntry ) ) {
321399 // Return stale data immediately and refresh in background (SWR)
322400 const backgroundRefresh = async ( ) => {
323401 try {
324- const freshResponse = await fetch ( input , cleanFetchOptions ( init ) ) ;
402+ const freshResponse = await fetch ( input , cleanOptions ) ;
325403
326404 if ( freshResponse . ok && ( method === 'GET' || method === 'POST' || method === 'PUT' ) ) {
327405 const freshCacheEntry = await responseToCache ( freshResponse . clone ( ) , init ) ;
328- const cacheTTL = freshCacheEntry . expiresAt
329- ? Math . floor ( ( freshCacheEntry . expiresAt - Date . now ( ) ) / 1000 )
330- : 86400 ; // Default 24 hours
406+ const cacheTTL = computeTTL ( freshCacheEntry . expiresAt ) ;
331407 await cache . set ( cacheKey , freshCacheEntry , { ttl : cacheTTL } ) ;
332408 }
333409 } catch ( error ) {
@@ -350,16 +426,14 @@ export async function cachedFetch(
350426 }
351427
352428 // Fetch from origin (cache miss or expired)
353- const response = await fetch ( input , cleanFetchOptions ( init ) ) ;
429+ const response = await fetch ( input , cleanOptions ) ;
354430
355431 // Only cache successful responses (2xx) and GET/POST/PUT requests
356432 if ( response . ok && ( method === 'GET' || method === 'POST' || method === 'PUT' ) ) {
357433 const cacheEntry = await responseToCache ( response . clone ( ) , init ) ;
358434
359435 // Store in cache with appropriate TTL
360- const cacheTTL = cacheEntry . expiresAt
361- ? Math . floor ( ( cacheEntry . expiresAt - Date . now ( ) ) / 1000 )
362- : 86400 ; // Default 24 hours
436+ const cacheTTL = computeTTL ( cacheEntry . expiresAt ) ;
363437
364438 cache . set ( cacheKey , cacheEntry , { ttl : cacheTTL } ) . catch ( ( error : unknown ) => {
365439 console . error ( '[cached-middleware-fetch] Failed to cache response:' , error ) ;
@@ -370,7 +444,7 @@ export async function cachedFetch(
370444 } catch ( error ) {
371445 // If cache operations fail, fallback to regular fetch
372446 console . error ( '[cached-middleware-fetch] Cache operation failed:' , error ) ;
373- return fetch ( input , cleanFetchOptions ( init ) ) ;
447+ return fetch ( input , cleanOptions ) ;
374448 }
375449}
376450
0 commit comments