diff --git a/src/run/handlers/server.ts b/src/run/handlers/server.ts index d7dd4c5c0a..e574080ade 100644 --- a/src/run/handlers/server.ts +++ b/src/run/handlers/server.ts @@ -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) diff --git a/src/run/headers.test.ts b/src/run/headers.test.ts index ee71cd8891..0db873fc1f 100644 --- a/src/run/headers.test.ts +++ b/src/run/headers.test.ts @@ -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 = { @@ -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) }) @@ -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) }) @@ -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( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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) }) @@ -326,12 +416,12 @@ 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') @@ -339,7 +429,7 @@ describe('headers', () => { const requestContext = createRequestContext() requestContext.usedFsRead = true - setCacheControlHeaders(headers, request, requestContext) + setCacheControlHeaders(headers, request, requestContext, true) expect(headers.set).toHaveBeenNthCalledWith( 1, @@ -349,7 +439,7 @@ describe('headers', () => { expect(headers.set).toHaveBeenNthCalledWith( 2, 'netlify-cdn-cache-control', - 'max-age=31536000', + 'max-age=31536000, durable', ) }) @@ -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) }) @@ -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) }) @@ -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, @@ -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, @@ -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) }) @@ -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( @@ -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( @@ -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, diff --git a/src/run/headers.ts b/src/run/headers.ts index a5b80cc13b..34a9976efa 100644 --- a/src/run/headers.ts +++ b/src/run/headers.ts @@ -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) && @@ -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 @@ -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}`) } }