Skip to content

feat: use Netlify Durable Cache #2510

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

Merged
Merged
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
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
Loading