Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/build_and_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -910,10 +910,12 @@ jobs:
export IS_WEBPACK_TEST=1

BROWSER_NAME=firefox node run-tests.js \
test/production/pages-dir/production/test/index.test.ts
test/production/pages-dir/production/test/index.test.ts \
test/production/chunk-load-failure/chunk-load-failure.test.ts

NEXT_TEST_MODE=start BROWSER_NAME=safari node run-tests.js \
test/production/pages-dir/production/test/index.test.ts \
test/production/chunk-load-failure/chunk-load-failure.test.ts \
test/e2e/basepath/basepath.test.ts \
test/e2e/basepath/error-pages.test.ts

Expand Down
3 changes: 3 additions & 0 deletions test/production/chunk-load-failure/app/dynamic/async.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Async() {
return 'this is a lazy loaded async component'
}
16 changes: 16 additions & 0 deletions test/production/chunk-load-failure/app/dynamic/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
'use client'

import dynamic from 'next/dynamic'
import { Suspense } from 'react'

const Async = dynamic(() => import('./async'), { ssr: false })

export default function Page() {
return (
<>
<Suspense fallback={<div>Loading...</div>}>
<Async />
</Suspense>
</>
)
}
8 changes: 8 additions & 0 deletions test/production/chunk-load-failure/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default function Layout({ children }) {
return (
<html lang="en">
<head />
<body>{children}</body>
</html>
)
}
5 changes: 5 additions & 0 deletions test/production/chunk-load-failure/app/other/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use client'

export default function Page() {
return <>this is other</>
}
106 changes: 106 additions & 0 deletions test/production/chunk-load-failure/chunk-load-failure.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { nextTestSetup } from 'e2e-utils'
import { recursiveReadDir } from 'next/dist/lib/recursive-readdir'
import path from 'path'
import fs from 'fs'
import { retry } from 'next-test-utils'

describe('chunk-load-failure', () => {
const { next } = nextTestSetup({
files: __dirname,
})

async function getNextDynamicChunk() {
const chunksPath = path.join(next.testDir, '.next/static/')
const browserChunks = await recursiveReadDir(chunksPath, {
pathnameFilter: (f) => /\.js$/.test(f),
})
let nextDynamicChunks = browserChunks.filter((f) =>
fs
.readFileSync(path.join(chunksPath, f), 'utf8')
.includes('this is a lazy loaded async component')
)
expect(nextDynamicChunks).toHaveLength(1)

return nextDynamicChunks[0]
}

it('should report async chunk load failures', async () => {
let nextDynamicChunk = await getNextDynamicChunk()

let pageError: Error | undefined
const browser = await next.browser('/dynamic', {
beforePageLoad(page) {
page.route('**/' + nextDynamicChunk, async (route) => {
await route.abort('connectionreset')
})
page.on('pageerror', (error: Error) => {
pageError = error
})
},
})

await retry(async () => {
const body = await browser.elementByCss('body')
expect(await body.text()).toMatch(
/Application error: a client-side exception has occurred while loading/
)
})

expect(pageError).toBeDefined()
expect(pageError.name).toBe('ChunkLoadError')
if (process.env.IS_TURBOPACK_TEST) {
expect(pageError.message).toStartWith(
'Failed to load chunk /_next/static/' + nextDynamicChunk
)
} else {
expect(pageError.message).toMatch(/^Loading chunk \S+ failed./)
expect(pageError.message).toContain('/_next/static/' + nextDynamicChunk)
}
})

it('should report aborted chunks when navigating away', async () => {
let nextDynamicChunk = await getNextDynamicChunk()

let resolve
try {
const browser = await next.browser('/dynamic', {
beforePageLoad(page) {
page.route('**/' + nextDynamicChunk, async (route) => {
// deterministically ensure that the async chunk is still loading during the navigation
await new Promise((r) => {
resolve = r
})
})
page.on('pageerror', (error: Error) => {
console.log('pageerror', error)
})
},
})

await browser.get(next.url + '/other')

let body = await browser.elementByCss('body')
expect(await body.text()).toMatch('this is other')

const browserLogs = (await browser.log()).filter(
(m) => m.source === 'warning' || m.source === 'error'
)

if (process.env.BROWSER_NAME === 'firefox') {
expect(browserLogs).toContainEqual(
expect.objectContaining({
message: expect.stringContaining(
'Loading failed for the <script> with source'
),
})
)
} else {
// Chrome and Safari doesn't show any errors or warnings here
expect(browserLogs).toBeEmpty()
}
} finally {
// prevent hanging
resolve?.()
}
})
})
4 changes: 4 additions & 0 deletions test/production/chunk-load-failure/next.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {}

export default nextConfig
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ function loadChunkByUrlInternal(
thenable,
loadedChunk
)
entry = thenable.then(resolve).catch((error) => {
entry = thenable.then(resolve).catch((cause) => {
let loadReason: string
switch (sourceType) {
case SourceType.Runtime:
Expand All @@ -268,16 +268,14 @@ function loadChunkByUrlInternal(
(sourceType) => `Unknown source type: ${sourceType}`
)
}
throw new Error(
let error = new Error(
`Failed to load chunk ${chunkUrl} ${loadReason}${
error ? `: ${error}` : ''
cause ? `: ${cause}` : ''
}`,
error
? {
cause: error,
}
: undefined
cause ? { cause } : undefined
)
error.name = 'ChunkLoadError'
throw error
})
instrumentedBackendLoadChunks.set(thenable, entry)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,16 +99,16 @@ function loadRuntimeChunkPath(
const chunkModules: CompressedModuleFactories = require(resolved)
installCompressedModuleFactories(chunkModules, 0, moduleFactories)
loadedChunks.add(chunkPath)
} catch (e) {
} catch (cause) {
let errorMessage = `Failed to load chunk ${chunkPath}`

if (sourcePath) {
errorMessage += ` from runtime for chunk ${sourcePath}`
}

throw new Error(errorMessage, {
cause: e,
})
const error = new Error(errorMessage, { cause })
error.name = 'ChunkLoadError'
throw error
}
}

Expand All @@ -133,15 +133,13 @@ function loadChunkAsync(
const chunkModules: CompressedModuleFactories = require(resolved)
installCompressedModuleFactories(chunkModules, 0, moduleFactories)
entry = loadedChunk
} catch (e) {
} catch (cause) {
const errorMessage = `Failed to load chunk ${chunkPath} from module ${this.m.id}`
const error = new Error(errorMessage, { cause })
error.name = 'ChunkLoadError'

// Cache the failure promise, future requests will also get this same rejection
entry = Promise.reject(
new Error(errorMessage, {
cause: e,
})
)
entry = Promise.reject(error)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
entry = Promise.reject(error)
entry = Promise.resolve().then(() => { throw error })

This is important as Promise.reject leads to a unhandled rejection even with catch

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that was a preexisting problem though then? I didn't change that part?

}
chunkCache.set(chunkPath, entry)
}
Expand Down

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Large diffs are not rendered by default.

Loading