Skip to content

Commit

Permalink
feat: specify durable cache-control directive
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
serhalp committed Jun 25, 2024
1 parent b53be90 commit 75a601e
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 23 deletions.
5 changes: 4 additions & 1 deletion src/run/handlers/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,10 @@ export default async (request: Request, context: FutureContext) => {

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

setCacheControlHeaders(response.headers, request, requestContext)
const useDurableCache = context.flags.get('serverless_functions_nextjs_durable_cache') as
| boolean
| undefined
setCacheControlHeaders(response.headers, request, requestContext, useDurableCache)
setCacheTagsHeaders(response.headers, requestContext)
setVaryHeaders(response.headers, request, nextConfig)
setCacheStatusHeader(response.headers)
Expand Down
130 changes: 110 additions & 20 deletions src/run/headers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,96 @@ describe('headers', () => {
describe('setCacheControlHeaders', () => {
const defaultUrl = 'https://example.com'

describe('Durable Cache feature flag disabled', () => {
test('should set permanent, non-durable "netlify-cdn-cache-control" if "cache-control" is not set and "requestContext.usedFsRead" is truthy', () => {
const headers = new Headers()
const request = new Request(defaultUrl)
vi.spyOn(headers, 'set')

const requestContext = createRequestContext()
requestContext.usedFsRead = true

setCacheControlHeaders(headers, request, requestContext, false)

expect(headers.set).toHaveBeenNthCalledWith(
1,
'cache-control',
'public, max-age=0, must-revalidate',
)
expect(headers.set).toHaveBeenNthCalledWith(
2,
'netlify-cdn-cache-control',
'max-age=31536000',
)
})

describe('route handler responses with a specified `revalidate` value', () => {
test('should set non-durable SWC=1yr with 1yr TTL if "{netlify-,}cdn-cache-control" is not present and `revalidate` is `false` (GET)', () => {
const headers = new Headers()
const request = new Request(defaultUrl)
vi.spyOn(headers, 'set')

const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: false }
setCacheControlHeaders(headers, request, ctx, false)

expect(headers.set).toHaveBeenCalledTimes(1)
expect(headers.set).toHaveBeenNthCalledWith(
1,
'netlify-cdn-cache-control',
's-maxage=31536000, stale-while-revalidate=31536000',
)
})

test('should set non-durable SWC=1yr with 1yr TTL if "{netlify-,}cdn-cache-control" is not present and `revalidate` is `false` (HEAD)', () => {
const headers = new Headers()
const request = new Request(defaultUrl, { method: 'HEAD' })
vi.spyOn(headers, 'set')

const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: false }
setCacheControlHeaders(headers, request, ctx, false)

expect(headers.set).toHaveBeenCalledTimes(1)
expect(headers.set).toHaveBeenNthCalledWith(
1,
'netlify-cdn-cache-control',
's-maxage=31536000, stale-while-revalidate=31536000',
)
})

test('should set non-durable SWC=1yr with given TTL if "{netlify-,}cdn-cache-control" is not present and `revalidate` is a number (GET)', () => {
const headers = new Headers()
const request = new Request(defaultUrl)
vi.spyOn(headers, 'set')

const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: 7200 }
setCacheControlHeaders(headers, request, ctx, false)

expect(headers.set).toHaveBeenCalledTimes(1)
expect(headers.set).toHaveBeenNthCalledWith(
1,
'netlify-cdn-cache-control',
's-maxage=7200, stale-while-revalidate=31536000',
)
})

test('should set non-durable SWC=1yr with 1yr TTL if "{netlify-,}cdn-cache-control" is not present and `revalidate` is a number (HEAD)', () => {
const headers = new Headers()
const request = new Request(defaultUrl, { method: 'HEAD' })
vi.spyOn(headers, 'set')

const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: 7200 }
setCacheControlHeaders(headers, request, ctx, false)

expect(headers.set).toHaveBeenCalledTimes(1)
expect(headers.set).toHaveBeenNthCalledWith(
1,
'netlify-cdn-cache-control',
's-maxage=7200, stale-while-revalidate=31536000',
)
})
})
})

describe('route handler responses with a specified `revalidate` value', () => {
test('should not set any headers if "cdn-cache-control" is present', () => {
const givenHeaders = {
Expand All @@ -204,7 +294,7 @@ describe('headers', () => {
vi.spyOn(headers, 'set')

const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: false }
setCacheControlHeaders(headers, request, ctx)
setCacheControlHeaders(headers, request, ctx, true)

expect(headers.set).toHaveBeenCalledTimes(0)
})
Expand All @@ -218,7 +308,7 @@ describe('headers', () => {
vi.spyOn(headers, 'set')

const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: false }
setCacheControlHeaders(headers, request, ctx)
setCacheControlHeaders(headers, request, ctx, true)

expect(headers.set).toHaveBeenCalledTimes(0)
})
Expand All @@ -232,7 +322,7 @@ describe('headers', () => {
vi.spyOn(headers, 'set')

const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: false }
setCacheControlHeaders(headers, request, ctx)
setCacheControlHeaders(headers, request, ctx, true)

expect(headers.set).toHaveBeenCalledTimes(1)
expect(headers.set).toHaveBeenNthCalledWith(
Expand All @@ -251,7 +341,7 @@ describe('headers', () => {
vi.spyOn(headers, 'set')

const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: false }
setCacheControlHeaders(headers, request, ctx)
setCacheControlHeaders(headers, request, ctx, true)

expect(headers.set).toHaveBeenCalledTimes(1)
expect(headers.set).toHaveBeenNthCalledWith(
Expand All @@ -267,7 +357,7 @@ describe('headers', () => {
vi.spyOn(headers, 'set')

const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: false }
setCacheControlHeaders(headers, request, ctx)
setCacheControlHeaders(headers, request, ctx, true)

expect(headers.set).toHaveBeenCalledTimes(1)
expect(headers.set).toHaveBeenNthCalledWith(
Expand All @@ -283,7 +373,7 @@ describe('headers', () => {
vi.spyOn(headers, 'set')

const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: 7200 }
setCacheControlHeaders(headers, request, ctx)
setCacheControlHeaders(headers, request, ctx, true)

expect(headers.set).toHaveBeenCalledTimes(1)
expect(headers.set).toHaveBeenNthCalledWith(
Expand All @@ -299,7 +389,7 @@ describe('headers', () => {
vi.spyOn(headers, 'set')

const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: 7200 }
setCacheControlHeaders(headers, request, ctx)
setCacheControlHeaders(headers, request, ctx, true)

expect(headers.set).toHaveBeenCalledTimes(1)
expect(headers.set).toHaveBeenNthCalledWith(
Expand All @@ -315,7 +405,7 @@ describe('headers', () => {
vi.spyOn(headers, 'set')

const ctx: RequestContext = { ...createRequestContext(), routeHandlerRevalidate: false }
setCacheControlHeaders(headers, request, ctx)
setCacheControlHeaders(headers, request, ctx, true)

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

setCacheControlHeaders(headers, request, createRequestContext())
setCacheControlHeaders(headers, request, createRequestContext(), true)

expect(headers.set).toHaveBeenCalledTimes(0)
})

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

const requestContext = createRequestContext()
requestContext.usedFsRead = true

setCacheControlHeaders(headers, request, requestContext)
setCacheControlHeaders(headers, request, requestContext, true)

expect(headers.set).toHaveBeenNthCalledWith(
1,
Expand All @@ -349,7 +439,7 @@ describe('headers', () => {
expect(headers.set).toHaveBeenNthCalledWith(
2,
'netlify-cdn-cache-control',
'max-age=31536000',
'max-age=31536000, durable',
)
})

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

setCacheControlHeaders(headers, request, createRequestContext())
setCacheControlHeaders(headers, request, createRequestContext(), true)

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

setCacheControlHeaders(headers, request, createRequestContext())
setCacheControlHeaders(headers, request, createRequestContext(), true)

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

setCacheControlHeaders(headers, request, createRequestContext())
setCacheControlHeaders(headers, request, createRequestContext(), true)

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

setCacheControlHeaders(headers, request, createRequestContext())
setCacheControlHeaders(headers, request, createRequestContext(), true)

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

setCacheControlHeaders(headers, request, createRequestContext())
setCacheControlHeaders(headers, request, createRequestContext(), true)

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

setCacheControlHeaders(headers, request, createRequestContext())
setCacheControlHeaders(headers, request, createRequestContext(), true)

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

setCacheControlHeaders(headers, request, createRequestContext())
setCacheControlHeaders(headers, request, createRequestContext(), true)

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

setCacheControlHeaders(headers, request, createRequestContext())
setCacheControlHeaders(headers, request, createRequestContext(), true)

expect(headers.set).toHaveBeenNthCalledWith(
1,
Expand Down
7 changes: 5 additions & 2 deletions src/run/headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,10 @@ export const setCacheControlHeaders = (
headers: Headers,
request: Request,
requestContext: RequestContext,
useDurableCache = false,
) => {
const durableCacheDirective = useDurableCache ? ', durable' : ''

if (
typeof requestContext.routeHandlerRevalidate !== 'undefined' &&
['GET', 'HEAD'].includes(request.method) &&
Expand All @@ -231,7 +234,7 @@ export const setCacheControlHeaders = (
// if we are serving already stale response, instruct edge to not attempt to cache that response
headers.get('x-nextjs-cache') === 'STALE'
? 'public, max-age=0, must-revalidate'
: `s-maxage=${requestContext.routeHandlerRevalidate === false ? 31536000 : requestContext.routeHandlerRevalidate}, stale-while-revalidate=31536000`
: `s-maxage=${requestContext.routeHandlerRevalidate === false ? 31536000 : requestContext.routeHandlerRevalidate}, stale-while-revalidate=31536000${durableCacheDirective}`

headers.set('netlify-cdn-cache-control', cdnCacheControl)
return
Expand Down Expand Up @@ -270,7 +273,7 @@ export const setCacheControlHeaders = (
) {
// handle CDN Cache Control on static files
headers.set('cache-control', 'public, max-age=0, must-revalidate')
headers.set('netlify-cdn-cache-control', `max-age=31536000`)
headers.set('netlify-cdn-cache-control', `max-age=31536000${durableCacheDirective}`)
}
}

Expand Down

0 comments on commit 75a601e

Please sign in to comment.