1
0
Fork 0
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:
Arunoda Susiripala 2017-02-21 05:18:17 +05:30 committed by Guillermo Rauch
parent 20c7d98efe
commit 0bd250f4aa
5 changed files with 65 additions and 15 deletions

View file

@ -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
}
}

View file

@ -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

View file

@ -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>

View file

@ -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!')
}

View file

@ -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()
}) })
}) })