Skip to content

Commit 942e45a

Browse files
committed
Fix inconsistency with 404 getStaticProps cache-control (#66674)
While investigating unexpected stale responses when leveraging `getStaticProps` on the 404 page noticed we have an inconsistency between local and deployed due to `Cache-Control` being set to `no-store, must-revalidate` even though a revalidate period is provided. The inconsistency also differs between the HTML response and the data response `_next/data` which causes even more unexpected behavior. To avoid this behavior, this replaces the handling to ensure we honor the originally provided revalidate period during `notFound: true` for the `Cache-Control` header. Validated against provided reproduction here https://github.com/fusdev0/next-notfound-revalidate Deployment: https://vercel.live/link/next-notfound-revalidate-govzskknf-vtest314-ijjk-testing.vercel.app/fallback-blocking/fasdf Prior PR for prior context that introduced this #19165 x-ref: [slack thread](https://vercel.slack.com/archives/C0676QZBWKS/p1717492459342109) # Conflicts: # packages/next/src/server/base-server.ts
1 parent 9728a35 commit 942e45a

File tree

4 files changed

+43
-20
lines changed

4 files changed

+43
-20
lines changed

packages/next/src/server/base-server.ts

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -852,8 +852,8 @@ export default abstract class Server<ServerOptions extends Options = Options> {
852852
...(typeof val === 'string'
853853
? [val]
854854
: Array.isArray(val)
855-
? val
856-
: []),
855+
? val
856+
: []),
857857
]),
858858
]
859859
}
@@ -904,8 +904,8 @@ export default abstract class Server<ServerOptions extends Options = Options> {
904904
req.headers['x-forwarded-port'] ??= this.port
905905
? this.port.toString()
906906
: isHttps
907-
? '443'
908-
: '80'
907+
? '443'
908+
: '80'
909909
req.headers['x-forwarded-proto'] ??= isHttps ? 'https' : 'http'
910910
req.headers['x-forwarded-for'] ??= originalRequest.socket?.remoteAddress
911911

@@ -1674,8 +1674,8 @@ export default abstract class Server<ServerOptions extends Options = Options> {
16741674
typeof fallbackField === 'string'
16751675
? 'static'
16761676
: fallbackField === null
1677-
? 'blocking'
1678-
: fallbackField,
1677+
? 'blocking'
1678+
: fallbackField,
16791679
}
16801680
}
16811681

@@ -2647,10 +2647,10 @@ export default abstract class Server<ServerOptions extends Options = Options> {
26472647
isOnDemandRevalidate
26482648
? 'REVALIDATED'
26492649
: cacheEntry.isMiss
2650-
? 'MISS'
2651-
: cacheEntry.isStale
2652-
? 'STALE'
2653-
: 'HIT'
2650+
? 'MISS'
2651+
: cacheEntry.isStale
2652+
? 'STALE'
2653+
: 'HIT'
26542654
)
26552655
}
26562656

@@ -2684,9 +2684,8 @@ export default abstract class Server<ServerOptions extends Options = Options> {
26842684
typeof cacheEntry.revalidate !== 'undefined' &&
26852685
(!this.renderOpts.dev || (hasServerProps && !isDataReq))
26862686
) {
2687-
// If this is a preview mode request, we shouldn't cache it. We also don't
2688-
// cache 404 pages.
2689-
if (isPreviewMode || (is404Page && !isDataReq)) {
2687+
// If this is a preview mode request, we shouldn't cache it
2688+
if (isPreviewMode) {
26902689
revalidate = 0
26912690
}
26922691

@@ -2698,6 +2697,18 @@ export default abstract class Server<ServerOptions extends Options = Options> {
26982697
}
26992698
}
27002699

2700+
// If we are rendering the 404 page we derive the cache-control
2701+
// revalidate period from the value that trigged the not found
2702+
// to be rendered. So if `getStaticProps` returns
2703+
// { notFound: true, revalidate 60 } the revalidate period should
2704+
// be 60 but if a static asset 404s directly it should have a revalidate
2705+
// period of 0 so that it doesn't get cached unexpectedly by a CDN
2706+
else if (is404Page) {
2707+
const notFoundRevalidate = getRequestMeta(req, 'notFoundRevalidate')
2708+
revalidate =
2709+
typeof notFoundRevalidate === 'undefined' ? 0 : notFoundRevalidate
2710+
}
2711+
27012712
// If the cache entry has a revalidate value that's a number, use it.
27022713
else if (typeof cacheEntry.revalidate === 'number') {
27032714
if (cacheEntry.revalidate < 1) {
@@ -2731,6 +2742,12 @@ export default abstract class Server<ServerOptions extends Options = Options> {
27312742
}
27322743

27332744
if (!cachedData) {
2745+
// add revalidate metadata before rendering 404 page
2746+
// so that we can use this as source of truth for the
2747+
// cache-control header instead of what the 404 page returns
2748+
// for the revalidate value
2749+
addRequestMeta(req, 'notFoundRevalidate', cacheEntry.revalidate)
2750+
27342751
if (cacheEntry.revalidate) {
27352752
res.setHeader(
27362753
'Cache-Control',
@@ -2749,7 +2766,6 @@ export default abstract class Server<ServerOptions extends Options = Options> {
27492766
if (this.renderOpts.dev) {
27502767
query.__nextNotFoundSrcPage = pathname
27512768
}
2752-
27532769
await this.render404(req, res, { pathname, query }, false)
27542770
return null
27552771
} else if (cachedData.kind === 'REDIRECT') {

packages/next/src/server/request-meta.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ export interface RequestMeta {
9494
cacheEntry: any,
9595
requestMeta: any
9696
) => Promise<boolean | void> | boolean | void
97+
98+
/**
99+
* The previous revalidate before rendering 404 page for notFound: true
100+
*/
101+
notFoundRevalidate?: number | false
97102
}
98103

99104
/**

test/integration/not-found-revalidate/pages/404.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@ export const getStaticProps = () => {
1414
notFound: true,
1515
random: Math.random(),
1616
},
17-
revalidate: 1,
17+
revalidate: 6000,
1818
}
1919
}

test/integration/not-found-revalidate/test/index.test.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,17 +81,19 @@ const runTests = () => {
8181
let res = await fetchViaHTTP(appPort, '/fallback-blocking/hello')
8282
let $ = cheerio.load(await res.text())
8383

84-
const privateCache =
85-
'private, no-cache, no-store, max-age=0, must-revalidate'
86-
expect(res.headers.get('cache-control')).toBe(privateCache)
84+
expect(res.headers.get('cache-control')).toBe(
85+
`s-maxage=1, stale-while-revalidate`
86+
)
8787
expect(res.status).toBe(404)
8888
expect(JSON.parse($('#props').text()).notFound).toBe(true)
8989

9090
await waitFor(1000)
9191
res = await fetchViaHTTP(appPort, '/fallback-blocking/hello')
9292
$ = cheerio.load(await res.text())
9393

94-
expect(res.headers.get('cache-control')).toBe(privateCache)
94+
expect(res.headers.get('cache-control')).toBe(
95+
`s-maxage=1, stale-while-revalidate`
96+
)
9597
expect(res.status).toBe(404)
9698
expect(JSON.parse($('#props').text()).notFound).toBe(true)
9799

@@ -146,7 +148,7 @@ const runTests = () => {
146148
let $ = cheerio.load(await res.text())
147149

148150
expect(res.headers.get('cache-control')).toBe(
149-
'private, no-cache, no-store, max-age=0, must-revalidate'
151+
`s-maxage=1, stale-while-revalidate`
150152
)
151153
expect(res.status).toBe(404)
152154
expect(JSON.parse($('#props').text()).notFound).toBe(true)

0 commit comments

Comments
 (0)