Skip to content

Commit 863b1c1

Browse files
authored
Merge pull request #8 from bookernath/copilot/fix-f58ddaef-ff45-4ac4-83d2-a0a51538adae
Add verbose logging controlled by CACHED_MIDDLEWARE_FETCH_LOGGER environment variable
2 parents 174c577 + be3d31c commit 863b1c1

2 files changed

Lines changed: 95 additions & 0 deletions

File tree

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,46 @@ const response = await fetch('https://api.example.com/data', {
265265
});
266266
```
267267

268+
## Debugging and Verbose Logging
269+
270+
Enable detailed logging to understand caching behavior and troubleshoot issues by setting the `CACHED_MIDDLEWARE_FETCH_LOGGER` environment variable:
271+
272+
```bash
273+
export CACHED_MIDDLEWARE_FETCH_LOGGER=1
274+
```
275+
276+
When enabled, you'll see detailed logs for all caching operations:
277+
278+
```typescript
279+
// With verbose logging enabled
280+
const response = await cachedFetch('https://api.example.com/data', {
281+
next: { revalidate: 300, tags: ['api-data'] }
282+
});
283+
284+
// Console output:
285+
// [cached-middleware-fetch] Starting cachedFetch: GET https://api.example.com/data {
286+
// cacheOption: 'auto no cache',
287+
// revalidate: 300,
288+
// expires: undefined,
289+
// tags: ['api-data'],
290+
// fetchCacheKeyPrefix: undefined
291+
// }
292+
// [cached-middleware-fetch] Generated cache key: a1b2c3d4e5f6...
293+
// [cached-middleware-fetch] Cache HIT (age: 45s, expires in: 255s)
294+
```
295+
296+
**Logged Information:**
297+
- Request details (method, URL, cache options)
298+
- Cache key generation
299+
- Cache lookup results (HIT/MISS/STALE with timing)
300+
- Background refresh operations (SWR)
301+
- Cache storage operations with TTL
302+
- Fallback scenarios
303+
304+
**Environment Variable Values:**
305+
- `CACHED_MIDDLEWARE_FETCH_LOGGER=1` - Enable verbose logging
306+
- `CACHED_MIDDLEWARE_FETCH_LOGGER=0` or unset - Disable verbose logging (default)
307+
268308
## API Reference
269309

270310
### `cachedFetch(input, init?)`

src/index.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ import type { CachedFetchOptions, CacheEntry } from './types';
55
// Re-export types for convenience
66
export type { CachedFetchOptions, CacheEntry } from './types';
77

8+
/**
9+
* Verbose logger that only logs when CACHED_MIDDLEWARE_FETCH_LOGGER=1
10+
*/
11+
function verboseLog(...args: any[]): void {
12+
if (process.env.CACHED_MIDDLEWARE_FETCH_LOGGER === '1') {
13+
console.log('[cached-middleware-fetch]', ...args);
14+
}
15+
}
16+
817
/**
918
* Process body for cache key generation, matching Next.js behavior
1019
* Note: The body is consumed here for cache key generation only.
@@ -379,8 +388,17 @@ export async function cachedFetch(
379388
const cacheOption = init?.cache || 'auto no cache';
380389
const revalidate = init?.next?.revalidate;
381390

391+
verboseLog(`Starting cachedFetch: ${method} ${url}`, {
392+
cacheOption,
393+
revalidate,
394+
expires: init?.next?.expires,
395+
tags: init?.next?.tags,
396+
fetchCacheKeyPrefix: init?.next?.fetchCacheKeyPrefix
397+
});
398+
382399
// Skip cache for no-store or revalidate: 0
383400
if (cacheOption === 'no-store' || revalidate === 0) {
401+
verboseLog(`Skipping cache due to ${cacheOption === 'no-store' ? 'no-store' : 'revalidate: 0'}`);
384402
const response = await fetch(input, cleanOptions);
385403

386404
// Clone the response to avoid body consumption issues
@@ -395,6 +413,7 @@ export async function cachedFetch(
395413
responseWithCacheHeaders.headers.set('X-Cache-Status', 'MISS');
396414
responseWithCacheHeaders.headers.set('X-Cache-Age', '0');
397415

416+
verboseLog(`Response: ${response.status} ${response.statusText} (cache bypassed)`);
398417
return responseWithCacheHeaders;
399418
}
400419

@@ -405,12 +424,15 @@ export async function cachedFetch(
405424
}
406425
const cacheKey = await generateCacheKey(input, cleanOptions, init?.next?.fetchCacheKeyPrefix, bodyChunks);
407426

427+
verboseLog(`Generated cache key: ${cacheKey}`);
428+
408429
// Get Vercel Runtime Cache instance
409430
const cache = getCache();
410431

411432
try {
412433
// Try to get from cache first
413434
if (cacheOption === 'force-cache' || cacheOption === 'auto no cache') {
435+
verboseLog(`Looking up cache entry for key: ${cacheKey}`);
414436
const cachedEntry = await cache.get(cacheKey) as CacheEntry | undefined;
415437

416438
if (
@@ -420,18 +442,27 @@ export async function cachedFetch(
420442
cachedEntry.headers &&
421443
!isCacheEntryExpired(cachedEntry)
422444
) {
445+
const now = Date.now();
446+
const cacheAge = Math.floor((now - cachedEntry.timestamp) / 1000);
447+
const expiresIn = cachedEntry.expiresAt ? Math.floor((cachedEntry.expiresAt - now) / 1000) : 'never';
448+
423449
// Check if we need to revalidate in the background
424450
const isStale = needsRevalidation(cachedEntry);
425451
if (isStale) {
452+
verboseLog(`Cache STALE (age: ${cacheAge}s, expires in: ${expiresIn}s) - triggering background refresh`);
426453
// Return stale data immediately and refresh in background (SWR)
427454
const backgroundRefresh = async () => {
428455
try {
456+
verboseLog(`Background refresh started for: ${url}`);
429457
const freshResponse = await fetch(input, cleanOptions);
430458

431459
if (freshResponse.ok && (method === 'GET' || method === 'POST' || method === 'PUT')) {
432460
const freshCacheEntry = await responseToCache(freshResponse.clone(), init);
433461
const cacheTTL = computeTTL(freshCacheEntry.expiresAt);
434462
await cache.set(cacheKey, freshCacheEntry, { ttl: cacheTTL });
463+
verboseLog(`Background refresh completed and cached (TTL: ${cacheTTL}s)`);
464+
} else {
465+
verboseLog(`Background refresh completed but not cached (status: ${freshResponse.status}, method: ${method})`);
435466
}
436467
} catch (error) {
437468
console.error('[cached-middleware-fetch] Background refresh failed:', error);
@@ -441,24 +472,41 @@ export async function cachedFetch(
441472
// Use waitUntil to extend the lifetime of the request for background refresh
442473
if (typeof waitUntil === 'function') {
443474
waitUntil(backgroundRefresh());
475+
verboseLog(`Background refresh scheduled with waitUntil`);
444476
} else {
445477
// Fallback if waitUntil is not available (non-Vercel environment)
446478
backgroundRefresh().catch(() => {});
479+
verboseLog(`Background refresh scheduled as fire-and-forget (no waitUntil available)`);
447480
}
481+
} else {
482+
verboseLog(`Cache HIT (age: ${cacheAge}s, expires in: ${expiresIn}s)`);
448483
}
449484

450485
// Return cached response with appropriate cache status
451486
return cacheToResponse(cachedEntry, isStale ? 'STALE' : 'HIT');
487+
} else {
488+
if (!cachedEntry) {
489+
verboseLog(`Cache MISS - no entry found`);
490+
} else if (isCacheEntryExpired(cachedEntry)) {
491+
verboseLog(`Cache MISS - entry expired`);
492+
} else {
493+
verboseLog(`Cache MISS - entry invalid`);
494+
}
452495
}
496+
} else {
497+
verboseLog(`Skipping cache lookup due to cache option: ${cacheOption}`);
453498
}
454499

455500
// Fetch from origin (cache miss or expired)
501+
verboseLog(`Fetching from origin: ${method} ${url}`);
456502
const response = await fetch(input, cleanOptions);
457503

458504
// Clone the response first to avoid body consumption issues
459505
const responseForCaching = response.clone();
460506
const responseForReturn = response.clone();
461507

508+
verboseLog(`Origin response: ${response.status} ${response.statusText}`);
509+
462510
// Add cache status headers to indicate this was a miss
463511
const responseWithCacheHeaders = new Response(responseForReturn.body, {
464512
status: response.status,
@@ -470,25 +518,32 @@ export async function cachedFetch(
470518

471519
// Only cache successful responses (2xx) and GET/POST/PUT requests
472520
if (response.ok && (method === 'GET' || method === 'POST' || method === 'PUT')) {
521+
verboseLog(`Caching response (status: ${response.status}, method: ${method})`);
473522
const cacheEntry = await responseToCache(responseForCaching, init);
474523

475524
// Store in cache with appropriate TTL
476525
const cacheTTL = computeTTL(cacheEntry.expiresAt);
526+
verboseLog(`Storing in cache with TTL: ${cacheTTL}s, expires at: ${cacheEntry.expiresAt ? new Date(cacheEntry.expiresAt).toISOString() : 'never'}`);
477527

478528
cache.set(cacheKey, cacheEntry, { ttl: cacheTTL }).catch((error: unknown) => {
479529
console.error('[cached-middleware-fetch] Failed to cache response:', error);
480530
});
531+
} else {
532+
verboseLog(`Not caching response (status: ${response.status}, method: ${method}, ok: ${response.ok})`);
481533
}
482534

483535
return responseWithCacheHeaders;
484536
} catch (error) {
485537
// If cache operations fail, fallback to regular fetch
486538
console.error('[cached-middleware-fetch] Cache operation failed:', error);
539+
verboseLog(`Falling back to regular fetch due to cache error`);
487540
const fallbackResponse = await fetch(input, cleanOptions);
488541

489542
// Clone the response to avoid body consumption issues
490543
const fallbackResponseClone = fallbackResponse.clone();
491544

545+
verboseLog(`Fallback response: ${fallbackResponse.status} ${fallbackResponse.statusText}`);
546+
492547
// Add cache status headers to indicate this was a miss due to error
493548
const responseWithCacheHeaders = new Response(fallbackResponseClone.body, {
494549
status: fallbackResponse.status,

0 commit comments

Comments
 (0)