From 28f5b387cd01538921833a68812b2b3d5b019067 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 20 Oct 2023 17:02:59 -0700 Subject: [PATCH] refactor: update revalidate handling for render responses --- packages/next/src/server/base-server.ts | 74 +++++++++++-------- packages/next/src/server/lib/revalidate.ts | 12 +++ packages/next/src/server/next-server.ts | 22 +++--- packages/next/src/server/render.tsx | 11 +-- .../index.ts => send-payload.ts} | 26 +++---- .../server/send-payload/revalidate-headers.ts | 30 -------- packages/next/src/server/web-server.ts | 4 +- 7 files changed, 85 insertions(+), 94 deletions(-) rename packages/next/src/server/{send-payload/index.ts => send-payload.ts} (77%) delete mode 100644 packages/next/src/server/send-payload/revalidate-headers.ts diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index f271526285a6b..813cb02413773 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -21,7 +21,6 @@ import type { PreviewData, ServerRuntime, SizeLimit } from 'next/types' import type { PagesManifest } from '../build/webpack/plugins/pages-manifest-plugin' import type { OutgoingHttpHeaders } from 'http2' import type { BaseNextRequest, BaseNextResponse } from './base-http' -import type { PayloadOptions } from './send-payload' import type { ManifestRewriteRoute, ManifestRoute, @@ -40,6 +39,7 @@ import type { } from './future/route-modules/app-route/module' import type { Server as HTTPServer } from 'http' import type { ImageConfigComplete } from '../shared/lib/image-config' +import type { MiddlewareMatcher } from '../build/analysis/get-page-static-info' import { format as formatUrl, parse as parseUrl } from 'url' import { formatHostname } from './lib/format-hostname' @@ -55,7 +55,7 @@ import { import { isDynamicRoute } from '../shared/lib/router/utils' import { checkIsOnDemandRevalidate } from './api-utils' import { setConfig } from '../shared/lib/runtime-config.external' -import { setRevalidateHeaders } from './send-payload/revalidate-headers' +import { formatRevalidate, type Revalidate } from './lib/revalidate' import { execOnce } from '../shared/lib/utils' import { isBlockedPage } from './utils' import { isBot } from '../shared/lib/router/utils/is-bot' @@ -76,7 +76,6 @@ import { normalizeAppPath } from '../shared/lib/router/utils/app-paths' import { getHostname } from '../shared/lib/get-hostname' import { parseUrl as parseUrlUtil } from '../shared/lib/router/utils/parse-url' import { getNextPathnameInfo } from '../shared/lib/router/utils/get-next-pathname-info' -import type { MiddlewareMatcher } from '../build/analysis/get-page-static-info' import { RSC, RSC_VARY_HEADER, @@ -108,6 +107,7 @@ import { toNodeOutgoingHttpHeaders, } from './web/utils' import { + CACHE_ONE_YEAR, NEXT_CACHE_TAGS_HEADER, NEXT_QUERY_PARAM_PREFIX, } from '../lib/constants' @@ -282,7 +282,7 @@ export class WrappedBuildError extends Error { type ResponsePayload = { type: 'html' | 'json' | 'rsc' body: RenderResult - revalidateOptions?: any + revalidate?: Revalidate } export default abstract class Server { @@ -343,7 +343,7 @@ export default abstract class Server { type: 'html' | 'json' | 'rsc' generateEtags: boolean poweredByHeader: boolean - options?: PayloadOptions + revalidate?: Revalidate } ): Promise @@ -1429,19 +1429,23 @@ export default abstract class Server { return } const { req, res } = ctx - const { body, type, revalidateOptions } = payload + const { body, type } = payload + let { revalidate } = payload if (!res.sent) { const { generateEtags, poweredByHeader, dev } = this.renderOpts + + // In dev, we should not cache pages for any reason. if (dev) { - // In dev, we should not cache pages for any reason. res.setHeader('Cache-Control', 'no-store, must-revalidate') + revalidate = undefined } + return this.sendRenderResult(req, res, { result: body, type, generateEtags, poweredByHeader, - options: revalidateOptions, + revalidate, }) } } @@ -2425,23 +2429,38 @@ export default abstract class Server { ) } - const { revalidate, value: cachedData } = cacheEntry - const revalidateOptions: any = - typeof revalidate !== 'undefined' && - (!this.renderOpts.dev || (hasServerProps && !isDataReq)) - ? { - // When the page is 404 cache-control should not be added unless - // we are rendering the 404 page for notFound: true which should - // cache according to revalidate correctly - private: isPreviewMode || (is404Page && cachedData), - stateful: !isSSG, - revalidate, - } - : undefined + const { value: cachedData } = cacheEntry + + // Coerce the revalidate parameter from the render. + let revalidate: Revalidate | undefined + if ( + typeof cacheEntry.revalidate !== 'undefined' && + !this.renderOpts.dev && + hasServerProps && + !isDataReq && + cachedData?.kind !== 'ROUTE' + ) { + if (isPreviewMode || (is404Page && !isDataReq)) { + revalidate = 0 + } else if (!isSSG && !res.getHeader('Cache-Control')) { + revalidate = 0 + } else if (typeof cacheEntry.revalidate === 'number') { + if (cacheEntry.revalidate < 1) { + throw new Error( + `invariant: invalid Cache-Control duration provided: ${cacheEntry.revalidate} < 1` + ) + } + + revalidate = cacheEntry.revalidate + } else if (typeof cacheEntry.revalidate === 'boolean') { + revalidate = CACHE_ONE_YEAR + } + } + cacheEntry.revalidate = revalidate if (!cachedData) { - if (revalidateOptions) { - setRevalidateHeaders(res, revalidateOptions) + if (cacheEntry.revalidate) { + res.setHeader('Cache-Control', formatRevalidate(cacheEntry.revalidate)) } if (isDataReq) { res.statusCode = 404 @@ -2455,9 +2474,6 @@ export default abstract class Server { return null } } else if (cachedData.kind === 'REDIRECT') { - if (revalidateOptions) { - setRevalidateHeaders(res, revalidateOptions) - } if (isDataReq) { return { type: 'json', @@ -2465,7 +2481,7 @@ export default abstract class Server { // @TODO: Handle flight data. JSON.stringify(cachedData.props) ), - revalidateOptions, + revalidate: cacheEntry.revalidate, } } else { await handleRedirect(cachedData.props) @@ -2518,7 +2534,7 @@ export default abstract class Server { body: isDataReq ? RenderResult.fromStatic(cachedData.pageData as string) : cachedData.html, - revalidateOptions, + revalidate: cacheEntry.revalidate, } } @@ -2527,7 +2543,7 @@ export default abstract class Server { body: isDataReq ? RenderResult.fromStatic(JSON.stringify(cachedData.pageData)) : cachedData.html, - revalidateOptions, + revalidate: cacheEntry.revalidate, } } } diff --git a/packages/next/src/server/lib/revalidate.ts b/packages/next/src/server/lib/revalidate.ts index 2cb9fd60583a1..17f661840223b 100644 --- a/packages/next/src/server/lib/revalidate.ts +++ b/packages/next/src/server/lib/revalidate.ts @@ -1,3 +1,5 @@ +import { CACHE_ONE_YEAR } from '../../lib/constants' + /** * The revalidate option used internally for pages. A value of `false` means * that the page should not be revalidated. A number means that the page @@ -6,3 +8,13 @@ * value for this option. */ export type Revalidate = number | false + +export function formatRevalidate(revalidate: Revalidate): string { + if (revalidate === 0) { + return 'private, no-cache, no-store, max-age=0, must-revalidate' + } else if (typeof revalidate === 'number') { + return `s-maxage=${revalidate}, stale-while-revalidate` + } + + return `s-maxage=${CACHE_ONE_YEAR}, stale-while-revalidate` +} diff --git a/packages/next/src/server/next-server.ts b/packages/next/src/server/next-server.ts index b987f1021358e..7aa6efa3290b7 100644 --- a/packages/next/src/server/next-server.ts +++ b/packages/next/src/server/next-server.ts @@ -15,17 +15,20 @@ import type { FetchEventResult } from './web/types' import type { PrerenderManifest } from '../build' import type { BaseNextRequest, BaseNextResponse } from './base-http' import type { PagesManifest } from '../build/webpack/plugins/pages-manifest-plugin' -import type { PayloadOptions } from './send-payload' import type { NextParsedUrlQuery, NextUrlWithParsedQuery } from './request-meta' -import { getRouteMatcher } from '../shared/lib/router/utils/route-matcher' import type { Params } from '../shared/lib/router/utils/route-matcher' import type { MiddlewareRouteMatch } from '../shared/lib/router/utils/middleware-route-matcher' import type { RouteMatch } from './future/route-matches/route-match' +import type { IncomingMessage, ServerResponse } from 'http' +import type { PagesAPIRouteModule } from './future/route-modules/pages-api/module' +import type { UrlWithParsedQuery } from 'url' +import type { ParsedUrlQuery } from 'querystring' +import type { ParsedUrl } from '../shared/lib/router/utils/parse-url' +import type { Revalidate } from './lib/revalidate' import fs from 'fs' import { join, resolve, isAbsolute } from 'path' -import type { IncomingMessage, ServerResponse } from 'http' -import type { PagesAPIRouteModule } from './future/route-modules/pages-api/module' +import { getRouteMatcher } from '../shared/lib/router/utils/route-matcher' import { addRequestMeta, getRequestMeta } from './request-meta' import { PAGES_MANIFEST, @@ -41,11 +44,8 @@ import { INTERNAL_HEADERS, } from '../shared/lib/constants' import { findDir } from '../lib/find-pages-dir' -import type { UrlWithParsedQuery } from 'url' import { NodeNextRequest, NodeNextResponse } from './base-http/node' import { sendRenderResult } from './send-payload' -import type { ParsedUrlQuery } from 'querystring' -import type { ParsedUrl } from '../shared/lib/router/utils/parse-url' import { parseUrl } from '../shared/lib/router/utils/parse-url' import * as Log from '../build/output/log' @@ -387,13 +387,17 @@ export default class NextNodeServer extends BaseServer { type: 'html' | 'json' generateEtags: boolean poweredByHeader: boolean - options?: PayloadOptions | undefined + revalidate: Revalidate | undefined } ): Promise { return sendRenderResult({ req: req.originalRequest, res: res.originalResponse, - ...options, + result: options.result, + type: options.type, + generateEtags: options.generateEtags, + poweredByHeader: options.poweredByHeader, + revalidate: options.revalidate, }) } diff --git a/packages/next/src/server/render.tsx b/packages/next/src/server/render.tsx index 96c39c32ca9d2..cc98f8e56a726 100644 --- a/packages/next/src/server/render.tsx +++ b/packages/next/src/server/render.tsx @@ -38,6 +38,7 @@ import type { PagesModule } from './future/route-modules/pages/module' import type { ComponentsEnhancer } from '../shared/lib/utils' import type { NextParsedUrlQuery } from './request-meta' import type { Revalidate } from './lib/revalidate' +import type { COMPILER_NAMES } from '../shared/lib/constants' import React from 'react' import ReactDOMServer from 'react-dom/server.browser' @@ -51,9 +52,7 @@ import { SERVER_PROPS_SSG_CONFLICT, SSG_GET_INITIAL_PROPS_CONFLICT, UNSTABLE_REVALIDATE_RENAME_ERROR, - CACHE_ONE_YEAR, } from '../lib/constants' -import type { COMPILER_NAMES } from '../shared/lib/constants' import { NEXT_BUILTIN_DOCUMENT, SERVER_PROPS_ID, @@ -105,7 +104,7 @@ import { import { getTracer } from './lib/trace/tracer' import { RenderSpan } from './lib/trace/constants' import { ReflectAdapter } from './web/spec-extension/adapters/reflect' -import { setRevalidateHeaders } from './send-payload' +import { formatRevalidate } from './lib/revalidate' let tryGetPreviewData: typeof import('./api-utils/node/try-get-preview-data').tryGetPreviewData let warn: typeof import('../build/output/log').warn @@ -510,11 +509,7 @@ export async function renderToHTMLImpl( // ensure we set cache header so it's not rendered on-demand // every request if (isAutoExport && !dev && isExperimentalCompile) { - setRevalidateHeaders(res, { - revalidate: CACHE_ONE_YEAR, - private: false, - stateful: false, - }) + res.setHeader('Cache-Control', formatRevalidate(false)) isAutoExport = false } diff --git a/packages/next/src/server/send-payload/index.ts b/packages/next/src/server/send-payload.ts similarity index 77% rename from packages/next/src/server/send-payload/index.ts rename to packages/next/src/server/send-payload.ts index bc0278cacef85..56299102836e0 100644 --- a/packages/next/src/server/send-payload/index.ts +++ b/packages/next/src/server/send-payload.ts @@ -1,18 +1,12 @@ import type { IncomingMessage, ServerResponse } from 'http' -import type RenderResult from '../render-result' +import type RenderResult from './render-result' +import type { Revalidate } from './lib/revalidate' -import { isResSent } from '../../shared/lib/utils' -import { generateETag } from '../lib/etag' +import { isResSent } from '../shared/lib/utils' +import { generateETag } from './lib/etag' import fresh from 'next/dist/compiled/fresh' -import { setRevalidateHeaders } from './revalidate-headers' -import { RSC_CONTENT_TYPE_HEADER } from '../../client/components/app-router-headers' - -export type PayloadOptions = - | { private: true } - | { private: boolean; stateful: true } - | { private: boolean; stateful: false; revalidate: number | false } - -export { setRevalidateHeaders } +import { formatRevalidate } from './lib/revalidate' +import { RSC_CONTENT_TYPE_HEADER } from '../client/components/app-router-headers' export function sendEtagResponse( req: IncomingMessage, @@ -45,7 +39,7 @@ export async function sendRenderResult({ type, generateEtags, poweredByHeader, - options, + revalidate, }: { req: IncomingMessage res: ServerResponse @@ -53,7 +47,7 @@ export async function sendRenderResult({ type: 'html' | 'json' | 'rsc' generateEtags: boolean poweredByHeader: boolean - options?: PayloadOptions + revalidate: Revalidate | undefined }): Promise { if (isResSent(res)) { return @@ -63,8 +57,8 @@ export async function sendRenderResult({ res.setHeader('X-Powered-By', 'Next.js') } - if (options != null) { - setRevalidateHeaders(res, options) + if (typeof revalidate !== 'undefined') { + res.setHeader('Cache-Control', formatRevalidate(revalidate)) } const payload = result.isDynamic ? null : result.toUnchunkedString() diff --git a/packages/next/src/server/send-payload/revalidate-headers.ts b/packages/next/src/server/send-payload/revalidate-headers.ts deleted file mode 100644 index eeb012e33757b..0000000000000 --- a/packages/next/src/server/send-payload/revalidate-headers.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { ServerResponse } from 'http' -import type { BaseNextResponse } from '../base-http' -import type { PayloadOptions } from './index' - -export function setRevalidateHeaders( - res: ServerResponse | BaseNextResponse, - options: PayloadOptions -) { - if (options.private || options.stateful) { - if (options.private || !res.getHeader('Cache-Control')) { - res.setHeader( - 'Cache-Control', - `private, no-cache, no-store, max-age=0, must-revalidate` - ) - } - } else if (typeof options.revalidate === 'number') { - if (options.revalidate < 1) { - throw new Error( - `invariant: invalid Cache-Control duration provided: ${options.revalidate} < 1` - ) - } - - res.setHeader( - 'Cache-Control', - `s-maxage=${options.revalidate}, stale-while-revalidate` - ) - } else if (options.revalidate === false) { - res.setHeader('Cache-Control', `s-maxage=31536000, stale-while-revalidate`) - } -} diff --git a/packages/next/src/server/web-server.ts b/packages/next/src/server/web-server.ts index ea0439d08fccf..730150604291e 100644 --- a/packages/next/src/server/web-server.ts +++ b/packages/next/src/server/web-server.ts @@ -2,7 +2,6 @@ import type { WebNextRequest, WebNextResponse } from './base-http/web' import type RenderResult from './render-result' import type { NextParsedUrlQuery, NextUrlWithParsedQuery } from './request-meta' import type { Params } from '../shared/lib/router/utils/route-matcher' -import type { PayloadOptions } from './send-payload' import type { LoadComponentsReturnType } from './load-components' import type { PrerenderManifest } from '../build' import type { @@ -12,6 +11,7 @@ import type { Options, RouteHandler, } from './base-server' +import type { Revalidate } from './lib/revalidate' import { byteLength } from './api-utils/web' import BaseServer, { NoFallbackError } from './base-server' @@ -230,7 +230,7 @@ export default class NextWebServer extends BaseServer { type: 'html' | 'json' generateEtags: boolean poweredByHeader: boolean - options?: PayloadOptions | undefined + revalidate: Revalidate | undefined } ): Promise { res.setHeader('X-Edge-Runtime', '1')