Ensure timed out prefetches are cleaned up correctly (#28899)

This applies the fix from the awesome investigation done in https://github.com/vercel/next.js/issues/28797 by @jayphelps and adds a test to ensure this is working as expected. It seems that the `route-loader` has a race condition while prefetching and if a script is executed before we have created a current "future" entry to resolve the entry stays in a pending state causing routes to hang so this handles the condition by ensuring pending/errored entries do not stay around. 

## Bug

- [x] Related issues linked using `fixes #number`
- [x] Integration tests added
- [x] Errors have helpful link attached, see `contributing.md`

Fixes: https://github.com/vercel/next.js/issues/28797
Fixes: https://github.com/vercel/next.js/issues/27783
This commit is contained in:
JJ Kasper 2021-09-08 01:37:04 -05:00 committed by GitHub
parent 4f8d883acd
commit b71df190e5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 63 additions and 18 deletions

View file

@ -60,8 +60,13 @@ function withFuture<T>(
}) })
map.set(key, (entry = { resolve: resolver!, future: prom })) map.set(key, (entry = { resolve: resolver!, future: prom }))
return generator return generator
? // eslint-disable-next-line no-sequences ? generator()
generator().then((value) => (resolver(value), value)) // eslint-disable-next-line no-sequences
.then((value) => (resolver(value), value))
.catch((err) => {
map.delete(key)
throw err
})
: prom : prom
} }

View file

@ -1 +1 @@
export default () => <p>Hello world</p> export default () => <p id="another">Hello world</p>

View file

@ -1,12 +1,14 @@
/* eslint-env jest */ /* eslint-env jest */
import { import {
nextServer, findPort,
runNextCommand, killApp,
startApp, nextBuild,
stopApp, nextStart,
waitFor, waitFor,
} from 'next-test-utils' } from 'next-test-utils'
import http from 'http'
import httpProxy from 'http-proxy'
import webdriver from 'next-webdriver' import webdriver from 'next-webdriver'
import { join } from 'path' import { join } from 'path'
import { readFile } from 'fs-extra' import { readFile } from 'fs-extra'
@ -14,24 +16,62 @@ import { readFile } from 'fs-extra'
jest.setTimeout(1000 * 60 * 5) jest.setTimeout(1000 * 60 * 5)
const appDir = join(__dirname, '../') const appDir = join(__dirname, '../')
let appPort
let server
let app let app
let appPort
let stallJs
let proxyServer
describe('Prefetching Links in viewport', () => { describe('Prefetching Links in viewport', () => {
beforeAll(async () => { beforeAll(async () => {
await runNextCommand(['build', appDir]) await nextBuild(appDir)
const port = await findPort()
app = await nextStart(appDir, port)
appPort = await findPort()
app = nextServer({ const proxy = httpProxy.createProxyServer({
dir: join(__dirname, '../'), target: `http://localhost:${port}`,
dev: false,
quiet: true,
}) })
server = await startApp(app) proxyServer = http.createServer(async (req, res) => {
appPort = server.address().port if (stallJs && req.url.includes('chunks/pages/another')) {
console.log('stalling request for', req.url)
await new Promise((resolve) => setTimeout(resolve, 5 * 1000))
}
proxy.web(req, res)
})
proxy.on('error', (err) => {
console.warn('Failed to proxy', err)
})
await new Promise((resolve) => {
proxyServer.listen(appPort, () => resolve())
})
})
afterAll(async () => {
await killApp(app)
proxyServer.close()
})
it('should handle timed out prefetch correctly', async () => {
try {
stallJs = true
const browser = await webdriver(appPort, '/')
await browser.elementByCss('#scroll-to-another').click()
// wait for preload to timeout
await waitFor(6 * 1000)
await browser
.elementByCss('#link-another')
.click()
.waitForElementByCss('#another')
expect(await browser.elementByCss('#another').text()).toBe('Hello world')
} finally {
stallJs = false
}
}) })
afterAll(() => stopApp(server))
it('should prefetch with link in viewport onload', async () => { it('should prefetch with link in viewport onload', async () => {
let browser let browser
@ -280,6 +320,7 @@ describe('Prefetching Links in viewport', () => {
expect(await browser.eval('window.hadUnhandledReject')).toBeFalsy() expect(await browser.eval('window.hadUnhandledReject')).toBeFalsy()
await browser.waitForElementByCss('#invalid-link')
await browser.elementByCss('#invalid-link').moveTo() await browser.elementByCss('#invalid-link').moveTo()
expect(await browser.eval('window.hadUnhandledReject')).toBeFalsy() expect(await browser.eval('window.hadUnhandledReject')).toBeFalsy()
}) })

View file

@ -20,7 +20,6 @@ const appDir = join(__dirname, '../')
function runTests() { function runTests() {
it('should cancel slow page loads on re-navigation', async () => { it('should cancel slow page loads on re-navigation', async () => {
const browser = await webdriver(appPort, '/') const browser = await webdriver(appPort, '/')
await waitFor(5000)
await browser.elementByCss('#link-1').click() await browser.elementByCss('#link-1').click()
await waitFor(3000) await waitFor(3000)