mirror of
https://github.com/terribleplan/next.js.git
synced 2024-01-19 02:48:18 +00:00
Handle BUILD_ID mismatch error (#1224)
* Reload the page if the buildIds are mismatch. * Reload the browser with main.js and commons.js buildId mismatch. * Implement proper reloading with an API to persist the state. * Add some tests for force reload. * Change _reload to _forceReload. * Add a section about reload hooks to the README. * Allow to add a hook to handle BUILD_ID mismatch. * Remove readme docs. * Do not show a custom error to the user. * Cancel the routing when there's a BUILD_ID mismatch. * Fix a typo. * Passing route to SingletonRouter.onBuildIdMismatch * Handle buildId mismatch automatically.
This commit is contained in:
parent
20c7d98efe
commit
0bd250f4aa
|
@ -1,3 +1,4 @@
|
||||||
|
/* global window, location */
|
||||||
import _Router from './router'
|
import _Router from './router'
|
||||||
|
|
||||||
const SingletonRouter = {
|
const SingletonRouter = {
|
||||||
|
@ -75,3 +76,11 @@ export const createRouter = function (...args) {
|
||||||
|
|
||||||
// Export the actual Router class, which is usually used inside the server
|
// Export the actual Router class, which is usually used inside the server
|
||||||
export const Router = _Router
|
export const Router = _Router
|
||||||
|
|
||||||
|
export function _notifyBuildIdMismatch (nextRoute) {
|
||||||
|
if (SingletonRouter.onAppUpdated) {
|
||||||
|
SingletonRouter.onAppUpdated(nextRoute)
|
||||||
|
} else {
|
||||||
|
location.href = nextRoute
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import evalScript from '../eval-script'
|
||||||
import shallowEquals from '../shallow-equals'
|
import shallowEquals from '../shallow-equals'
|
||||||
import PQueue from '../p-queue'
|
import PQueue from '../p-queue'
|
||||||
import { loadGetInitialProps, getLocationOrigin } from '../utils'
|
import { loadGetInitialProps, getLocationOrigin } from '../utils'
|
||||||
|
import { _notifyBuildIdMismatch } from './'
|
||||||
|
|
||||||
// Add "fetch" polyfill for older browsers
|
// Add "fetch" polyfill for older browsers
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
|
@ -75,7 +76,7 @@ export default class Router extends EventEmitter {
|
||||||
data,
|
data,
|
||||||
props,
|
props,
|
||||||
error
|
error
|
||||||
} = await this.getRouteInfo(route, pathname, query)
|
} = await this.getRouteInfo(route, pathname, query, as)
|
||||||
|
|
||||||
if (error && error.cancelled) {
|
if (error && error.cancelled) {
|
||||||
this.emit('routeChangeError', error, as)
|
this.emit('routeChangeError', error, as)
|
||||||
|
@ -116,7 +117,7 @@ export default class Router extends EventEmitter {
|
||||||
data,
|
data,
|
||||||
props,
|
props,
|
||||||
error
|
error
|
||||||
} = await this.getRouteInfo(route, pathname, query)
|
} = await this.getRouteInfo(route, pathname, query, url)
|
||||||
|
|
||||||
if (error && error.cancelled) {
|
if (error && error.cancelled) {
|
||||||
this.emit('routeChangeError', error, url)
|
this.emit('routeChangeError', error, url)
|
||||||
|
@ -162,7 +163,7 @@ export default class Router extends EventEmitter {
|
||||||
this.emit('routeChangeStart', as)
|
this.emit('routeChangeStart', as)
|
||||||
const {
|
const {
|
||||||
data, props, error
|
data, props, error
|
||||||
} = await this.getRouteInfo(route, pathname, query)
|
} = await this.getRouteInfo(route, pathname, query, as)
|
||||||
|
|
||||||
if (error && error.cancelled) {
|
if (error && error.cancelled) {
|
||||||
this.emit('routeChangeError', error, as)
|
this.emit('routeChangeError', error, as)
|
||||||
|
@ -189,11 +190,16 @@ export default class Router extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRouteInfo (route, pathname, query) {
|
async getRouteInfo (route, pathname, query, as) {
|
||||||
const routeInfo = {}
|
const routeInfo = {}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { Component, err, jsonPageRes } = routeInfo.data = await this.fetchComponent(route)
|
routeInfo.data = await this.fetchComponent(route, as)
|
||||||
|
if (!routeInfo.data) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const { Component, err, jsonPageRes } = routeInfo.data
|
||||||
const ctx = { err, pathname, query, jsonPageRes }
|
const ctx = { err, pathname, query, jsonPageRes }
|
||||||
routeInfo.props = await this.getInitialProps(Component, ctx)
|
routeInfo.props = await this.getInitialProps(Component, ctx)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -229,7 +235,7 @@ export default class Router extends EventEmitter {
|
||||||
return this.prefetchQueue.add(() => this.fetchRoute(route))
|
return this.prefetchQueue.add(() => this.fetchRoute(route))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchComponent (route) {
|
async fetchComponent (route, as) {
|
||||||
let data = this.components[route]
|
let data = this.components[route]
|
||||||
if (data) return data
|
if (data) return data
|
||||||
|
|
||||||
|
@ -240,6 +246,15 @@ export default class Router extends EventEmitter {
|
||||||
|
|
||||||
const jsonPageRes = await this.fetchRoute(route)
|
const jsonPageRes = await this.fetchRoute(route)
|
||||||
const jsonData = await jsonPageRes.json()
|
const jsonData = await jsonPageRes.json()
|
||||||
|
|
||||||
|
if (jsonData.buildIdMismatch) {
|
||||||
|
_notifyBuildIdMismatch(as)
|
||||||
|
|
||||||
|
const error = Error('Abort due to BUILD_ID mismatch')
|
||||||
|
error.cancelled = true
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
const newData = {
|
const newData = {
|
||||||
...loadComponent(jsonData),
|
...loadComponent(jsonData),
|
||||||
jsonPageRes
|
jsonPageRes
|
||||||
|
|
12
readme.md
12
readme.md
|
@ -310,6 +310,7 @@ Here's a list of supported events:
|
||||||
- `routeChangeStart(url)` - Fires when a route starts to change
|
- `routeChangeStart(url)` - Fires when a route starts to change
|
||||||
- `routeChangeComplete(url)` - Fires when a route changed completely
|
- `routeChangeComplete(url)` - Fires when a route changed completely
|
||||||
- `routeChangeError(err, url)` - Fires when there's an error when changing routes
|
- `routeChangeError(err, url)` - Fires when there's an error when changing routes
|
||||||
|
- `appUpdated(nextRoute)` - Fires when switching pages and there's a new version of the app
|
||||||
|
|
||||||
> Here `url` is the URL shown in the browser. If you call `Router.push(url, as)` (or similar), then the value of `url` will be `as`.
|
> Here `url` is the URL shown in the browser. If you call `Router.push(url, as)` (or similar), then the value of `url` will be `as`.
|
||||||
|
|
||||||
|
@ -337,6 +338,17 @@ Router.onRouteChangeError = (err, url) => {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you change a route while in between a new deployment, we can't navigate the app via client side. We need to do a full browser navigation. We do it automatically for you.
|
||||||
|
|
||||||
|
But you can customize that via `Route.onAppUpdated` event like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
Router.onAppUpdated = (nextUrl) => {
|
||||||
|
// persist the local state
|
||||||
|
location.href = nextUrl
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Prefetching Pages
|
### Prefetching Pages
|
||||||
|
|
||||||
<p><details>
|
<p><details>
|
||||||
|
|
|
@ -81,19 +81,30 @@ export default class Server {
|
||||||
},
|
},
|
||||||
|
|
||||||
'/_next/:buildId/main.js': async (req, res, params) => {
|
'/_next/:buildId/main.js': async (req, res, params) => {
|
||||||
this.handleBuildId(params.buildId, res)
|
if (!this.handleBuildId(params.buildId, res)) {
|
||||||
|
throwBuildIdMismatchError()
|
||||||
|
}
|
||||||
|
|
||||||
const p = join(this.dir, '.next/main.js')
|
const p = join(this.dir, '.next/main.js')
|
||||||
await this.serveStatic(req, res, p)
|
await this.serveStatic(req, res, p)
|
||||||
},
|
},
|
||||||
|
|
||||||
'/_next/:buildId/commons.js': async (req, res, params) => {
|
'/_next/:buildId/commons.js': async (req, res, params) => {
|
||||||
this.handleBuildId(params.buildId, res)
|
if (!this.handleBuildId(params.buildId, res)) {
|
||||||
|
throwBuildIdMismatchError()
|
||||||
|
}
|
||||||
|
|
||||||
const p = join(this.dir, '.next/commons.js')
|
const p = join(this.dir, '.next/commons.js')
|
||||||
await this.serveStatic(req, res, p)
|
await this.serveStatic(req, res, p)
|
||||||
},
|
},
|
||||||
|
|
||||||
'/_next/:buildId/pages/:path*': async (req, res, params) => {
|
'/_next/:buildId/pages/:path*': async (req, res, params) => {
|
||||||
this.handleBuildId(params.buildId, res)
|
if (!this.handleBuildId(params.buildId, res)) {
|
||||||
|
res.setHeader('Content-Type', 'application/json')
|
||||||
|
res.end(JSON.stringify({ buildIdMismatch: true }))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const paths = params.path || ['index']
|
const paths = params.path || ['index']
|
||||||
const pathname = `/${paths.join('/')}`
|
const pathname = `/${paths.join('/')}`
|
||||||
await this.renderJSON(req, res, pathname)
|
await this.renderJSON(req, res, pathname)
|
||||||
|
@ -277,14 +288,13 @@ export default class Server {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleBuildId (buildId, res) {
|
handleBuildId (buildId, res) {
|
||||||
if (this.dev) return
|
if (this.dev) return true
|
||||||
if (buildId !== this.renderOpts.buildId) {
|
if (buildId !== this.renderOpts.buildId) {
|
||||||
const errorMessage = 'Build id mismatch!' +
|
return false
|
||||||
'Seems like the server and the client version of files are not the same.'
|
|
||||||
throw new Error(errorMessage)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res.setHeader('Cache-Control', 'max-age=365000000, immutable')
|
res.setHeader('Cache-Control', 'max-age=365000000, immutable')
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
getCompilationError (page) {
|
getCompilationError (page) {
|
||||||
|
@ -298,3 +308,7 @@ export default class Server {
|
||||||
if (p) return errors.get(p)[0]
|
if (p) return errors.get(p)[0]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function throwBuildIdMismatchError () {
|
||||||
|
throw new Error('BUILD_ID Mismatched!')
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/* global describe, test, expect */
|
/* global describe, test, expect */
|
||||||
|
|
||||||
export default function ({ app }) {
|
export default function (context) {
|
||||||
describe('Misc', () => {
|
describe('Misc', () => {
|
||||||
test('finishes response', async () => {
|
test('finishes response', async () => {
|
||||||
const res = {
|
const res = {
|
||||||
|
@ -9,7 +9,7 @@ export default function ({ app }) {
|
||||||
this.finished = true
|
this.finished = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const html = await app.renderToHTML({}, res, '/finish-response', {})
|
const html = await context.app.renderToHTML({}, res, '/finish-response', {})
|
||||||
expect(html).toBeFalsy()
|
expect(html).toBeFalsy()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue