Skip to content

Commit 75a601e

Browse files
committed
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 b53be90 commit 75a601e

File tree

3 files changed

+119
-23
lines changed

3 files changed

+119
-23
lines changed

src/run/handlers/server.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,10 @@ 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 = context.flags.get('serverless_functions_nextjs_durable_cache') as
116+
| boolean
117+
| undefined
118+
setCacheControlHeaders(response.headers, request, requestContext, useDurableCache)
116119
setCacheTagsHeaders(response.headers, requestContext)
117120
setVaryHeaders(response.headers, request, nextConfig)
118121
setCacheStatusHeader(response.headers)

src/run/headers.test.ts

+110-20
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,96 @@ 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+
197287
describe('route handler responses with a specified `revalidate` value', () => {
198288
test('should not set any headers if "cdn-cache-control" is present', () => {
199289
const givenHeaders = {
@@ -204,7 +294,7 @@ describe('headers', () => {
204294
vi.spyOn(headers, 'set')
205295

206296
const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: false }
207-
setCacheControlHeaders(headers, request, ctx)
297+
setCacheControlHeaders(headers, request, ctx, true)
208298

209299
expect(headers.set).toHaveBeenCalledTimes(0)
210300
})
@@ -218,7 +308,7 @@ describe('headers', () => {
218308
vi.spyOn(headers, 'set')
219309

220310
const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: false }
221-
setCacheControlHeaders(headers, request, ctx)
311+
setCacheControlHeaders(headers, request, ctx, true)
222312

223313
expect(headers.set).toHaveBeenCalledTimes(0)
224314
})
@@ -232,7 +322,7 @@ describe('headers', () => {
232322
vi.spyOn(headers, 'set')
233323

234324
const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: false }
235-
setCacheControlHeaders(headers, request, ctx)
325+
setCacheControlHeaders(headers, request, ctx, true)
236326

237327
expect(headers.set).toHaveBeenCalledTimes(1)
238328
expect(headers.set).toHaveBeenNthCalledWith(
@@ -251,7 +341,7 @@ describe('headers', () => {
251341
vi.spyOn(headers, 'set')
252342

253343
const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: false }
254-
setCacheControlHeaders(headers, request, ctx)
344+
setCacheControlHeaders(headers, request, ctx, true)
255345

256346
expect(headers.set).toHaveBeenCalledTimes(1)
257347
expect(headers.set).toHaveBeenNthCalledWith(
@@ -267,7 +357,7 @@ describe('headers', () => {
267357
vi.spyOn(headers, 'set')
268358

269359
const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: false }
270-
setCacheControlHeaders(headers, request, ctx)
360+
setCacheControlHeaders(headers, request, ctx, true)
271361

272362
expect(headers.set).toHaveBeenCalledTimes(1)
273363
expect(headers.set).toHaveBeenNthCalledWith(
@@ -283,7 +373,7 @@ describe('headers', () => {
283373
vi.spyOn(headers, 'set')
284374

285375
const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: 7200 }
286-
setCacheControlHeaders(headers, request, ctx)
376+
setCacheControlHeaders(headers, request, ctx, true)
287377

288378
expect(headers.set).toHaveBeenCalledTimes(1)
289379
expect(headers.set).toHaveBeenNthCalledWith(
@@ -299,7 +389,7 @@ describe('headers', () => {
299389
vi.spyOn(headers, 'set')
300390

301391
const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: 7200 }
302-
setCacheControlHeaders(headers, request, ctx)
392+
setCacheControlHeaders(headers, request, ctx, true)
303393

304394
expect(headers.set).toHaveBeenCalledTimes(1)
305395
expect(headers.set).toHaveBeenNthCalledWith(
@@ -315,7 +405,7 @@ describe('headers', () => {
315405
vi.spyOn(headers, 'set')
316406

317407
const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: false }
318-
setCacheControlHeaders(headers, request, ctx)
408+
setCacheControlHeaders(headers, request, ctx, true)
319409

320410
expect(headers.set).toHaveBeenCalledTimes(0)
321411
})
@@ -326,20 +416,20 @@ describe('headers', () => {
326416
const request = new Request(defaultUrl)
327417
vi.spyOn(headers, 'set')
328418

329-
setCacheControlHeaders(headers, request, createRequestContext())
419+
setCacheControlHeaders(headers, request, createRequestContext(), true)
330420

331421
expect(headers.set).toHaveBeenCalledTimes(0)
332422
})
333423

334-
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', () => {
335425
const headers = new Headers()
336426
const request = new Request(defaultUrl)
337427
vi.spyOn(headers, 'set')
338428

339429
const requestContext = createRequestContext()
340430
requestContext.usedFsRead = true
341431

342-
setCacheControlHeaders(headers, request, requestContext)
432+
setCacheControlHeaders(headers, request, requestContext, true)
343433

344434
expect(headers.set).toHaveBeenNthCalledWith(
345435
1,
@@ -349,7 +439,7 @@ describe('headers', () => {
349439
expect(headers.set).toHaveBeenNthCalledWith(
350440
2,
351441
'netlify-cdn-cache-control',
352-
'max-age=31536000',
442+
'max-age=31536000, durable',
353443
)
354444
})
355445

@@ -362,7 +452,7 @@ describe('headers', () => {
362452
const request = new Request(defaultUrl)
363453
vi.spyOn(headers, 'set')
364454

365-
setCacheControlHeaders(headers, request, createRequestContext())
455+
setCacheControlHeaders(headers, request, createRequestContext(), true)
366456

367457
expect(headers.set).toHaveBeenCalledTimes(0)
368458
})
@@ -376,7 +466,7 @@ describe('headers', () => {
376466
const request = new Request(defaultUrl)
377467
vi.spyOn(headers, 'set')
378468

379-
setCacheControlHeaders(headers, request, createRequestContext())
469+
setCacheControlHeaders(headers, request, createRequestContext(), true)
380470

381471
expect(headers.set).toHaveBeenCalledTimes(0)
382472
})
@@ -389,7 +479,7 @@ describe('headers', () => {
389479
const request = new Request(defaultUrl)
390480
vi.spyOn(headers, 'set')
391481

392-
setCacheControlHeaders(headers, request, createRequestContext())
482+
setCacheControlHeaders(headers, request, createRequestContext(), true)
393483

394484
expect(headers.set).toHaveBeenNthCalledWith(
395485
1,
@@ -411,7 +501,7 @@ describe('headers', () => {
411501
const request = new Request(defaultUrl, { method: 'HEAD' })
412502
vi.spyOn(headers, 'set')
413503

414-
setCacheControlHeaders(headers, request, createRequestContext())
504+
setCacheControlHeaders(headers, request, createRequestContext(), true)
415505

416506
expect(headers.set).toHaveBeenNthCalledWith(
417507
1,
@@ -433,7 +523,7 @@ describe('headers', () => {
433523
const request = new Request(defaultUrl, { method: 'POST' })
434524
vi.spyOn(headers, 'set')
435525

436-
setCacheControlHeaders(headers, request, createRequestContext())
526+
setCacheControlHeaders(headers, request, createRequestContext(), true)
437527

438528
expect(headers.set).toHaveBeenCalledTimes(0)
439529
})
@@ -446,7 +536,7 @@ describe('headers', () => {
446536
const request = new Request(defaultUrl)
447537
vi.spyOn(headers, 'set')
448538

449-
setCacheControlHeaders(headers, request, createRequestContext())
539+
setCacheControlHeaders(headers, request, createRequestContext(), true)
450540

451541
expect(headers.set).toHaveBeenNthCalledWith(1, 'cache-control', 'public')
452542
expect(headers.set).toHaveBeenNthCalledWith(
@@ -464,7 +554,7 @@ describe('headers', () => {
464554
const request = new Request(defaultUrl)
465555
vi.spyOn(headers, 'set')
466556

467-
setCacheControlHeaders(headers, request, createRequestContext())
557+
setCacheControlHeaders(headers, request, createRequestContext(), true)
468558

469559
expect(headers.set).toHaveBeenNthCalledWith(1, 'cache-control', 'max-age=604800')
470560
expect(headers.set).toHaveBeenNthCalledWith(
@@ -482,7 +572,7 @@ describe('headers', () => {
482572
const request = new Request(defaultUrl)
483573
vi.spyOn(headers, 'set')
484574

485-
setCacheControlHeaders(headers, request, createRequestContext())
575+
setCacheControlHeaders(headers, request, createRequestContext(), true)
486576

487577
expect(headers.set).toHaveBeenNthCalledWith(
488578
1,

src/run/headers.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,10 @@ export const setCacheControlHeaders = (
219219
headers: Headers,
220220
request: Request,
221221
requestContext: RequestContext,
222+
useDurableCache = false,
222223
) => {
224+
const durableCacheDirective = useDurableCache ? ', durable' : ''
225+
223226
if (
224227
typeof requestContext.routeHandlerRevalidate !== 'undefined' &&
225228
['GET', 'HEAD'].includes(request.method) &&
@@ -231,7 +234,7 @@ export const setCacheControlHeaders = (
231234
// if we are serving already stale response, instruct edge to not attempt to cache that response
232235
headers.get('x-nextjs-cache') === 'STALE'
233236
? 'public, max-age=0, must-revalidate'
234-
: `s-maxage=${requestContext.routeHandlerRevalidate === false ? 31536000 : requestContext.routeHandlerRevalidate}, stale-while-revalidate=31536000`
237+
: `s-maxage=${requestContext.routeHandlerRevalidate === false ? 31536000 : requestContext.routeHandlerRevalidate}, stale-while-revalidate=31536000${durableCacheDirective}`
235238

236239
headers.set('netlify-cdn-cache-control', cdnCacheControl)
237240
return
@@ -270,7 +273,7 @@ export const setCacheControlHeaders = (
270273
) {
271274
// handle CDN Cache Control on static files
272275
headers.set('cache-control', 'public, max-age=0, must-revalidate')
273-
headers.set('netlify-cdn-cache-control', `max-age=31536000`)
276+
headers.set('netlify-cdn-cache-control', `max-age=31536000${durableCacheDirective}`)
274277
}
275278
}
276279

0 commit comments

Comments
 (0)