From 8ef2905b304e04a5068f9a6eae0c143aaaff24a4 Mon Sep 17 00:00:00 2001 From: Mike DiDomizio Date: Mon, 29 May 2023 18:06:25 -0400 Subject: [PATCH] Revert "feat: make `enableUndici: true` default and remove `node-fetch` (#49061)" This reverts commit e6acd40cba2c565919ee3e94bb9e8a100dc69495. --- .../js/src/polyfill/app-polyfills.ts | 1 + packages/next/src/build/index.ts | 2 +- packages/next/src/build/utils.ts | 3 + packages/next/src/cli/next-info.ts | 4 +- packages/next/src/export/index.ts | 1 + packages/next/src/export/worker.ts | 3 + packages/next/src/lib/download-wasm-swc.ts | 30 +-- packages/next/src/server/config-schema.ts | 3 + packages/next/src/server/config-shared.ts | 2 + packages/next/src/server/config.ts | 37 +++- .../next/src/server/dev/next-dev-server.ts | 2 + .../src/server/dev/static-paths-worker.ts | 3 + .../next/src/server/node-polyfill-fetch.ts | 39 ++-- packages/next/src/telemetry/post-payload.ts | 9 +- packages/next/taskfile-swc.js | 4 +- test/e2e/undici-fetch/index.test.ts | 177 ++++++++++++------ .../test/index.test.js | 4 +- .../undici-keep-alive/next.config.js | 5 + .../undici-keep-alive/pages/api/json.js | 5 + .../undici-keep-alive/pages/blog/[slug].js | 23 +++ .../undici-keep-alive/pages/ssg.js | 9 + .../undici-keep-alive/pages/ssr.js | 9 + .../undici-keep-alive/test/index.test.js | 99 ++++++++++ 23 files changed, 359 insertions(+), 115 deletions(-) create mode 100644 test/integration/undici-keep-alive/next.config.js create mode 100644 test/integration/undici-keep-alive/pages/api/json.js create mode 100644 test/integration/undici-keep-alive/pages/blog/[slug].js create mode 100644 test/integration/undici-keep-alive/pages/ssg.js create mode 100644 test/integration/undici-keep-alive/pages/ssr.js create mode 100644 test/integration/undici-keep-alive/test/index.test.js diff --git a/packages/next-swc/crates/next-core/js/src/polyfill/app-polyfills.ts b/packages/next-swc/crates/next-core/js/src/polyfill/app-polyfills.ts index 7016090b6b70..20188edb6faa 100644 --- a/packages/next-swc/crates/next-core/js/src/polyfill/app-polyfills.ts +++ b/packages/next-swc/crates/next-core/js/src/polyfill/app-polyfills.ts @@ -2,3 +2,4 @@ import 'next/dist/server/node-polyfill-fetch' import 'next/dist/server/node-polyfill-web-streams' import 'next/dist/server/node-polyfill-headers' import './async-local-storage' +;(globalThis as any).__NEXT_USE_UNDICI = true diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index efabbb5fbbfd..c5a7ac240617 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -1263,6 +1263,7 @@ export default async function build( configFileName, runtimeEnvConfig, httpAgentOptions: config.httpAgentOptions, + enableUndici: config.experimental.enableUndici, locales: config.i18n?.locales, defaultLocale: config.i18n?.defaultLocale, nextConfigOutput: config.output, @@ -1422,7 +1423,6 @@ export default async function build( if (pageType === 'app' || !isReservedPage(page)) { try { let edgeInfo: any - if (isEdgeRuntime(pageRuntime)) { if (pageType === 'app') { edgeRuntimeAppCount++ diff --git a/packages/next/src/build/utils.ts b/packages/next/src/build/utils.ts index cbb7a221e4ed..aacf8c5b1650 100644 --- a/packages/next/src/build/utils.ts +++ b/packages/next/src/build/utils.ts @@ -1327,6 +1327,7 @@ export async function isPageStatic({ configFileName, runtimeEnvConfig, httpAgentOptions, + enableUndici, locales, defaultLocale, parentId, @@ -1344,6 +1345,7 @@ export async function isPageStatic({ configFileName: string runtimeEnvConfig: any httpAgentOptions: NextConfigComplete['httpAgentOptions'] + enableUndici?: NextConfigComplete['experimental']['enableUndici'] locales?: string[] defaultLocale?: string parentId?: any @@ -1376,6 +1378,7 @@ export async function isPageStatic({ require('../shared/lib/runtime-config').setConfig(runtimeEnvConfig) setHttpClientAndAgentOptions({ httpAgentOptions, + experimental: { enableUndici }, }) let componentsResult: LoadComponentsReturnType diff --git a/packages/next/src/cli/next-info.ts b/packages/next/src/cli/next-info.ts index 6cec344a78d2..982d3655a2e5 100755 --- a/packages/next/src/cli/next-info.ts +++ b/packages/next/src/cli/next-info.ts @@ -4,9 +4,7 @@ import childProcess from 'child_process' import chalk from 'next/dist/compiled/chalk' import arg from 'next/dist/compiled/arg/index.js' -const { fetch } = require('next/dist/compiled/undici') as { - fetch: typeof global.fetch -} +import fetch from 'next/dist/compiled/node-fetch' import { printAndExit } from '../server/lib/utils' import { CliCommand } from '../lib/commands' import isError from '../lib/is-error' diff --git a/packages/next/src/export/index.ts b/packages/next/src/export/index.ts index 0adfb2019d2a..81b6d2bbec8a 100644 --- a/packages/next/src/export/index.ts +++ b/packages/next/src/export/index.ts @@ -725,6 +725,7 @@ export default async function exportApp( parentSpanId: pageExportSpan.id, httpAgentOptions: nextConfig.httpAgentOptions, serverComponents: options.hasAppDir, + enableUndici: nextConfig.experimental.enableUndici, debugOutput: options.debugOutput, isrMemoryCacheSize: nextConfig.experimental.isrMemoryCacheSize, fetchCache: nextConfig.experimental.appDir, diff --git a/packages/next/src/export/worker.ts b/packages/next/src/export/worker.ts index ecf7db9028f3..23e747c4e782 100644 --- a/packages/next/src/export/worker.ts +++ b/packages/next/src/export/worker.ts @@ -80,6 +80,7 @@ interface ExportPageInput { parentSpanId: any httpAgentOptions: NextConfigComplete['httpAgentOptions'] serverComponents?: boolean + enableUndici: NextConfigComplete['experimental']['enableUndici'] debugOutput?: boolean isrMemoryCacheSize?: NextConfigComplete['experimental']['isrMemoryCacheSize'] fetchCache?: boolean @@ -137,6 +138,7 @@ export default async function exportPage({ disableOptimizedLoading, httpAgentOptions, serverComponents, + enableUndici, debugOutput, isrMemoryCacheSize, fetchCache, @@ -145,6 +147,7 @@ export default async function exportPage({ }: ExportPageInput): Promise { setHttpClientAndAgentOptions({ httpAgentOptions, + experimental: { enableUndici }, }) const exportPageSpan = trace('export-page-worker', parentSpanId) diff --git a/packages/next/src/lib/download-wasm-swc.ts b/packages/next/src/lib/download-wasm-swc.ts index 3cd77a5c84cd..5e1b2294121e 100644 --- a/packages/next/src/lib/download-wasm-swc.ts +++ b/packages/next/src/lib/download-wasm-swc.ts @@ -3,12 +3,7 @@ import fs from 'fs' import path from 'path' import * as Log from '../build/output/log' import tar from 'next/dist/compiled/tar' -const { fetch } = require('next/dist/compiled/undici') as { - fetch: typeof global.fetch -} -const { WritableStream } = require('node:stream/web') as { - WritableStream: typeof global.WritableStream -} +import fetch from 'next/dist/compiled/node-fetch' import { fileExists } from './file-exists' import { getRegistry } from './helpers/get-registry' @@ -102,24 +97,17 @@ export async function downloadWasmSwc( const registry = getRegistry() await fetch(`${registry}${pkgName}/-/${tarFileName}`).then((res) => { - const { ok, body } = res - if (!ok) { + if (!res.ok) { throw new Error(`request failed with status ${res.status}`) } - if (!body) { - throw new Error('request failed with empty body') - } const cacheWriteStream = fs.createWriteStream(tempFile) - return body.pipeTo( - new WritableStream({ - write(chunk) { - cacheWriteStream.write(chunk) - }, - close() { - cacheWriteStream.close() - }, - }) - ) + + return new Promise((resolve, reject) => { + res.body + .pipe(cacheWriteStream) + .on('error', (err) => reject(err)) + .on('finish', () => resolve()) + }).finally(() => cacheWriteStream.close()) }) await fs.promises.rename(tempFile, path.join(cacheDirectory, tarFileName)) } diff --git a/packages/next/src/server/config-schema.ts b/packages/next/src/server/config-schema.ts index f1a27d295b96..54f03a946c53 100644 --- a/packages/next/src/server/config-schema.ts +++ b/packages/next/src/server/config-schema.ts @@ -420,6 +420,9 @@ const configSchema = { }, type: 'array', }, + enableUndici: { + type: 'boolean', + }, workerThreads: { type: 'boolean', }, diff --git a/packages/next/src/server/config-shared.ts b/packages/next/src/server/config-shared.ts index 8d3d4ae586f7..74ccc23109f5 100644 --- a/packages/next/src/server/config-shared.ts +++ b/packages/next/src/server/config-shared.ts @@ -217,6 +217,7 @@ export interface ExperimentalConfig { * [webpack/webpack#ModuleNotoundError.js#L13-L42](https://github.com/webpack/webpack/blob/2a0536cf510768111a3a6dceeb14cb79b9f59273/lib/ModuleNotFoundError.js#L13-L42) */ fallbackNodePolyfills?: false + enableUndici?: boolean sri?: { algorithm?: SubresourceIntegrityAlgorithm } @@ -702,6 +703,7 @@ export const defaultConfig: NextConfig = { disablePostcssPresetEnv: undefined, amp: undefined, urlImports: undefined, + enableUndici: false, adjustFontFallbacks: false, adjustFontFallbacksWithSizeAdjust: false, turbo: undefined, diff --git a/packages/next/src/server/config.ts b/packages/next/src/server/config.ts index 7f07a1ddda3f..6f56da8fc1b8 100644 --- a/packages/next/src/server/config.ts +++ b/packages/next/src/server/config.ts @@ -19,11 +19,17 @@ import { import { loadWebpackHook } from './config-utils' import { ImageConfig, imageConfigDefault } from '../shared/lib/image-config' import { loadEnvConfig, updateInitialEnv } from '@next/env' +import { gte as semverGte } from 'next/dist/compiled/semver' import { flushAndExit } from '../telemetry/flush-and-exit' import { findRootDir } from '../lib/find-root' export { DomainLocale, NextConfig, normalizeConfig } from './config-shared' +const NODE_16_VERSION = '16.8.0' +const NODE_18_VERSION = '18.0.0' +const isAboveNodejs16 = semverGte(process.version, NODE_16_VERSION) +const isAboveNodejs18 = semverGte(process.version, NODE_18_VERSION) + const experimentalWarning = execOnce( (configFileName: string, features: string[]) => { const s = features.length > 1 ? 's' : '' @@ -43,9 +49,26 @@ const experimentalWarning = execOnce( } ) -export function setHttpClientAndAgentOptions(config: { - httpAgentOptions?: NextConfig['httpAgentOptions'] -}) { +export function setHttpClientAndAgentOptions( + config: { + httpAgentOptions?: NextConfig['httpAgentOptions'] + experimental?: { + enableUndici?: boolean + } + }, + silent = false +) { + if (isAboveNodejs16) { + // Node.js 18 has undici built-in. + if (config.experimental?.enableUndici && !isAboveNodejs18) { + // When appDir is enabled undici is the default because of Response.clone() issues in node-fetch + ;(globalThis as any).__NEXT_USE_UNDICI = config.experimental?.enableUndici + } + } else if (config.experimental?.enableUndici && !silent) { + Log.warn( + `\`enableUndici\` option requires Node.js v${NODE_16_VERSION} or greater. Falling back to \`node-fetch\`` + ) + } if ((globalThis as any).__NEXT_HTTP_AGENT) { // We only need to assign once because we want // to reuse the same agent for all requests. @@ -263,6 +286,10 @@ function assignDefaults( ) } + if (result.experimental?.appDir) { + result.experimental.enableUndici = true + } + if (result.basePath !== '') { if (result.basePath === '/') { throw new Error( @@ -485,7 +512,7 @@ function assignDefaults( result.output = undefined } - setHttpClientAndAgentOptions(result || defaultConfig) + setHttpClientAndAgentOptions(result || defaultConfig, silent) if (result.i18n) { const { i18n } = result @@ -832,6 +859,6 @@ export default async function loadConfig( silent ) as NextConfigComplete completeConfig.configFileName = configFileName - setHttpClientAndAgentOptions(completeConfig) + setHttpClientAndAgentOptions(completeConfig, silent) return completeConfig } diff --git a/packages/next/src/server/dev/next-dev-server.ts b/packages/next/src/server/dev/next-dev-server.ts index 2a6264ea51f0..1a4fa2e414b7 100644 --- a/packages/next/src/server/dev/next-dev-server.ts +++ b/packages/next/src/server/dev/next-dev-server.ts @@ -1650,6 +1650,7 @@ export default class DevServer extends Server { publicRuntimeConfig, serverRuntimeConfig, httpAgentOptions, + experimental: { enableUndici }, } = this.nextConfig const { locales, defaultLocale } = this.nextConfig.i18n || {} const staticPathsWorker = this.getStaticPathsWorker() @@ -1664,6 +1665,7 @@ export default class DevServer extends Server { serverRuntimeConfig, }, httpAgentOptions, + enableUndici, locales, defaultLocale, originalAppPath, diff --git a/packages/next/src/server/dev/static-paths-worker.ts b/packages/next/src/server/dev/static-paths-worker.ts index bdb7c48621e5..3efd7b8e1779 100644 --- a/packages/next/src/server/dev/static-paths-worker.ts +++ b/packages/next/src/server/dev/static-paths-worker.ts @@ -26,6 +26,7 @@ export async function loadStaticPaths({ pathname, config, httpAgentOptions, + enableUndici, locales, defaultLocale, isAppPath, @@ -40,6 +41,7 @@ export async function loadStaticPaths({ pathname: string config: RuntimeConfig httpAgentOptions: NextConfigComplete['httpAgentOptions'] + enableUndici: NextConfigComplete['enableUndici'] locales?: string[] defaultLocale?: string isAppPath?: boolean @@ -58,6 +60,7 @@ export async function loadStaticPaths({ require('../../shared/lib/runtime-config').setConfig(config) setHttpClientAndAgentOptions({ httpAgentOptions, + experimental: { enableUndici }, }) const components = await loadComponents({ diff --git a/packages/next/src/server/node-polyfill-fetch.ts b/packages/next/src/server/node-polyfill-fetch.ts index 1ae59a43e243..8b71a693e852 100644 --- a/packages/next/src/server/node-polyfill-fetch.ts +++ b/packages/next/src/server/node-polyfill-fetch.ts @@ -3,7 +3,9 @@ if (typeof fetch === 'undefined' && typeof globalThis.fetch === 'undefined') { function getFetchImpl() { - return require('next/dist/compiled/undici') + return (global as any).__NEXT_USE_UNDICI + ? require('next/dist/compiled/undici') + : require('next/dist/compiled/node-fetch') } function getRequestImpl() { @@ -20,19 +22,30 @@ if (typeof fetch === 'undefined' && typeof globalThis.fetch === 'undefined') { globalThis.fetch = (...args: any[]) => { const fetchImpl = getFetchImpl() - // Undici does not support the `keepAlive` option, - // instead we have to pass a custom dispatcher - if ( - !(global as any).__NEXT_HTTP_AGENT_OPTIONS?.keepAlive && - !(global as any).__NEXT_UNDICI_AGENT_SET - ) { - ;(global as any).__NEXT_UNDICI_AGENT_SET = true - fetchImpl.setGlobalDispatcher(new fetchImpl.Agent({ pipelining: 0 })) - console.warn( - 'Warning - Configuring `keepAlive: false` is deprecated. Use `{ headers: { connection: "close" } }` instead.' - ) + if ((global as any).__NEXT_USE_UNDICI) { + // Undici does not support the `keepAlive` option, + // instead we have to pass a custom dispatcher + if ( + !(global as any).__NEXT_HTTP_AGENT_OPTIONS?.keepAlive && + !(global as any).__NEXT_UNDICI_AGENT_SET + ) { + ;(global as any).__NEXT_UNDICI_AGENT_SET = true + fetchImpl.setGlobalDispatcher(new fetchImpl.Agent({ pipelining: 0 })) + } + return fetchImpl.fetch(...args) + } + const agent = ({ protocol }: any) => + protocol === 'http:' + ? (global as any).__NEXT_HTTP_AGENT + : (global as any).__NEXT_HTTPS_AGENT + + if (!args[1]) { + args[1] = { agent } + } else if (!args[1].agent) { + args[1].agent = agent } - return fetchImpl.fetch(...args) + + return fetchImpl(...args) } Object.defineProperties(global, { diff --git a/packages/next/src/telemetry/post-payload.ts b/packages/next/src/telemetry/post-payload.ts index e782b9f826df..052307337382 100644 --- a/packages/next/src/telemetry/post-payload.ts +++ b/packages/next/src/telemetry/post-payload.ts @@ -1,13 +1,7 @@ import retry from 'next/dist/compiled/async-retry' -const { fetch } = require('next/dist/compiled/undici') as { - fetch: typeof global.fetch -} +import fetch from 'next/dist/compiled/node-fetch' export function _postPayload(endpoint: string, body: object, signal?: any) { - if (!signal && 'timeout' in AbortSignal) { - // @ts-expect-error Needs @types/node@16.14.0 or newer - signal = AbortSignal.timeout(5000) - } return ( retry( () => @@ -15,6 +9,7 @@ export function _postPayload(endpoint: string, body: object, signal?: any) { method: 'POST', body: JSON.stringify(body), headers: { 'content-type': 'application/json' }, + timeout: 5000, signal, }).then((res) => { if (!res.ok) { diff --git a/packages/next/taskfile-swc.js b/packages/next/taskfile-swc.js index ae6581f756b0..d3104e4b2a36 100644 --- a/packages/next/taskfile-swc.js +++ b/packages/next/taskfile-swc.js @@ -70,8 +70,8 @@ module.exports = function (task) { }, env: { targets: { - // Follows version defined in packages/next/package.json#engine - // Currently a few minors behind due to babel class transpiling + // Should match version defined in packages/next/package.json#engine + // Should also match NODE_16_VERSION in packages/next/src/server/config.ts node: '16.8.0', }, }, diff --git a/test/e2e/undici-fetch/index.test.ts b/test/e2e/undici-fetch/index.test.ts index b673beb37d8d..626e1ca7cac2 100644 --- a/test/e2e/undici-fetch/index.test.ts +++ b/test/e2e/undici-fetch/index.test.ts @@ -1,75 +1,130 @@ import { createNext } from 'e2e-utils' import { NextInstance } from 'test/lib/next-modes/base' import { fetchViaHTTP } from 'next-test-utils' +import semver from 'semver' -describe('undici fetch', () => { - let next: NextInstance +if ( + semver.lt(process.version, '16.8.0') || + semver.gte(process.version, '18.0.0') || + (global as any).isNextDeploy +) { + it('skipping for Node.js versions <16.8.0 and >18.0.0', () => { + expect(true).toBe(true) + }) +} else { + describe('undici fetch', () => { + let next: NextInstance - beforeAll(async () => { - next = await createNext({ - files: { - 'pages/api/globalFetch.js': ` - import { ReadableStream } from 'node:stream/web'; - export default async function globalFetch(req, res) { - try { - const response = await fetch('https://example.vercel.sh') - res.json({ value: response.body instanceof ReadableStream }) - } catch (error) { - console.error(error); - res.send(error); + beforeAll(async () => { + next = await createNext({ + files: { + 'pages/api/globalFetch.js': ` + import { ReadableStream } from 'node:stream/web'; + export default async function globalFetch(req, res) { + try { + const response = await fetch('https://example.vercel.sh') + res.json({ value: response.body instanceof ReadableStream }) + } catch (error) { + console.error(error); + res.send(error); + } + } + `, + 'pages/api/globalHeaders.js': ` + export default async function globalHeaders(req, res) { + res.json({ + value: (new Headers())[Symbol.iterator].name === 'entries' + }) + } + `, + 'pages/api/globalRequest.js': ` + export default async function globalRequest(req, res) { + res.json({ + value: (new Request('https://example.vercel.sh')).headers[Symbol.iterator].name === 'entries' + }) } - } - `, - 'pages/api/globalHeaders.js': ` - export default async function globalHeaders(req, res) { - res.json({ - value: (new Headers())[Symbol.iterator].name === 'entries' - }) - } - `, - 'pages/api/globalRequest.js': ` - export default async function globalRequest(req, res) { - res.json({ - value: (new Request('https://example.vercel.sh')).headers[Symbol.iterator].name === 'entries' - }) - } - `, - 'pages/api/globalResponse.js': ` - export default async function globalResponse(req, res) { - res.json({ - value: (new Response()).headers[Symbol.iterator].name === 'entries' - }) - } - `, - }, - dependencies: {}, + `, + 'pages/api/globalResponse.js': ` + export default async function globalResponse(req, res) { + res.json({ + value: (new Response()).headers[Symbol.iterator].name === 'entries' + }) + } + `, + }, + dependencies: {}, + nextConfig: { + experimental: { + enableUndici: true, + }, + }, + }) }) - }) - afterAll(() => next.destroy()) + afterAll(() => next.destroy()) - describe('undici', () => { - it('global fetch should return true when undici is used', async () => { - const result = await fetchViaHTTP(next.url, '/api/globalFetch') - const data = await result.json() - expect(data.value).toBe(true) - }) + describe('undici', () => { + it('global fetch should return true when undici is used', async () => { + const result = await fetchViaHTTP(next.url, '/api/globalFetch') + const data = await result.json() + expect(data.value).toBe(true) + }) - it('global Headers should return true when undici is used', async () => { - const result = await fetchViaHTTP(next.url, '/api/globalHeaders') - const data = await result.json() - expect(data.value).toBe(true) - }) + it('global Headers should return true when undici is used', async () => { + const result = await fetchViaHTTP(next.url, '/api/globalHeaders') + const data = await result.json() + expect(data.value).toBe(true) + }) + + it('global Request should return true when undici is used', async () => { + const result = await fetchViaHTTP(next.url, '/api/globalRequest') + const data = await result.json() + expect(data.value).toBe(true) + }) - it('global Request should return true when undici is used', async () => { - const result = await fetchViaHTTP(next.url, '/api/globalRequest') - const data = await result.json() - expect(data.value).toBe(true) + it('global Response should return true when undici is used', async () => { + const result = await fetchViaHTTP(next.url, '/api/globalResponse') + const data = await result.json() + expect(data.value).toBe(true) + }) }) - it('global Response should return true when undici is used', async () => { - const result = await fetchViaHTTP(next.url, '/api/globalResponse') - const data = await result.json() - expect(data.value).toBe(true) + describe('node-fetch', () => { + beforeAll(async () => { + await next.stop() + await next.patchFile( + 'next.config.js', + `module.exports = ${JSON.stringify({ + experimental: { + enableUndici: false, + }, + })}` + ) + await next.start() + }) + + it('global fetch should return false when node-fetch is used', async () => { + const result = await fetchViaHTTP(next.url, '/api/globalFetch') + const data = await result.json() + expect(data.value).toBe(false) + }) + + it('global Headers should return false when node-fetch is used', async () => { + const result = await fetchViaHTTP(next.url, '/api/globalHeaders') + const data = await result.json() + expect(data.value).toBe(false) + }) + + it('global Request should return false when node-fetch is used', async () => { + const result = await fetchViaHTTP(next.url, '/api/globalRequest') + const data = await result.json() + expect(data.value).toBe(false) + }) + + it('global Response should return false when node-fetch is used', async () => { + const result = await fetchViaHTTP(next.url, '/api/globalResponse') + const data = await result.json() + expect(data.value).toBe(false) + }) }) }) -}) +} diff --git a/test/integration/config-experimental-warning/test/index.test.js b/test/integration/config-experimental-warning/test/index.test.js index 70a36e16bd20..13842d0083fb 100644 --- a/test/integration/config-experimental-warning/test/index.test.js +++ b/test/integration/config-experimental-warning/test/index.test.js @@ -84,14 +84,14 @@ describe('Config Experimental Warning', () => { configFile.write(` module.exports = { experimental: { - urlImports: true, + enableUndici: true, workerThreads: true, } } `) const { stderr } = await nextBuild(appDir, [], { stderr: true }) expect(stderr).toMatch( - 'You have enabled experimental features (urlImports, workerThreads) in next.config.js.' + 'You have enabled experimental features (enableUndici, workerThreads) in next.config.js.' ) }) diff --git a/test/integration/undici-keep-alive/next.config.js b/test/integration/undici-keep-alive/next.config.js new file mode 100644 index 000000000000..da96200516af --- /dev/null +++ b/test/integration/undici-keep-alive/next.config.js @@ -0,0 +1,5 @@ +module.exports = { + experimental: { + enableUndici: true, + }, +} diff --git a/test/integration/undici-keep-alive/pages/api/json.js b/test/integration/undici-keep-alive/pages/api/json.js new file mode 100644 index 000000000000..ea00e00131b0 --- /dev/null +++ b/test/integration/undici-keep-alive/pages/api/json.js @@ -0,0 +1,5 @@ +export default async function handler(_req, res) { + const fetchRes = await fetch('http://localhost:44001') + const props = await fetchRes.json() + res.json(props) +} diff --git a/test/integration/undici-keep-alive/pages/blog/[slug].js b/test/integration/undici-keep-alive/pages/blog/[slug].js new file mode 100644 index 000000000000..307b8469e02a --- /dev/null +++ b/test/integration/undici-keep-alive/pages/blog/[slug].js @@ -0,0 +1,23 @@ +export default function Blog(props) { + return
{JSON.stringify(props)}
+} + +export async function getStaticProps({ params: { slug } }) { + return { props: { slug } } +} + +export async function getStaticPaths() { + const res = await fetch('http://localhost:44001') + const obj = await res.json() + if (obj.connection === 'keep-alive') { + return { + paths: [{ params: { slug: 'first' } }], + fallback: false, + } + } + + return { + paths: [], + fallback: false, + } +} diff --git a/test/integration/undici-keep-alive/pages/ssg.js b/test/integration/undici-keep-alive/pages/ssg.js new file mode 100644 index 000000000000..65dc1c60ae6a --- /dev/null +++ b/test/integration/undici-keep-alive/pages/ssg.js @@ -0,0 +1,9 @@ +export default function SSG(props) { + return
{JSON.stringify(props)}
+} + +export async function getStaticProps() { + const res = await fetch('http://localhost:44001') + const props = await res.json() + return { props } +} diff --git a/test/integration/undici-keep-alive/pages/ssr.js b/test/integration/undici-keep-alive/pages/ssr.js new file mode 100644 index 000000000000..10a053c2ed66 --- /dev/null +++ b/test/integration/undici-keep-alive/pages/ssr.js @@ -0,0 +1,9 @@ +export default function SSR(props) { + return
{JSON.stringify(props)}
+} + +export async function getServerSideProps() { + const res = await fetch('http://localhost:44001') + const props = await res.json() + return { props } +} diff --git a/test/integration/undici-keep-alive/test/index.test.js b/test/integration/undici-keep-alive/test/index.test.js new file mode 100644 index 000000000000..212768a458c8 --- /dev/null +++ b/test/integration/undici-keep-alive/test/index.test.js @@ -0,0 +1,99 @@ +/* eslint-env jest */ + +import { join } from 'path' +import { createServer } from 'http' +import { + fetchViaHTTP, + nextBuild, + findPort, + nextStart, + launchApp, + killApp, +} from 'next-test-utils' +import webdriver from 'next-webdriver' + +const appDir = join(__dirname, '../') + +let appPort +let app +let mockServer + +describe('undici-keep-alive', () => { + describe('dev', () => { + beforeAll(async () => { + mockServer = createServer((req, res) => { + // we can test request headers by sending them + // back with the response + const { connection } = req.headers + res.end(JSON.stringify({ connection })) + }) + mockServer.listen(44001) + appPort = await findPort() + app = await launchApp(appDir, appPort) + }) + afterAll(async () => { + await killApp(app) + mockServer.close() + }) + + runTests() + }) + + describe('production', () => { + beforeAll(async () => { + mockServer = createServer((req, res) => { + // we can test request headers by sending them + // back with the response + const { connection } = req.headers + res.end(JSON.stringify({ connection })) + }) + mockServer.listen(44001) + const { stdout, stderr } = await nextBuild(appDir, [], { + stdout: true, + stderr: true, + }) + if (stdout) console.log(stdout) + if (stderr) console.error(stderr) + appPort = await findPort() + app = await nextStart(appDir, appPort) + }) + afterAll(async () => { + await killApp(app) + mockServer.close() + }) + + runTests() + }) + + function runTests() { + it('should send keep-alive for json API', async () => { + const res = await fetchViaHTTP(appPort, '/api/json') + const obj = await res.json() + expect(obj).toEqual({ connection: 'keep-alive' }) + }) + + it('should send keep-alive for getStaticProps', async () => { + const browser = await webdriver(appPort, '/ssg') + const props = await browser.elementById('props').text() + const obj = JSON.parse(props) + expect(obj).toEqual({ connection: 'keep-alive' }) + await browser.close() + }) + + it('should send keep-alive for getStaticPaths', async () => { + const browser = await webdriver(appPort, '/blog/first') + const props = await browser.elementById('props').text() + const obj = JSON.parse(props) + expect(obj).toEqual({ slug: 'first' }) + await browser.close() + }) + + it('should send keep-alive for getServerSideProps', async () => { + const browser = await webdriver(appPort, '/ssr') + const props = await browser.elementById('props').text() + const obj = JSON.parse(props) + expect(obj).toEqual({ connection: 'keep-alive' }) + await browser.close() + }) + } +})