diff --git a/packages/next/client/index.tsx b/packages/next/client/index.tsx index 0f2c6ccbf33bd..8115aab7519d6 100644 --- a/packages/next/client/index.tsx +++ b/packages/next/client/index.tsx @@ -744,7 +744,7 @@ if (process.env.__NEXT_RSC) { let response = rscCache.get(cacheKey) if (response) return response - const bufferCacheKey = cacheKey + ',' + id + const bufferCacheKey = cacheKey + ',' + router.route + ',' + id if (serverDataBuffer.has(bufferCacheKey)) { const t = new TransformStream() const writer = t.writable.getWriter() diff --git a/packages/next/client/page-loader.ts b/packages/next/client/page-loader.ts index d4119a3b2767c..0ec4e6945827b 100644 --- a/packages/next/client/page-loader.ts +++ b/packages/next/client/page-loader.ts @@ -148,7 +148,7 @@ export default class PageLoader { const getHrefForSlug = (path: string) => { if (rsc) { - return path + search + (search ? `&` : '?') + '__flight__' + return path + search + (search ? `&` : '?') + '__flight__=1' } const dataRoute = getAssetPathFromRoute( diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts index a0ff7eb45b242..180768e6d354a 100644 --- a/packages/next/server/base-server.ts +++ b/packages/next/server/base-server.ts @@ -1126,9 +1126,13 @@ export default abstract class Server { // Toggle whether or not this is a Data request const isDataReq = !!query._nextDataReq && (isSSG || hasServerProps) delete query._nextDataReq + // Don't delete query.__flight__ yet, it still needs to be used in renderToHTML later + const isFlightRequest = Boolean( + this.serverComponentManifest && query.__flight__ + ) // we need to ensure the status code if /404 is visited directly - if (is404Page && !isDataReq) { + if (is404Page && !isDataReq && !isFlightRequest) { res.statusCode = 404 } diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index 7976f6c7eda06..0d77d18f5f0dc 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -477,8 +477,9 @@ export async function renderToHTML( if (isServerComponent) { serverComponentsInlinedTransformStream = new TransformStream() + const search = stringifyQuery(query) Component = createServerComponentRenderer(App, OriginalComponent, { - cachePrefix: pathname + '?' + stringifyQuery(query), + cachePrefix: pathname + (search ? `?${search}` : ''), transformStream: serverComponentsInlinedTransformStream, serverComponentManifest, runtime, diff --git a/test/integration/react-streaming-and-server-components/app/pages/404.js b/test/integration/react-streaming-and-server-components/app/pages/404.js index fd264d6b7ed87..9b643c13f00b9 100644 --- a/test/integration/react-streaming-and-server-components/app/pages/404.js +++ b/test/integration/react-streaming-and-server-components/app/pages/404.js @@ -17,8 +17,7 @@ function Data() { export default function Page404() { return ( - custom-404-page -
+ custom-404-page
) diff --git a/test/integration/react-streaming-and-server-components/test/basic.js b/test/integration/react-streaming-and-server-components/test/basic.js index d9fe204a5cf79..468108ffe94b1 100644 --- a/test/integration/react-streaming-and-server-components/test/basic.js +++ b/test/integration/react-streaming-and-server-components/test/basic.js @@ -1,6 +1,7 @@ +import webdriver from 'next-webdriver' import { renderViaHTTP } from 'next-test-utils' -export default async function basic(context, env) { +export default async function basic(context) { it('should render 404 error correctly', async () => { const path404HTML = await renderViaHTTP(context.appPort, '/404') const pathNotFoundHTML = await renderViaHTTP(context.appPort, '/not-found') @@ -27,4 +28,11 @@ export default async function basic(context, env) { const res = await renderViaHTTP(context.appPort, '/api/ping') expect(res).toContain('pong') }) + + it('should handle suspense error page correctly (node stream)', async () => { + const browser = await webdriver(context.appPort, '/404') + const hydrationContent = await browser.waitForElementByCss('#__next').text() + + expect(hydrationContent).toBe('custom-404-pagenext_streaming_data') + }) } diff --git a/test/integration/react-streaming-and-server-components/test/index.test.js b/test/integration/react-streaming-and-server-components/test/index.test.js index c0733b50468b5..6777fa461c88a 100644 --- a/test/integration/react-streaming-and-server-components/test/index.test.js +++ b/test/integration/react-streaming-and-server-components/test/index.test.js @@ -27,7 +27,6 @@ const documentPage = new File(join(appDir, 'pages/_document.jsx')) const appPage = new File(join(appDir, 'pages/_app.js')) const appServerPage = new File(join(appDir, 'pages/_app.server.js')) const error500Page = new File(join(appDir, 'pages/500.js')) -const error404Page = new File(join(appDir, 'pages/404.js')) const nextConfig = new File(join(appDir, 'next.config.js')) const documentWithGip = ` @@ -73,33 +72,6 @@ export default function Page500() { } ` -const suspense404 = ` -import { Suspense } from 'react' - -let result -let promise -function Data() { - if (result) return result - if (!promise) - promise = new Promise((res) => { - setTimeout(() => { - result = 'next_streaming_data' - res() - }, 500) - }) - throw promise -} - -export default function Page404() { - return ( - - custom-404-page - - - ) -} -` - describe('Edge runtime - basic', () => { it('should warn user for experimental risk with server components', async () => { const edgeRuntimeWarning = @@ -116,20 +88,6 @@ describe('Edge runtime - basic', () => { const { stderr } = await nextBuild(nativeModuleTestAppDir) expect(stderr).toContain(fsImportedErrorMessage) }) - - it('should handle suspense error page correctly (node stream)', async () => { - error404Page.write(suspense404) - const appPort = await findPort() - await nextBuild(appDir) - await nextStart(appDir, appPort) - const browser = await webdriver(appPort, '/404') - const hydrationContent = await browser.eval( - `document.querySelector('#__next').textContent` - ) - expect(hydrationContent).toBe('custom-404-pagenext_streaming_data') - - error404Page.restore() - }) }) describe('Edge runtime - prod', () => { @@ -200,8 +158,8 @@ describe('Edge runtime - prod', () => { expect(path500HTML).toContain('custom-500-page') }) - basic(context, 'prod') - rsc(context) + basic(context) + rsc(context, 'edge') streaming(context) }) @@ -246,15 +204,16 @@ describe('Edge runtime - dev', () => { expect(path500HTML).toContain('Error: oops') }) - basic(context, 'dev') - rsc(context) + basic(context) + rsc(context, 'edge') streaming(context) }) const nodejsRuntimeBasicSuite = { runTests: (context, env) => { - basic(context, env) + basic(context) streaming(context) + rsc(context, 'nodejs') if (env === 'prod') { it('should generate middleware SSR manifests for Node.js', async () => { diff --git a/test/integration/react-streaming-and-server-components/test/rsc.js b/test/integration/react-streaming-and-server-components/test/rsc.js index 7fe0bfc227163..77cee8d228c47 100644 --- a/test/integration/react-streaming-and-server-components/test/rsc.js +++ b/test/integration/react-streaming-and-server-components/test/rsc.js @@ -3,7 +3,12 @@ import webdriver from 'next-webdriver' import cheerio from 'cheerio' import { renderViaHTTP, check } from 'next-test-utils' -export default function (context) { +function getNodeBySelector(html, selector) { + const $ = cheerio.load(html) + return $(selector) +} + +export default function (context, runtime) { it('should render server components correctly', async () => { const homeHTML = await renderViaHTTP(context.appPort, '/', null, { headers: { @@ -29,8 +34,10 @@ export default function (context) { it('should support next/link in server components', async () => { const linkHTML = await renderViaHTTP(context.appPort, '/next-api/link') - const $ = cheerio.load(linkHTML) - const linkText = $('div[hidden] > a[href="/"]').text() + const linkText = getNodeBySelector( + linkHTML, + 'div[hidden] > a[href="/"]' + ).text() expect(linkText).toContain('go home') @@ -52,25 +59,36 @@ export default function (context) { expect(await browser.eval('window.beforeNav')).toBe(1) }) - it('should suspense next/image in server components', async () => { - const imageHTML = await renderViaHTTP(context.appPort, '/next-api/image') - const $ = cheerio.load(imageHTML) - const imageTag = $('div[hidden] > span > span > img') + // Disable next/image for nodejs runtime temporarily + if (runtime === 'edge') { + it('should suspense next/image in server components', async () => { + const imageHTML = await renderViaHTTP(context.appPort, '/next-api/image') + const imageTag = getNodeBySelector( + imageHTML, + 'div[hidden] > span > span > img' + ) - expect(imageTag.attr('src')).toContain('data:image') - }) + expect(imageTag.attr('src')).toContain('data:image') + }) + } it('should handle multiple named exports correctly', async () => { const clientExportsHTML = await renderViaHTTP( context.appPort, '/client-exports' ) - const $clientExports = cheerio.load(clientExportsHTML) - expect($clientExports('div[hidden] > div > #named-exports').text()).toBe( - 'abcde' - ) + + expect( + getNodeBySelector( + clientExportsHTML, + 'div[hidden] > div > #named-exports' + ).text() + ).toBe('abcde') expect( - $clientExports('div[hidden] > div > #default-exports-arrow').text() + getNodeBySelector( + clientExportsHTML, + 'div[hidden] > div > #default-exports-arrow' + ).text() ).toBe('client-default-export-arrow') const browser = await webdriver(context.appPort, '/client-exports') @@ -83,4 +101,22 @@ export default function (context) { expect(textNamedExports).toBe('abcde') expect(textDefaultExportsArrow).toBe('client-default-export-arrow') }) + + it('should handle 404 requests and missing routes correctly', async () => { + const id = '#text' + const content = 'custom-404-page' + const page404HTML = await renderViaHTTP(context.appPort, '/404') + const pageUnknownHTML = await renderViaHTTP(context.appPort, '/no.where') + + const page404Browser = await webdriver(context.appPort, '/404') + const pageUnknownBrowser = await webdriver(context.appPort, '/no.where') + + expect(await page404Browser.waitForElementByCss(id).text()).toBe(content) + expect(await pageUnknownBrowser.waitForElementByCss(id).text()).toBe( + content + ) + + expect(getNodeBySelector(page404HTML, id).text()).toBe(content) + expect(getNodeBySelector(pageUnknownHTML, id).text()).toBe(content) + }) }