Skip to content

fix: fix cache analysis not rendering when edge cache miss occurs #92

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ const getServedBySource = (
// 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 @@ const getServedBySource = (
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
// 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
)
const hasSubsequentCacheHits = cacheStatus.some(
entry => entry.cacheName !== 'Netlify Edge' && entry.parameters.hit
)

if (netlifyEdgeMiss && !hasSubsequentCacheHits) {
return ServedBySource.CdnOrigin
}

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