diff --git a/packages/next/client/page-loader.js b/packages/next/client/page-loader.js index d6d6712ce044504c7da71c111885b99d072ce680..c8d3b1197d97aa7428167e0ac592e74a9bbf27be 100644 --- a/packages/next/client/page-loader.js +++ b/packages/next/client/page-loader.js @@ -1,12 +1,12 @@ import mitt from '../next-server/lib/mitt' +import { addBasePath, markLoadingError } from '../next-server/lib/router/router' +import escapePathDelimiters from '../next-server/lib/router/utils/escape-path-delimiters' +import getAssetPathFromRoute from './../next-server/lib/router/utils/get-asset-path-from-route' import { isDynamicRoute } from './../next-server/lib/router/utils/is-dynamic' +import { parseRelativeUrl } from './../next-server/lib/router/utils/parse-relative-url' +import { searchParamsToUrlQuery } from './../next-server/lib/router/utils/querystring' import { getRouteMatcher } from './../next-server/lib/router/utils/route-matcher' import { getRouteRegex } from './../next-server/lib/router/utils/route-regex' -import { searchParamsToUrlQuery } from './../next-server/lib/router/utils/querystring' -import { parseRelativeUrl } from './../next-server/lib/router/utils/parse-relative-url' -import escapePathDelimiters from '../next-server/lib/router/utils/escape-path-delimiters' -import getAssetPathFromRoute from './../next-server/lib/router/utils/get-asset-path-from-route' -import { addBasePath } from '../next-server/lib/router/router' function hasRel(rel, link) { try { @@ -16,9 +16,7 @@ function hasRel(rel, link) { } function pageLoadError(route) { - const error = new Error(`Error loading ${route}`) - error.code = 'PAGE_LOAD_ERROR' - return error + return markLoadingError(new Error(`Error loading ${route}`)) } const relPrefetch = diff --git a/packages/next/next-server/lib/router/router.ts b/packages/next/next-server/lib/router/router.ts index 825c85c79de7ffe41de9dbd57712644897f63d2b..ae75e2ed9a0d20df887f6e5fe05129077fa448fc 100644 --- a/packages/next/next-server/lib/router/router.ts +++ b/packages/next/next-server/lib/router/router.ts @@ -98,6 +98,11 @@ export function resolveHref(currentPath: string, href: Url): string { } } +const PAGE_LOAD_ERROR = Symbol('PAGE_LOAD_ERROR') +export function markLoadingError(err: Error): Error { + return Object.defineProperty(err, PAGE_LOAD_ERROR, {}) +} + function prepareUrlAs(router: NextRouter, url: Url, as: Url) { // If url and as provided as an object representation, // we'll format them into the string version here. @@ -205,7 +210,7 @@ function fetchNextData(dataHref: string, isServerRender: boolean) { // on a client-side transition. Otherwise, we'd get into an infinite // loop. if (!isServerRender) { - ;(err as any).code = 'PAGE_LOAD_ERROR' + markLoadingError(err) } throw err }) @@ -641,7 +646,7 @@ export default class Router implements BaseRouter { throw err } - if (err.code === 'PAGE_LOAD_ERROR' || loadErrorFail) { + if (PAGE_LOAD_ERROR in err || loadErrorFail) { Router.events.emit('routeChangeError', err, as) // If we can't load the page it could be one of following reasons diff --git a/test/integration/production/next.config.js b/test/integration/production/next.config.js index 4c11e179b1e04b1feebd49f4a8aaa54086be80a7..6570ba14b26d63afd22830f29969dccc22af7a7e 100644 --- a/test/integration/production/next.config.js +++ b/test/integration/production/next.config.js @@ -10,6 +10,16 @@ module.exports = { destination: '/:lang/about', permanent: false, }, + { + source: '/nonexistent', + destination: '/about', + permanent: false, + }, + { + source: '/shadowed-page', + destination: '/about', + permanent: false, + }, ] }, } diff --git a/test/integration/production/pages/shadowed-page.js b/test/integration/production/pages/shadowed-page.js new file mode 100644 index 0000000000000000000000000000000000000000..1fde3b29aee2db472e4c8c06ac6c0536017a0f6d --- /dev/null +++ b/test/integration/production/pages/shadowed-page.js @@ -0,0 +1,7 @@ +export function getServerSideProps() { + throw new Error('oops!') +} + +export default function ShadowedPage() { + return
Not routable!
+} diff --git a/test/integration/production/pages/to-nonexistent.js b/test/integration/production/pages/to-nonexistent.js new file mode 100644 index 0000000000000000000000000000000000000000..f92bdea30817d1eef8e22944b1d50c6b6678fa45 --- /dev/null +++ b/test/integration/production/pages/to-nonexistent.js @@ -0,0 +1,11 @@ +import Link from 'next/link' + +export default function ToNonexistent() { + return ( +
+ + Nonexistent Page + +
+ ) +} diff --git a/test/integration/production/pages/to-shadowed-page.js b/test/integration/production/pages/to-shadowed-page.js new file mode 100644 index 0000000000000000000000000000000000000000..01c5ee4e3a647231ff1b61917d5baca9797c99a7 --- /dev/null +++ b/test/integration/production/pages/to-shadowed-page.js @@ -0,0 +1,11 @@ +import Link from 'next/link' + +export default function ToShadowed() { + return ( +
+ + Shadowed Page + +
+ ) +} diff --git a/test/integration/production/test/index.test.js b/test/integration/production/test/index.test.js index 9868a02015de177d6c6d78d563dc95525f1e3346..f9daa94aa3541ad70232473955ac8050ffa99aa7 100644 --- a/test/integration/production/test/index.test.js +++ b/test/integration/production/test/index.test.js @@ -373,6 +373,31 @@ describe('Production Usage', () => { expect(title).toBe('hello from title') expect(url).toBe('/with-title') }) + + it('should reload page successfully (on bad link)', async () => { + const browser = await webdriver(appPort, '/to-nonexistent') + await browser.eval(function setup() { + window.__DATA_BE_GONE = 'true' + }) + await browser.waitForElementByCss('#to-nonexistent-page') + await browser.click('#to-nonexistent-page') + await browser.waitForElementByCss('.about-page') + + const oldData = await browser.eval(`window.__DATA_BE_GONE`) + expect(oldData).toBeFalsy() + }) + + it('should reload page successfully (on bad data fetch)', async () => { + const browser = await webdriver(appPort, '/to-shadowed-page') + await browser.eval(function setup() { + window.__DATA_BE_GONE = 'true' + }) + await browser.waitForElementByCss('#to-shadowed-page').click() + await browser.waitForElementByCss('.about-page') + + const oldData = await browser.eval(`window.__DATA_BE_GONE`) + expect(oldData).toBeFalsy() + }) }) it('should navigate to external site and back', async () => {