Skip to content

Commit 233fc2f

Browse files
authored
feat: use Netlify Durable Cache (#2510)
* test: add missing coverage for route handler headers * chore: allow e2e fixtures to override site id * feat: specify `durable` cache-control directive This is gated behind a feature flag for now. I can't link to any public docs yet, but by the time you're reading this you should be able to find a section on "Durable caching" at https://docs.netlify.com.
1 parent 3fea441 commit 233fc2f

File tree

7 files changed

+347
-37
lines changed

7 files changed

+347
-37
lines changed

src/run/handlers/server.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,9 @@ export default async (request: Request, context: FutureContext) => {
112112

113113
await adjustDateHeader({ headers: response.headers, request, span, tracer, requestContext })
114114

115-
setCacheControlHeaders(response.headers, request, requestContext)
115+
const useDurableCache =
116+
context.flags.get('serverless_functions_nextjs_durable_cache_disable') !== true
117+
setCacheControlHeaders(response.headers, request, requestContext, useDurableCache)
116118
setCacheTagsHeaders(response.headers, requestContext)
117119
setVaryHeaders(response.headers, request, nextConfig)
118120
setCacheStatusHeader(response.headers)

src/run/headers.test.ts

+236-19
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
55
import { type FixtureTestContext } from '../../tests/utils/contexts.js'
66
import { generateRandomObjectID, startMockBlobStore } from '../../tests/utils/helpers.js'
77

8-
import { createRequestContext } from './handlers/request-context.cjs'
8+
import { createRequestContext, type RequestContext } from './handlers/request-context.cjs'
99
import { setCacheControlHeaders, setVaryHeaders } from './headers.js'
1010

1111
beforeEach<FixtureTestContext>(async (ctx) => {
@@ -194,25 +194,242 @@ describe('headers', () => {
194194
describe('setCacheControlHeaders', () => {
195195
const defaultUrl = 'https://example.com'
196196

197+
describe('Durable Cache feature flag disabled', () => {
198+
test('should set permanent, non-durable "netlify-cdn-cache-control" if "cache-control" is not set and "requestContext.usedFsRead" is truthy', () => {
199+
const headers = new Headers()
200+
const request = new Request(defaultUrl)
201+
vi.spyOn(headers, 'set')
202+
203+
const requestContext = createRequestContext()
204+
requestContext.usedFsRead = true
205+
206+
setCacheControlHeaders(headers, request, requestContext, false)
207+
208+
expect(headers.set).toHaveBeenNthCalledWith(
209+
1,
210+
'cache-control',
211+
'public, max-age=0, must-revalidate',
212+
)
213+
expect(headers.set).toHaveBeenNthCalledWith(
214+
2,
215+
'netlify-cdn-cache-control',
216+
'max-age=31536000',
217+
)
218+
})
219+
220+
describe('route handler responses with a specified `revalidate` value', () => {
221+
test('should set non-durable SWC=1yr with 1yr TTL if "{netlify-,}cdn-cache-control" is not present and `revalidate` is `false` (GET)', () => {
222+
const headers = new Headers()
223+
const request = new Request(defaultUrl)
224+
vi.spyOn(headers, 'set')
225+
226+
const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: false }
227+
setCacheControlHeaders(headers, request, ctx, false)
228+
229+
expect(headers.set).toHaveBeenCalledTimes(1)
230+
expect(headers.set).toHaveBeenNthCalledWith(
231+
1,
232+
'netlify-cdn-cache-control',
233+
's-maxage=31536000, stale-while-revalidate=31536000',
234+
)
235+
})
236+
237+
test('should set non-durable SWC=1yr with 1yr TTL if "{netlify-,}cdn-cache-control" is not present and `revalidate` is `false` (HEAD)', () => {
238+
const headers = new Headers()
239+
const request = new Request(defaultUrl, { method: 'HEAD' })
240+
vi.spyOn(headers, 'set')
241+
242+
const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: false }
243+
setCacheControlHeaders(headers, request, ctx, false)
244+
245+
expect(headers.set).toHaveBeenCalledTimes(1)
246+
expect(headers.set).toHaveBeenNthCalledWith(
247+
1,
248+
'netlify-cdn-cache-control',
249+
's-maxage=31536000, stale-while-revalidate=31536000',
250+
)
251+
})
252+
253+
test('should set non-durable SWC=1yr with given TTL if "{netlify-,}cdn-cache-control" is not present and `revalidate` is a number (GET)', () => {
254+
const headers = new Headers()
255+
const request = new Request(defaultUrl)
256+
vi.spyOn(headers, 'set')
257+
258+
const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: 7200 }
259+
setCacheControlHeaders(headers, request, ctx, false)
260+
261+
expect(headers.set).toHaveBeenCalledTimes(1)
262+
expect(headers.set).toHaveBeenNthCalledWith(
263+
1,
264+
'netlify-cdn-cache-control',
265+
's-maxage=7200, stale-while-revalidate=31536000',
266+
)
267+
})
268+
269+
test('should set non-durable SWC=1yr with 1yr TTL if "{netlify-,}cdn-cache-control" is not present and `revalidate` is a number (HEAD)', () => {
270+
const headers = new Headers()
271+
const request = new Request(defaultUrl, { method: 'HEAD' })
272+
vi.spyOn(headers, 'set')
273+
274+
const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: 7200 }
275+
setCacheControlHeaders(headers, request, ctx, false)
276+
277+
expect(headers.set).toHaveBeenCalledTimes(1)
278+
expect(headers.set).toHaveBeenNthCalledWith(
279+
1,
280+
'netlify-cdn-cache-control',
281+
's-maxage=7200, stale-while-revalidate=31536000',
282+
)
283+
})
284+
})
285+
})
286+
287+
describe('route handler responses with a specified `revalidate` value', () => {
288+
test('should not set any headers if "cdn-cache-control" is present', () => {
289+
const givenHeaders = {
290+
'cdn-cache-control': 'public, max-age=0, must-revalidate',
291+
}
292+
const headers = new Headers(givenHeaders)
293+
const request = new Request(defaultUrl)
294+
vi.spyOn(headers, 'set')
295+
296+
const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: false }
297+
setCacheControlHeaders(headers, request, ctx, true)
298+
299+
expect(headers.set).toHaveBeenCalledTimes(0)
300+
})
301+
302+
test('should not set any headers if "netlify-cdn-cache-control" is present', () => {
303+
const givenHeaders = {
304+
'netlify-cdn-cache-control': 'public, max-age=0, must-revalidate',
305+
}
306+
const headers = new Headers(givenHeaders)
307+
const request = new Request(defaultUrl)
308+
vi.spyOn(headers, 'set')
309+
310+
const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: false }
311+
setCacheControlHeaders(headers, request, ctx, true)
312+
313+
expect(headers.set).toHaveBeenCalledTimes(0)
314+
})
315+
316+
test('should mark content as stale if "{netlify-,}cdn-cache-control" is not present and "x-nextjs-cache" is "STALE" (GET)', () => {
317+
const givenHeaders = {
318+
'x-nextjs-cache': 'STALE',
319+
}
320+
const headers = new Headers(givenHeaders)
321+
const request = new Request(defaultUrl)
322+
vi.spyOn(headers, 'set')
323+
324+
const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: false }
325+
setCacheControlHeaders(headers, request, ctx, true)
326+
327+
expect(headers.set).toHaveBeenCalledTimes(1)
328+
expect(headers.set).toHaveBeenNthCalledWith(
329+
1,
330+
'netlify-cdn-cache-control',
331+
'public, max-age=0, must-revalidate',
332+
)
333+
})
334+
335+
test('should mark content as stale if "{netlify-,}cdn-cache-control" is not present and "x-nextjs-cache" is "STALE" (HEAD)', () => {
336+
const givenHeaders = {
337+
'x-nextjs-cache': 'STALE',
338+
}
339+
const headers = new Headers(givenHeaders)
340+
const request = new Request(defaultUrl)
341+
vi.spyOn(headers, 'set')
342+
343+
const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: false }
344+
setCacheControlHeaders(headers, request, ctx, true)
345+
346+
expect(headers.set).toHaveBeenCalledTimes(1)
347+
expect(headers.set).toHaveBeenNthCalledWith(
348+
1,
349+
'netlify-cdn-cache-control',
350+
'public, max-age=0, must-revalidate',
351+
)
352+
})
353+
354+
test('should set durable SWC=1yr with 1yr TTL if "{netlify-,}cdn-cache-control" is not present and `revalidate` is `false` (HEAD)', () => {
355+
const headers = new Headers()
356+
const request = new Request(defaultUrl, { method: 'HEAD' })
357+
vi.spyOn(headers, 'set')
358+
359+
const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: false }
360+
setCacheControlHeaders(headers, request, ctx, true)
361+
362+
expect(headers.set).toHaveBeenCalledTimes(1)
363+
expect(headers.set).toHaveBeenNthCalledWith(
364+
1,
365+
'netlify-cdn-cache-control',
366+
's-maxage=31536000, stale-while-revalidate=31536000, durable',
367+
)
368+
})
369+
370+
test('should set durable SWC=1yr with given TTL if "{netlify-,}cdn-cache-control" is not present and `revalidate` is a number (GET)', () => {
371+
const headers = new Headers()
372+
const request = new Request(defaultUrl)
373+
vi.spyOn(headers, 'set')
374+
375+
const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: 7200 }
376+
setCacheControlHeaders(headers, request, ctx, true)
377+
378+
expect(headers.set).toHaveBeenCalledTimes(1)
379+
expect(headers.set).toHaveBeenNthCalledWith(
380+
1,
381+
'netlify-cdn-cache-control',
382+
's-maxage=7200, stale-while-revalidate=31536000, durable',
383+
)
384+
})
385+
386+
test('should set durable SWC=1yr with 1yr TTL if "{netlify-,}cdn-cache-control" is not present and `revalidate` is a number (HEAD)', () => {
387+
const headers = new Headers()
388+
const request = new Request(defaultUrl, { method: 'HEAD' })
389+
vi.spyOn(headers, 'set')
390+
391+
const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: 7200 }
392+
setCacheControlHeaders(headers, request, ctx, true)
393+
394+
expect(headers.set).toHaveBeenCalledTimes(1)
395+
expect(headers.set).toHaveBeenNthCalledWith(
396+
1,
397+
'netlify-cdn-cache-control',
398+
's-maxage=7200, stale-while-revalidate=31536000, durable',
399+
)
400+
})
401+
402+
test('should not set any headers on POST request', () => {
403+
const headers = new Headers()
404+
const request = new Request(defaultUrl, { method: 'POST' })
405+
vi.spyOn(headers, 'set')
406+
407+
const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: false }
408+
setCacheControlHeaders(headers, request, ctx, true)
409+
410+
expect(headers.set).toHaveBeenCalledTimes(0)
411+
})
412+
})
413+
197414
test('should not set any headers if "cache-control" is not set and "requestContext.usedFsRead" is not truthy', () => {
198415
const headers = new Headers()
199416
const request = new Request(defaultUrl)
200417
vi.spyOn(headers, 'set')
201418

202-
setCacheControlHeaders(headers, request, createRequestContext())
419+
setCacheControlHeaders(headers, request, createRequestContext(), true)
203420

204421
expect(headers.set).toHaveBeenCalledTimes(0)
205422
})
206423

207-
test('should set permanent "netlify-cdn-cache-control" if "cache-control" is not set and "requestContext.usedFsRead" is truthy', () => {
424+
test('should set permanent, durable "netlify-cdn-cache-control" if "cache-control" is not set and "requestContext.usedFsRead" is truthy', () => {
208425
const headers = new Headers()
209426
const request = new Request(defaultUrl)
210427
vi.spyOn(headers, 'set')
211428

212429
const requestContext = createRequestContext()
213430
requestContext.usedFsRead = true
214431

215-
setCacheControlHeaders(headers, request, requestContext)
432+
setCacheControlHeaders(headers, request, requestContext, true)
216433

217434
expect(headers.set).toHaveBeenNthCalledWith(
218435
1,
@@ -222,7 +439,7 @@ describe('headers', () => {
222439
expect(headers.set).toHaveBeenNthCalledWith(
223440
2,
224441
'netlify-cdn-cache-control',
225-
'max-age=31536000',
442+
'max-age=31536000, durable',
226443
)
227444
})
228445

@@ -235,7 +452,7 @@ describe('headers', () => {
235452
const request = new Request(defaultUrl)
236453
vi.spyOn(headers, 'set')
237454

238-
setCacheControlHeaders(headers, request, createRequestContext())
455+
setCacheControlHeaders(headers, request, createRequestContext(), true)
239456

240457
expect(headers.set).toHaveBeenCalledTimes(0)
241458
})
@@ -249,7 +466,7 @@ describe('headers', () => {
249466
const request = new Request(defaultUrl)
250467
vi.spyOn(headers, 'set')
251468

252-
setCacheControlHeaders(headers, request, createRequestContext())
469+
setCacheControlHeaders(headers, request, createRequestContext(), true)
253470

254471
expect(headers.set).toHaveBeenCalledTimes(0)
255472
})
@@ -262,7 +479,7 @@ describe('headers', () => {
262479
const request = new Request(defaultUrl)
263480
vi.spyOn(headers, 'set')
264481

265-
setCacheControlHeaders(headers, request, createRequestContext())
482+
setCacheControlHeaders(headers, request, createRequestContext(), true)
266483

267484
expect(headers.set).toHaveBeenNthCalledWith(
268485
1,
@@ -272,7 +489,7 @@ describe('headers', () => {
272489
expect(headers.set).toHaveBeenNthCalledWith(
273490
2,
274491
'netlify-cdn-cache-control',
275-
'public, max-age=0, must-revalidate',
492+
'public, max-age=0, must-revalidate, durable',
276493
)
277494
})
278495

@@ -284,7 +501,7 @@ describe('headers', () => {
284501
const request = new Request(defaultUrl, { method: 'HEAD' })
285502
vi.spyOn(headers, 'set')
286503

287-
setCacheControlHeaders(headers, request, createRequestContext())
504+
setCacheControlHeaders(headers, request, createRequestContext(), true)
288505

289506
expect(headers.set).toHaveBeenNthCalledWith(
290507
1,
@@ -294,7 +511,7 @@ describe('headers', () => {
294511
expect(headers.set).toHaveBeenNthCalledWith(
295512
2,
296513
'netlify-cdn-cache-control',
297-
'public, max-age=0, must-revalidate',
514+
'public, max-age=0, must-revalidate, durable',
298515
)
299516
})
300517

@@ -306,7 +523,7 @@ describe('headers', () => {
306523
const request = new Request(defaultUrl, { method: 'POST' })
307524
vi.spyOn(headers, 'set')
308525

309-
setCacheControlHeaders(headers, request, createRequestContext())
526+
setCacheControlHeaders(headers, request, createRequestContext(), true)
310527

311528
expect(headers.set).toHaveBeenCalledTimes(0)
312529
})
@@ -319,13 +536,13 @@ describe('headers', () => {
319536
const request = new Request(defaultUrl)
320537
vi.spyOn(headers, 'set')
321538

322-
setCacheControlHeaders(headers, request, createRequestContext())
539+
setCacheControlHeaders(headers, request, createRequestContext(), true)
323540

324541
expect(headers.set).toHaveBeenNthCalledWith(1, 'cache-control', 'public')
325542
expect(headers.set).toHaveBeenNthCalledWith(
326543
2,
327544
'netlify-cdn-cache-control',
328-
'public, s-maxage=604800',
545+
'public, s-maxage=604800, durable',
329546
)
330547
})
331548

@@ -337,25 +554,25 @@ describe('headers', () => {
337554
const request = new Request(defaultUrl)
338555
vi.spyOn(headers, 'set')
339556

340-
setCacheControlHeaders(headers, request, createRequestContext())
557+
setCacheControlHeaders(headers, request, createRequestContext(), true)
341558

342559
expect(headers.set).toHaveBeenNthCalledWith(1, 'cache-control', 'max-age=604800')
343560
expect(headers.set).toHaveBeenNthCalledWith(
344561
2,
345562
'netlify-cdn-cache-control',
346-
'max-age=604800, stale-while-revalidate=86400',
563+
'max-age=604800, stale-while-revalidate=86400, durable',
347564
)
348565
})
349566

350-
test('should set default "cache-control" header if it contains only "s-maxage" and "stale-whie-revalidate"', () => {
567+
test('should set default "cache-control" header if it contains only "s-maxage" and "stale-while-revalidate"', () => {
351568
const givenHeaders = {
352569
'cache-control': 's-maxage=604800, stale-while-revalidate=86400',
353570
}
354571
const headers = new Headers(givenHeaders)
355572
const request = new Request(defaultUrl)
356573
vi.spyOn(headers, 'set')
357574

358-
setCacheControlHeaders(headers, request, createRequestContext())
575+
setCacheControlHeaders(headers, request, createRequestContext(), true)
359576

360577
expect(headers.set).toHaveBeenNthCalledWith(
361578
1,
@@ -365,7 +582,7 @@ describe('headers', () => {
365582
expect(headers.set).toHaveBeenNthCalledWith(
366583
2,
367584
'netlify-cdn-cache-control',
368-
's-maxage=604800, stale-while-revalidate=86400',
585+
's-maxage=604800, stale-while-revalidate=86400, durable',
369586
)
370587
})
371588
})

0 commit comments

Comments
 (0)