未验证 提交 81e667bf 编写于 作者: J JJ Kasper 提交者: GitHub

Update fallback 404 handling to prevent reload loop (#18119)

This updates the fallback 404 handling to render the correct 404 page on the client when a 404 is returned from fetching the data route on a fallback page on the client. This prevents us from having to rely on a cache to be updated by the time we reload the page to prevent non-stop reloading. 

This also adds handling in serverless mode to ensure the correct 404 page is rendered when leveraging fallback: 'blocking' mode. 

Additional tests for the fallback: 'blocking' 404 handling will be added in a follow-up where returning notFound from `getServerSideProps` is also added. 
上级 4af36117
...@@ -79,6 +79,7 @@ export function createEntrypoints( ...@@ -79,6 +79,7 @@ export function createEntrypoints(
absoluteAppPath: pages['/_app'], absoluteAppPath: pages['/_app'],
absoluteDocumentPath: pages['/_document'], absoluteDocumentPath: pages['/_document'],
absoluteErrorPath: pages['/_error'], absoluteErrorPath: pages['/_error'],
absolute404Path: pages['/404'] || '',
distDir: DOT_NEXT_ALIAS, distDir: DOT_NEXT_ALIAS,
buildId, buildId,
assetPrefix: config.assetPrefix, assetPrefix: config.assetPrefix,
......
...@@ -19,6 +19,7 @@ export type ServerlessLoaderQuery = { ...@@ -19,6 +19,7 @@ export type ServerlessLoaderQuery = {
absoluteAppPath: string absoluteAppPath: string
absoluteDocumentPath: string absoluteDocumentPath: string
absoluteErrorPath: string absoluteErrorPath: string
absolute404Path: string
buildId: string buildId: string
assetPrefix: string assetPrefix: string
generateEtags: string generateEtags: string
...@@ -44,6 +45,7 @@ const nextServerlessLoader: loader.Loader = function () { ...@@ -44,6 +45,7 @@ const nextServerlessLoader: loader.Loader = function () {
absoluteAppPath, absoluteAppPath,
absoluteDocumentPath, absoluteDocumentPath,
absoluteErrorPath, absoluteErrorPath,
absolute404Path,
generateEtags, generateEtags,
poweredByHeader, poweredByHeader,
basePath, basePath,
...@@ -494,6 +496,7 @@ const nextServerlessLoader: loader.Loader = function () { ...@@ -494,6 +496,7 @@ const nextServerlessLoader: loader.Loader = function () {
export async function renderReqToHTML(req, res, renderMode, _renderOpts, _params) { export async function renderReqToHTML(req, res, renderMode, _renderOpts, _params) {
let Document let Document
let Error let Error
let NotFound
;[ ;[
getStaticProps, getStaticProps,
getServerSideProps, getServerSideProps,
...@@ -502,7 +505,8 @@ const nextServerlessLoader: loader.Loader = function () { ...@@ -502,7 +505,8 @@ const nextServerlessLoader: loader.Loader = function () {
App, App,
config, config,
{ default: Document }, { default: Document },
{ default: Error } { default: Error },
${absolute404Path ? `{ default: NotFound }, ` : ''}
] = await Promise.all([ ] = await Promise.all([
getStaticProps, getStaticProps,
getServerSideProps, getServerSideProps,
...@@ -511,7 +515,8 @@ const nextServerlessLoader: loader.Loader = function () { ...@@ -511,7 +515,8 @@ const nextServerlessLoader: loader.Loader = function () {
App, App,
config, config,
require('${absoluteDocumentPath}'), require('${absoluteDocumentPath}'),
require('${absoluteErrorPath}') require('${absoluteErrorPath}'),
${absolute404Path ? `require("${absolute404Path}"),` : ''}
]) ])
const fromExport = renderMode === 'export' || renderMode === true; const fromExport = renderMode === 'export' || renderMode === true;
...@@ -767,14 +772,41 @@ const nextServerlessLoader: loader.Loader = function () { ...@@ -767,14 +772,41 @@ const nextServerlessLoader: loader.Loader = function () {
if (!renderMode) { if (!renderMode) {
if (_nextData || getStaticProps || getServerSideProps) { if (_nextData || getStaticProps || getServerSideProps) {
sendPayload(req, res, _nextData ? JSON.stringify(renderOpts.pageData) : result, _nextData ? 'json' : 'html', ${ if (renderOpts.ssgNotFound) {
generateEtags === 'true' ? true : false res.statusCode = 404
}, {
private: isPreviewMode, const NotFoundComponent = ${
stateful: !!getServerSideProps, absolute404Path ? 'NotFound' : 'Error'
revalidate: renderOpts.revalidate, }
})
return null const errPathname = "${absolute404Path ? '/404' : '/_error'}"
const result = await renderToHTML(req, res, errPathname, parsedUrl.query, Object.assign({}, options, {
getStaticProps: undefined,
getStaticPaths: undefined,
getServerSideProps: undefined,
Component: NotFoundComponent,
err: undefined
}))
sendPayload(req, res, result, 'html', ${
generateEtags === 'true' ? true : false
}, {
private: isPreviewMode,
stateful: !!getServerSideProps,
revalidate: renderOpts.revalidate,
})
return null
} else {
sendPayload(req, res, _nextData ? JSON.stringify(renderOpts.pageData) : result, _nextData ? 'json' : 'html', ${
generateEtags === 'true' ? true : false
}, {
private: isPreviewMode,
stateful: !!getServerSideProps,
revalidate: renderOpts.revalidate,
})
return null
}
} }
} else if (isPreviewMode) { } else if (isPreviewMode) {
res.setHeader( res.setHeader(
......
...@@ -337,7 +337,7 @@ function fetchNextData(dataHref: string, isServerRender: boolean) { ...@@ -337,7 +337,7 @@ function fetchNextData(dataHref: string, isServerRender: boolean) {
// on a client-side transition. Otherwise, we'd get into an infinite // on a client-side transition. Otherwise, we'd get into an infinite
// loop. // loop.
if (!isServerRender || err.message === 'SSG Data NOT_FOUND') { if (!isServerRender) {
markLoadingError(err) markLoadingError(err)
} }
throw err throw err
...@@ -907,13 +907,6 @@ export default class Router implements BaseRouter { ...@@ -907,13 +907,6 @@ export default class Router implements BaseRouter {
// 3. Internal error while loading the page // 3. Internal error while loading the page
// So, doing a hard reload is the proper way to deal with this. // So, doing a hard reload is the proper way to deal with this.
if (process.env.NODE_ENV === 'development') {
// append __next404 query to prevent fallback from being re-served
// on reload in development
if (err.message === SSG_DATA_NOT_FOUND_ERROR && this.isSsr) {
as += `${as.indexOf('?') > -1 ? '&' : '?'}__next404=1`
}
}
window.location.href = as window.location.href = as
// Changing the URL doesn't block executing the current code path. // Changing the URL doesn't block executing the current code path.
...@@ -922,14 +915,34 @@ export default class Router implements BaseRouter { ...@@ -922,14 +915,34 @@ export default class Router implements BaseRouter {
} }
try { try {
const { page: Component, styleSheets } = await this.fetchComponent( let Component: ComponentType
'/_error' let styleSheets: StyleSheetTuple[]
) const ssg404 = err.message === SSG_DATA_NOT_FOUND_ERROR
if (ssg404) {
try {
;({ page: Component, styleSheets } = await this.fetchComponent(
'/404'
))
} catch (_err) {
// non-fatal fallback to _error
}
}
if (
typeof Component! === 'undefined' ||
typeof styleSheets! === 'undefined'
) {
;({ page: Component, styleSheets } = await this.fetchComponent(
'/_error'
))
}
const routeInfo: PrivateRouteInfo = { const routeInfo: PrivateRouteInfo = {
Component, Component,
styleSheets, styleSheets,
err, err: ssg404 ? undefined : err,
error: err, error: ssg404 ? undefined : err,
} }
try { try {
...@@ -938,6 +951,11 @@ export default class Router implements BaseRouter { ...@@ -938,6 +951,11 @@ export default class Router implements BaseRouter {
pathname, pathname,
query, query,
} as any) } as any)
if (ssg404 && routeInfo.props && routeInfo.props.pageProps) {
routeInfo.props.pageProps.statusCode = 404
}
console.log(routeInfo)
} catch (gipErr) { } catch (gipErr) {
console.error('Error in error page `getInitialProps`: ', gipErr) console.error('Error in error page `getInitialProps`: ', gipErr)
routeInfo.props = {} routeInfo.props = {}
......
...@@ -1144,7 +1144,6 @@ export default class Server { ...@@ -1144,7 +1144,6 @@ export default class Server {
...(components.getStaticProps ...(components.getStaticProps
? { ? {
amp: query.amp, amp: query.amp,
__next404: query.__next404,
_nextDataReq: query._nextDataReq, _nextDataReq: query._nextDataReq,
__nextLocale: query.__nextLocale, __nextLocale: query.__nextLocale,
} }
...@@ -1270,15 +1269,6 @@ export default class Server { ...@@ -1270,15 +1269,6 @@ export default class Server {
query.amp ? '.amp' : '' query.amp ? '.amp' : ''
}` }`
// In development we use a __next404 query to allow signaling we should
// render the 404 page after attempting to fetch the _next/data for a
// fallback page since the fallback page will always be available after
// reload and we don't want to re-serve it and instead want to 404.
if (this.renderOpts.dev && isSSG && query.__next404) {
delete query.__next404
throw new NoFallbackError()
}
// Complete the response with cached data if its present // Complete the response with cached data if its present
const cachedData = ssgCacheKey const cachedData = ssgCacheKey
? await this.incrementalCache.get(ssgCacheKey) ? await this.incrementalCache.get(ssgCacheKey)
......
...@@ -414,7 +414,6 @@ export async function renderToHTML( ...@@ -414,7 +414,6 @@ export async function renderToHTML(
const isFallback = !!query.__nextFallback const isFallback = !!query.__nextFallback
delete query.__nextFallback delete query.__nextFallback
delete query.__nextLocale delete query.__nextLocale
delete query.__next404
const isSSG = !!getStaticProps const isSSG = !!getStaticProps
const isBuildTimeSSG = isSSG && renderOpts.nextExport const isBuildTimeSSG = isSSG && renderOpts.nextExport
......
...@@ -95,16 +95,16 @@ describe('Build Output', () => { ...@@ -95,16 +95,16 @@ describe('Build Output', () => {
expect(indexSize.endsWith('B')).toBe(true) expect(indexSize.endsWith('B')).toBe(true)
// should be no bigger than 60.8 kb // should be no bigger than 60.8 kb
expect(parseFloat(indexFirstLoad) - 61).toBeLessThanOrEqual(0) expect(parseFloat(indexFirstLoad) - 61.2).toBeLessThanOrEqual(0)
expect(indexFirstLoad.endsWith('kB')).toBe(true) expect(indexFirstLoad.endsWith('kB')).toBe(true)
expect(parseFloat(err404Size) - 3.5).toBeLessThanOrEqual(0) expect(parseFloat(err404Size) - 3.5).toBeLessThanOrEqual(0)
expect(err404Size.endsWith('kB')).toBe(true) expect(err404Size.endsWith('kB')).toBe(true)
expect(parseFloat(err404FirstLoad) - 64.2).toBeLessThanOrEqual(0) expect(parseFloat(err404FirstLoad) - 64.4).toBeLessThanOrEqual(0)
expect(err404FirstLoad.endsWith('kB')).toBe(true) expect(err404FirstLoad.endsWith('kB')).toBe(true)
expect(parseFloat(sharedByAll) - 60.8).toBeLessThanOrEqual(0) expect(parseFloat(sharedByAll) - 61).toBeLessThanOrEqual(0)
expect(sharedByAll.endsWith('kB')).toBe(true) expect(sharedByAll.endsWith('kB')).toBe(true)
if (_appSize.endsWith('kB')) { if (_appSize.endsWith('kB')) {
......
...@@ -697,7 +697,7 @@ function runTests(isDev) { ...@@ -697,7 +697,7 @@ function runTests(isDev) {
true true
) )
expect(parsedUrl.pathname).toBe('/en/not-found/fallback/first') expect(parsedUrl.pathname).toBe('/en/not-found/fallback/first')
expect(parsedUrl.query).toEqual(isDev ? { __next404: '1' } : {}) expect(parsedUrl.query).toEqual({})
if (isDev) { if (isDev) {
// make sure page doesn't reload un-necessarily in development // make sure page doesn't reload un-necessarily in development
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册