Skip to content
19 changes: 18 additions & 1 deletion app/utils/getCacheAnalysis.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe('getCacheAnalysis', () => {
expect(result).toHaveProperty('servedBy')
expect(result).toHaveProperty('cacheStatus')
expect(result).toHaveProperty('cacheControl')
expect(result.servedBy.source).toBe(ServedBySource.CDN)
expect(result.servedBy.source).toBe(ServedBySource.CdnEdge)
})

it('integrates Cache-Status parsing correctly', () => {
Expand Down Expand Up @@ -66,6 +66,23 @@ describe('getCacheAnalysis', () => {

expect(() => getCacheAnalysis(headers, now)).toThrow('Could not determine who served the request')
})

it('returns CdnOrigin when Netlify Edge has a cache miss', () => {
const headers = {
'cache-status': '"Netlify Edge"; fwd=miss',
'debug-x-bb-host-id': 'cdn-glo-aws-cmh-57',
}
const now = Date.now()

const result = getCacheAnalysis(headers, now)

expect(result.servedBy.source).toBe(ServedBySource.CdnOrigin)
expect(result.servedBy.cdnNodes).toBe('cdn-glo-aws-cmh-57')
expect(result.cacheStatus).toHaveLength(1)
expect(result.cacheStatus[0]?.cacheName).toBe('Netlify Edge')
expect(result.cacheStatus[0]?.parameters.hit).toBe(false)
expect(result.cacheStatus[0]?.parameters.fwd).toBe('miss')
})
})

describe('parseCacheStatus', () => {
Expand Down
135 changes: 127 additions & 8 deletions app/utils/getServedBy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { describe, it, expect } from 'vitest'
import { getServedBy, ServedBySource, type ParsedCacheStatusEntry } from './getServedBy'

describe('getServedBy', () => {
it('returns CDN when Netlify Edge cache has a hit', () => {
it('returns CdnEdge when Netlify Edge cache has a hit', () => {
const headers = new Headers({
'Debug-X-BB-Host-Id': 'node1.example.com',
})
Expand All @@ -20,11 +20,11 @@ describe('getServedBy', () => {

const result = getServedBy(headers, cacheStatus)

expect(result.source).toBe(ServedBySource.CDN)
expect(result.source).toBe(ServedBySource.CdnEdge)
expect(result.cdnNodes).toBe('node1.example.com')
})

it('prioritizes CDN hit over durable cache hit when both are present', () => {
it('prioritizes CdnEdge hit over durable cache hit when both are present', () => {
const headers = new Headers({
'Debug-X-BB-Host-Id': 'node1.example.com',
})
Expand All @@ -49,14 +49,23 @@ describe('getServedBy', () => {

const result = getServedBy(headers, cacheStatus)

expect(result.source).toBe(ServedBySource.CDN)
expect(result.source).toBe(ServedBySource.CdnEdge)
})

it('returns DurableCache when Netlify Durable cache has a hit', () => {
const headers = new Headers({
'Debug-X-BB-Host-Id': 'node1.example.com',
})
const cacheStatus: ParsedCacheStatusEntry[] = [
{
cacheName: 'Netlify Edge',
parameters: {
hit: false,
fwd: 'miss',
stored: false,
collapsed: false,
},
},
{
cacheName: 'Netlify Durable',
parameters: {
Expand All @@ -77,7 +86,26 @@ describe('getServedBy', () => {
'Debug-X-NF-Function-Type': 'edge',
'Debug-X-BB-Host-Id': 'node1.example.com',
})
const cacheStatus: ParsedCacheStatusEntry[] = []
const cacheStatus: ParsedCacheStatusEntry[] = [
{
cacheName: 'Netlify Edge',
parameters: {
hit: false,
fwd: 'miss',
stored: false,
collapsed: false,
},
},
{
cacheName: 'Netlify Durable',
parameters: {
hit: false,
fwd: 'miss',
stored: false,
collapsed: false,
},
},
]

const result = getServedBy(headers, cacheStatus)

Expand All @@ -90,7 +118,26 @@ describe('getServedBy', () => {
'Debug-X-NF-Edge-Functions': 'middleware',
'Debug-X-BB-Host-Id': 'node1.example.com',
})
const cacheStatus: ParsedCacheStatusEntry[] = []
const cacheStatus: ParsedCacheStatusEntry[] = [
{
cacheName: 'Netlify Edge',
parameters: {
hit: false,
fwd: 'miss',
stored: false,
collapsed: false,
},
},
{
cacheName: 'Netlify Durable',
parameters: {
hit: false,
fwd: 'miss',
stored: false,
collapsed: false,
},
},
]

const result = getServedBy(headers, cacheStatus)

Expand All @@ -102,7 +149,26 @@ describe('getServedBy', () => {
'Debug-X-NF-Edge-Functions': 'middleware',
'Debug-X-BB-Host-Id': 'node1.example.com',
})
const cacheStatus: ParsedCacheStatusEntry[] = []
const cacheStatus: ParsedCacheStatusEntry[] = [
{
cacheName: 'Netlify Edge',
parameters: {
hit: false,
fwd: 'miss',
stored: false,
collapsed: false,
},
},
{
cacheName: 'Netlify Durable',
parameters: {
hit: false,
fwd: 'miss',
stored: false,
collapsed: false,
},
},
]

const result = getServedBy(headers, cacheStatus)

Expand Down Expand Up @@ -156,7 +222,60 @@ describe('getServedBy', () => {
)
})

it('ignores cache entries without hits', () => {
it('returns CdnOrigin when Netlify Edge cache has a miss', () => {
const headers = new Headers({
'Debug-X-BB-Host-Id': 'node1.example.com',
})
const cacheStatus: ParsedCacheStatusEntry[] = [
{
cacheName: 'Netlify Edge',
parameters: {
hit: false,
fwd: 'miss',
stored: false,
collapsed: false,
},
},
]
Comment on lines +229 to +239
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this one is realistic, but while we're refactoring the logic let's update the fixture in the other tests to be realistic. for example the durable cache one should have a Netlify Edge miss, the function one should have Netlify Edge and Durable Cache misses, etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated test fixtures to be realistic! The durable cache test now includes a Netlify Edge miss, and the function/edge function tests include both Netlify Edge and Netlify Durable misses. (fc87dc2)


const result = getServedBy(headers, cacheStatus)

expect(result.source).toBe(ServedBySource.CdnOrigin)
expect(result.cdnNodes).toBe('node1.example.com')
})

it('returns CdnOrigin when Netlify Edge miss and Netlify Durable miss (no cache hits)', () => {
const headers = new Headers({
'Debug-X-BB-Host-Id': 'node1.example.com',
})
const cacheStatus: ParsedCacheStatusEntry[] = [
{
cacheName: 'Netlify Edge',
parameters: {
hit: false,
fwd: 'miss',
stored: false,
collapsed: false,
},
},
{
cacheName: 'Netlify Durable',
parameters: {
hit: false,
fwd: 'miss',
stored: false,
collapsed: false,
},
},
]

const result = getServedBy(headers, cacheStatus)

expect(result.source).toBe(ServedBySource.CdnOrigin)
expect(result.cdnNodes).toBe('node1.example.com')
})

it('ignores cache entries without hits and picks first with hit', () => {
const headers = new Headers({
'Debug-X-BB-Host-Id': 'node1.example.com',
})
Expand Down
22 changes: 20 additions & 2 deletions app/utils/getServedBy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export enum ServedBySource {
CDN = 'CDN',
CdnEdge = 'CDN edge',
CdnOrigin = 'CDN origin',
DurableCache = 'Durable Cache',
Function = 'Function',
EdgeFunction = 'Edge Function',
Expand Down Expand Up @@ -40,13 +41,15 @@
// So, the first cache hit (starting from the user) is the one that served the request.
// But we don't quite want to return exactly the same concept of "caches" as in `Cache-Status`, so
// we need a bit of extra logic to map to other sources.

// First, check for cache hits
for (const {
cacheName,
parameters: { hit },
} of cacheStatus) {
if (!hit) continue

if (cacheName === 'Netlify Edge') return ServedBySource.CDN
if (cacheName === 'Netlify Edge') return ServedBySource.CdnEdge
if (cacheName === 'Netlify Durable') return ServedBySource.DurableCache
}

Expand All @@ -58,6 +61,21 @@
if (cacheHeaders.has('Debug-X-NF-Edge-Functions'))
return ServedBySource.EdgeFunction

// Check for the specific case of Netlify Edge miss with no subsequent cache hits - this handles

Check failure on line 64 in app/utils/getServedBy.ts

View workflow job for this annotation

GitHub Actions / lint

Trailing spaces not allowed
// the weird Netlify Cache-Status behavior where a miss on the CDN edge means the request was
// forwarded to CDN origin. According to Netlify's cache behavior, when there's a miss
// on "Netlify Edge" and no hits in subsequent caches, the request gets served by the CDN origin.
const netlifyEdgeMiss = cacheStatus.find(
entry => entry.cacheName === 'Netlify Edge' && !entry.parameters.hit

Check failure on line 69 in app/utils/getServedBy.ts

View workflow job for this annotation

GitHub Actions / lint

Missing trailing comma
)
const hasSubsequentCacheHits = cacheStatus.some(
entry => entry.cacheName !== 'Netlify Edge' && entry.parameters.hit

Check failure on line 72 in app/utils/getServedBy.ts

View workflow job for this annotation

GitHub Actions / lint

Missing trailing comma
)

Check failure on line 74 in app/utils/getServedBy.ts

View workflow job for this annotation

GitHub Actions / lint

Trailing spaces not allowed
if (netlifyEdgeMiss && !hasSubsequentCacheHits) {
return ServedBySource.CdnOrigin
}

throw new Error(
`Could not determine who served the request. Cache status: ${cacheStatus}`,
)
Expand Down
Loading