Skip to content

Commit

Permalink
feat: use Netlify Durable Cache (#2510)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
serhalp committed Jul 4, 2024
1 parent 3fea441 commit 233fc2f
Show file tree
Hide file tree
Showing 7 changed files with 347 additions and 37 deletions.
4 changes: 3 additions & 1 deletion src/run/handlers/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,9 @@ 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_disable') !== true
setCacheControlHeaders(response.headers, request, requestContext, useDurableCache)
setCacheTagsHeaders(response.headers, requestContext)
setVaryHeaders(response.headers, request, nextConfig)
setCacheStatusHeader(response.headers)
Expand Down
255 changes: 236 additions & 19 deletions src/run/headers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import { type FixtureTestContext } from '../../tests/utils/contexts.js'
import { generateRandomObjectID, startMockBlobStore } from '../../tests/utils/helpers.js'

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

beforeEach<FixtureTestContext>(async (ctx) => {
Expand Down Expand Up @@ -194,25 +194,242 @@ 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 = {
'cdn-cache-control': 'public, max-age=0, must-revalidate',
}
const headers = new Headers(givenHeaders)
const request = new Request(defaultUrl)
vi.spyOn(headers, 'set')

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

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

test('should not set any headers if "netlify-cdn-cache-control" is present', () => {
const givenHeaders = {
'netlify-cdn-cache-control': 'public, max-age=0, must-revalidate',
}
const headers = new Headers(givenHeaders)
const request = new Request(defaultUrl)
vi.spyOn(headers, 'set')

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

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

test('should mark content as stale if "{netlify-,}cdn-cache-control" is not present and "x-nextjs-cache" is "STALE" (GET)', () => {
const givenHeaders = {
'x-nextjs-cache': 'STALE',
}
const headers = new Headers(givenHeaders)
const request = new Request(defaultUrl)
vi.spyOn(headers, 'set')

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

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

test('should mark content as stale if "{netlify-,}cdn-cache-control" is not present and "x-nextjs-cache" is "STALE" (HEAD)', () => {
const givenHeaders = {
'x-nextjs-cache': 'STALE',
}
const headers = new Headers(givenHeaders)
const request = new Request(defaultUrl)
vi.spyOn(headers, 'set')

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

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

test('should set 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, true)

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

test('should set 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, true)

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

test('should set 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, true)

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

test('should not set any headers on POST request', () => {
const headers = new Headers()
const request = new Request(defaultUrl, { method: 'POST' })
vi.spyOn(headers, 'set')

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

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

test('should not set any headers if "cache-control" is not set and "requestContext.usedFsRead" is not truthy', () => {
const headers = new 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 @@ -222,7 +439,7 @@ describe('headers', () => {
expect(headers.set).toHaveBeenNthCalledWith(
2,
'netlify-cdn-cache-control',
'max-age=31536000',
'max-age=31536000, durable',
)
})

Expand All @@ -235,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 @@ -249,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 @@ -262,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 @@ -272,7 +489,7 @@ describe('headers', () => {
expect(headers.set).toHaveBeenNthCalledWith(
2,
'netlify-cdn-cache-control',
'public, max-age=0, must-revalidate',
'public, max-age=0, must-revalidate, durable',
)
})

Expand All @@ -284,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 @@ -294,7 +511,7 @@ describe('headers', () => {
expect(headers.set).toHaveBeenNthCalledWith(
2,
'netlify-cdn-cache-control',
'public, max-age=0, must-revalidate',
'public, max-age=0, must-revalidate, durable',
)
})

Expand All @@ -306,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 @@ -319,13 +536,13 @@ 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(
2,
'netlify-cdn-cache-control',
'public, s-maxage=604800',
'public, s-maxage=604800, durable',
)
})

Expand All @@ -337,25 +554,25 @@ 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(
2,
'netlify-cdn-cache-control',
'max-age=604800, stale-while-revalidate=86400',
'max-age=604800, stale-while-revalidate=86400, durable',
)
})

test('should set default "cache-control" header if it contains only "s-maxage" and "stale-whie-revalidate"', () => {
test('should set default "cache-control" header if it contains only "s-maxage" and "stale-while-revalidate"', () => {
const givenHeaders = {
'cache-control': 's-maxage=604800, stale-while-revalidate=86400',
}
const headers = new Headers(givenHeaders)
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 @@ -365,7 +582,7 @@ describe('headers', () => {
expect(headers.set).toHaveBeenNthCalledWith(
2,
'netlify-cdn-cache-control',
's-maxage=604800, stale-while-revalidate=86400',
's-maxage=604800, stale-while-revalidate=86400, durable',
)
})
})
Expand Down
Loading

0 comments on commit 233fc2f

Please sign in to comment.