Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(experimental): option to polyfill fetch using undici in Node.js <18 #40318

Merged
merged 61 commits into from Sep 27, 2022
Merged
Show file tree
Hide file tree
Changes from 46 commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
66408d8
polyfill undici
Ethan-Arrowood Sep 7, 2022
28a09b8
move polyfill to inside !global.fetch
Ethan-Arrowood Sep 7, 2022
0daf850
remove nodejs type update
Ethan-Arrowood Sep 7, 2022
1c5c027
update types and compile undici
Ethan-Arrowood Sep 7, 2022
f4169d7
try using global.__NEXT_USE_UNDICI and add tests
Ethan-Arrowood Sep 9, 2022
0b2475a
Merge branch 'canary' of github.com:vercel/next.js into feature/polyf…
Ethan-Arrowood Sep 9, 2022
07be524
remove auto-generated files
Ethan-Arrowood Sep 9, 2022
32d3677
Merge branch 'canary' of github.com:vercel/next.js into feature/polyf…
Ethan-Arrowood Sep 14, 2022
4cbc65f
Add ?. to useUndici
Ethan-Arrowood Sep 14, 2022
eb44e6b
Fix global config resolution
Ethan-Arrowood Sep 14, 2022
7fd115a
Fix global properties and add more tests
Ethan-Arrowood Sep 14, 2022
2a962a4
Update function name
Ethan-Arrowood Sep 15, 2022
6593733
remove unused code
Ethan-Arrowood Sep 15, 2022
a11d117
require fetch implementation conditionally
balazsorban44 Sep 15, 2022
ebc33d7
update lock file
balazsorban44 Sep 15, 2022
f623f65
add `keepAlive` tests for `undici`
balazsorban44 Sep 15, 2022
502db7f
handle `keepAlive` with `undici`
balazsorban44 Sep 15, 2022
88c65cd
Merge branch 'canary' into feature/polyfill-undici-fetch
balazsorban44 Sep 15, 2022
1651916
update compiled
balazsorban44 Sep 15, 2022
8393aaf
Merge branch 'feature/polyfill-undici-fetch' of github.com:Ethan-Arro…
balazsorban44 Sep 15, 2022
76d7b6c
Merge branch 'canary' into feature/polyfill-undici-fetch
Ethan-Arrowood Sep 15, 2022
367862f
add back `mini-css-extract-plugin` compiled
balazsorban44 Sep 16, 2022
3866700
revert compiled
balazsorban44 Sep 16, 2022
58ecb4c
Merge branch 'canary' into feature/polyfill-undici-fetch
balazsorban44 Sep 16, 2022
454d917
revert webpack compiled changes
balazsorban44 Sep 16, 2022
1b26d54
Merge branch 'feature/polyfill-undici-fetch' of github.com:Ethan-Arro…
balazsorban44 Sep 16, 2022
0de1fb4
revert
balazsorban44 Sep 16, 2022
ec21630
revert
balazsorban44 Sep 16, 2022
e8550fa
revert?
balazsorban44 Sep 16, 2022
7e6b1f9
more revert
balazsorban44 Sep 16, 2022
4919546
one more
balazsorban44 Sep 16, 2022
ef8b44f
update taskfile
balazsorban44 Sep 16, 2022
789b43b
update compiled
balazsorban44 Sep 16, 2022
245ff7b
Add version guard for <16.8.0
Ethan-Arrowood Sep 16, 2022
60d99d7
fix lock file
Ethan-Arrowood Sep 16, 2022
0de3c26
Update packages/next/server/config.ts
Ethan-Arrowood Sep 16, 2022
989f9c8
fix semver usage
Ethan-Arrowood Sep 16, 2022
ed03b2b
useUndici -> enableUndici
Ethan-Arrowood Sep 16, 2022
421891b
Merge branch 'canary' into feature/polyfill-undici-fetch
Ethan-Arrowood Sep 16, 2022
acf39cb
Merge branch 'canary' of github.com:vercel/next.js into feature/polyf…
Ethan-Arrowood Sep 19, 2022
203c3e7
Merge branch 'feature/polyfill-undici-fetch' of github.com:Ethan-Arro…
Ethan-Arrowood Sep 19, 2022
bbc2ac5
fix lock file
Ethan-Arrowood Sep 19, 2022
cbafb7f
Merge branch 'canary' into feature/polyfill-undici-fetch
Ethan-Arrowood Sep 19, 2022
d2087ce
Merge branch 'canary' into feature/polyfill-undici-fetch
ijjk Sep 21, 2022
f770bd5
update lock
ijjk Sep 21, 2022
31beccc
update compiled
ijjk Sep 21, 2022
7722ea5
Update packages/next/server/node-polyfill-fetch.js
Ethan-Arrowood Sep 21, 2022
424531a
add enableUndici to worker exportPage
Ethan-Arrowood Sep 21, 2022
7da8b81
Merge branch 'canary' of github.com:vercel/next.js into feature/polyf…
Ethan-Arrowood Sep 26, 2022
72c90fe
add missing argument in exportPage and revert compiled file changes
Ethan-Arrowood Sep 26, 2022
e5f3c60
remove compiled sass-loader file
Ethan-Arrowood Sep 26, 2022
fbc3e4d
Merge branch 'canary' into feature/polyfill-undici-fetch
Ethan-Arrowood Sep 26, 2022
fcfa989
update lock file
balazsorban44 Sep 26, 2022
08fefca
Merge branch 'canary' into feature/polyfill-undici-fetch
balazsorban44 Sep 26, 2022
b101ec6
update compiled
balazsorban44 Sep 26, 2022
82a0961
fix test
balazsorban44 Sep 26, 2022
ef18f28
tweak `enableUndici` warnings
balazsorban44 Sep 27, 2022
05ca18f
pass `enableUndici` to worker
balazsorban44 Sep 27, 2022
519c717
tweak test
balazsorban44 Sep 27, 2022
6002dba
Merge branch 'canary' into feature/polyfill-undici-fetch
ijjk Sep 27, 2022
8ed03ee
Merge branch 'canary' into feature/polyfill-undici-fetch
ijjk Sep 27, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions jest.config.js
Expand Up @@ -3,13 +3,17 @@ const nextJest = require('next/jest')
const createJestConfig = nextJest()

// Any custom config you want to pass to Jest
/** @type {import('jest').Config} */
const customJestConfig = {
testMatch: ['**/*.test.js', '**/*.test.ts', '**/*.test.tsx'],
setupFilesAfterEnv: ['<rootDir>/jest-setup-after-env.ts'],
verbose: true,
rootDir: 'test',
modulePaths: ['<rootDir>/lib'],
transformIgnorePatterns: ['/next[/\\\\]dist/', '/\\.next/'],
globals: {
AbortSignal: global.AbortSignal,
},
}

// createJestConfig is exported in this way to ensure that next/jest can load the Next.js config which is async
Expand Down
2 changes: 2 additions & 0 deletions packages/next/build/index.ts
Expand Up @@ -1204,6 +1204,7 @@ export default async function build(
configFileName,
runtimeEnvConfig,
httpAgentOptions: config.httpAgentOptions,
enableUndici: config.experimental.enableUndici,
locales: config.i18n?.locales,
defaultLocale: config.i18n?.defaultLocale,
pageRuntime: config.experimental.runtime,
Expand Down Expand Up @@ -1351,6 +1352,7 @@ export default async function build(
configFileName,
runtimeEnvConfig,
httpAgentOptions: config.httpAgentOptions,
enableUndici: config.experimental.enableUndici,
locales: config.i18n?.locales,
defaultLocale: config.i18n?.defaultLocale,
parentId: isPageStaticSpan.id,
Expand Down
9 changes: 7 additions & 2 deletions packages/next/build/utils.ts
Expand Up @@ -39,7 +39,7 @@ import {
LoadComponentsReturnType,
} from '../server/load-components'
import { trace } from '../trace'
import { setHttpAgentOptions } from '../server/config'
import { setHttpClientAndAgentOptions } from '../server/config'
import { recursiveDelete } from '../lib/recursive-delete'
import { Sema } from 'next/dist/compiled/async-sema'
import { MiddlewareManifest } from './webpack/plugins/middleware-plugin'
Expand Down Expand Up @@ -1167,6 +1167,7 @@ export async function isPageStatic({
configFileName,
runtimeEnvConfig,
httpAgentOptions,
enableUndici,
locales,
defaultLocale,
parentId,
Expand All @@ -1182,6 +1183,7 @@ export async function isPageStatic({
configFileName: string
runtimeEnvConfig: any
httpAgentOptions: NextConfigComplete['httpAgentOptions']
enableUndici?: NextConfigComplete['experimental']['enableUndici']
locales?: string[]
defaultLocale?: string
parentId?: any
Expand All @@ -1208,7 +1210,10 @@ export async function isPageStatic({
return isPageStaticSpan
.traceAsyncFn(async () => {
require('../shared/lib/runtime-config').setConfig(runtimeEnvConfig)
setHttpAgentOptions(httpAgentOptions)
setHttpClientAndAgentOptions({
httpAgentOptions,
experimental: { enableUndici },
})

let componentsResult: LoadComponentsReturnType
let prerenderRoutes: Array<string> | undefined
Expand Down
21 changes: 21 additions & 0 deletions packages/next/compiled/undici/LICENSE
@@ -0,0 +1,21 @@
MIT License

Copyright (c) Matteo Collina and Undici contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
2 changes: 2 additions & 0 deletions packages/next/compiled/undici/index.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/next/compiled/undici/package.json
@@ -0,0 +1 @@
{"name":"undici","main":"index.js","author":"Matteo Collina <hello@matteocollina.com>","license":"MIT"}
4 changes: 2 additions & 2 deletions packages/next/export/worker.ts
Expand Up @@ -23,7 +23,7 @@ import { requireFontManifest } from '../server/require'
import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path'
import { trace } from '../trace'
import { isInAmpMode } from '../shared/lib/amp-mode'
import { setHttpAgentOptions } from '../server/config'
import { setHttpClientAndAgentOptions } from '../server/config'
import RenderResult from '../server/render-result'
import isError from '../lib/is-error'
import { addRequestMeta } from '../server/request-meta'
Expand Down Expand Up @@ -118,7 +118,7 @@ export default async function exportPage({
httpAgentOptions,
serverComponents,
}: ExportPageInput): Promise<ExportPageResults> {
setHttpAgentOptions(httpAgentOptions)
setHttpClientAndAgentOptions({ httpAgentOptions })
Ethan-Arrowood marked this conversation as resolved.
Show resolved Hide resolved
const exportPageSpan = trace('export-page-worker', parentSpanId)

return exportPageSpan.traceAsyncFn(async () => {
Expand Down
1 change: 1 addition & 0 deletions packages/next/package.json
Expand Up @@ -263,6 +263,7 @@
"timers-browserify": "2.0.12",
"tty-browserify": "0.0.1",
"ua-parser-js": "0.7.28",
"undici": "5.10.0",
"unistore": "3.4.1",
"util": "0.12.4",
"uuid": "8.3.2",
Expand Down
3 changes: 3 additions & 0 deletions packages/next/server/config-schema.ts
Expand Up @@ -387,6 +387,9 @@ const configSchema = {
},
type: 'array',
},
enableUndici: {
type: 'boolean',
},
workerThreads: {
type: 'boolean',
},
Expand Down
2 changes: 2 additions & 0 deletions packages/next/server/config-shared.ts
Expand Up @@ -147,6 +147,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
}
Expand Down Expand Up @@ -582,6 +583,7 @@ export const defaultConfig: NextConfig = {
amp: undefined,
urlImports: undefined,
modularizeImports: undefined,
enableUndici: false,
adjustFontFallbacks: false,
},
}
Expand Down
1 change: 1 addition & 0 deletions packages/next/server/config-utils.ts
Expand Up @@ -139,6 +139,7 @@ export function loadWebpackHook() {
'next/dist/compiled/@babel/runtime/package.json',
],
['node-fetch', 'next/dist/compiled/node-fetch'],
['undici', 'next/dist/compiled/undici'],
].map(
// Use dynamic require.resolve to avoid statically analyzable since they're only for build time
([request, replacement]) => [request, require.resolve(replacement)]
Expand Down
24 changes: 15 additions & 9 deletions packages/next/server/config.ts
Expand Up @@ -13,6 +13,7 @@ import {
ExperimentalConfig,
NextConfigComplete,
validateConfig,
NextConfig,
} from './config-shared'
import { loadWebpackHook } from './config-utils'
import {
Expand All @@ -22,6 +23,7 @@ import {
} from '../shared/lib/image-config'
import { loadEnvConfig } from '@next/env'
import { hasNextSupport } from '../telemetry/ci-info'
import { gte as semverGte } from 'next/dist/compiled/semver'

export { DomainLocale, NextConfig, normalizeConfig } from './config-shared'

Expand All @@ -45,9 +47,14 @@ const experimentalWarning = execOnce(
}
)

export function setHttpAgentOptions(
options: NextConfigComplete['httpAgentOptions']
) {
export function setHttpClientAndAgentOptions(options: NextConfig) {
if (semverGte(process.version, '16.8.0')) {
;(global as any).__NEXT_USE_UNDICI = options.experimental?.enableUndici
} else {
balazsorban44 marked this conversation as resolved.
Show resolved Hide resolved
Log.warn(
'Cannot enable undici fetch. Must be on Node.js v16.8.0 or greater.'
)
}
if ((global as any).__NEXT_HTTP_AGENT) {
// We only need to assign once because we want
// to resuse the same agent for all requests.
Expand All @@ -58,8 +65,9 @@ export function setHttpAgentOptions(
throw new Error('Expected config.httpAgentOptions to be an object')
}

;(global as any).__NEXT_HTTP_AGENT = new HttpAgent(options)
;(global as any).__NEXT_HTTPS_AGENT = new HttpsAgent(options)
;(global as any).__NEXT_HTTP_AGENT_OPTIONS = options.httpAgentOptions
;(global as any).__NEXT_HTTP_AGENT = new HttpAgent(options.httpAgentOptions)
;(global as any).__NEXT_HTTPS_AGENT = new HttpsAgent(options.httpAgentOptions)
}

function assignDefaults(userConfig: { [key: string]: any }) {
Expand Down Expand Up @@ -545,9 +553,7 @@ function assignDefaults(userConfig: { [key: string]: any }) {

// TODO: Change defaultConfig type to NextConfigComplete
// so we don't need "!" here.
setHttpAgentOptions(
result.httpAgentOptions || defaultConfig.httpAgentOptions!
)
setHttpClientAndAgentOptions(result || defaultConfig)

if (result.i18n) {
const { i18n } = result
Expand Down Expand Up @@ -855,6 +861,6 @@ export default async function loadConfig(
// reactRoot can be updated correctly even with no next.config.js
const completeConfig = assignDefaults(defaultConfig) as NextConfigComplete
completeConfig.configFileName = configFileName
setHttpAgentOptions(completeConfig.httpAgentOptions)
setHttpClientAndAgentOptions(completeConfig)
return completeConfig
}
4 changes: 2 additions & 2 deletions packages/next/server/dev/static-paths-worker.ts
Expand Up @@ -7,7 +7,7 @@ import {
collectGenerateParams,
} from '../../build/utils'
import { loadComponents } from '../load-components'
import { setHttpAgentOptions } from '../config'
import { setHttpClientAndAgentOptions } from '../config'

type RuntimeConfig = any

Expand Down Expand Up @@ -49,7 +49,7 @@ export async function loadStaticPaths({

// update work memory runtime-config
require('../../shared/lib/runtime-config').setConfig(config)
setHttpAgentOptions(httpAgentOptions)
setHttpClientAndAgentOptions({ httpAgentOptions })
balazsorban44 marked this conversation as resolved.
Show resolved Hide resolved

const components = await loadComponents({
distDir,
Expand Down
69 changes: 51 additions & 18 deletions packages/next/server/node-polyfill-fetch.js
@@ -1,23 +1,56 @@
import fetch, {
Headers,
Request,
Response,
} from 'next/dist/compiled/node-fetch'

// Polyfill fetch() in the Node.js environment

if (!global.fetch) {
const agent = ({ protocol }) =>
protocol === 'http:' ? global.__NEXT_HTTP_AGENT : global.__NEXT_HTTPS_AGENT
const fetchWithAgent = (url, opts, ...rest) => {
if (!opts) {
opts = { agent }
} else if (!opts.agent) {
opts.agent = agent
function getFetchImpl() {
return global.__NEXT_USE_UNDICI
? require('next/dist/compiled/undici')
: require('next/dist/compiled/node-fetch')
}
// Due to limitation of global configuartion, we have to do this resolution at runtime
Ethan-Arrowood marked this conversation as resolved.
Show resolved Hide resolved
global.fetch = (...args) => {
const fetchImpl = getFetchImpl()

if (global.__NEXT_USE_UNDICI) {
// Undici does not support the `keepAlive` option,
// instead we have to pass a custom dispatcher
if (
!global.__NEXT_HTTP_AGENT_OPTIONS?.keepAlive &&
!global.__NEXT_UNDICI_AGENT_SET
) {
global.__NEXT_UNDICI_AGENT_SET = true
fetchImpl.setGlobalDispatcher(new fetchImpl.Agent({ pipelining: 0 }))
}
return fetchImpl.fetch(...args)
}
return fetch(url, opts, ...rest)
const agent = ({ protocol }) =>
Copy link
Member

Choose a reason for hiding this comment

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

This could be a problem because the agent is created for every fetch(). It should be reused, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, the Agent is created and assigned to global.__NEXT_HTTP_AGENT or global.__NEXT_HTTPS_AGENT. This function just returns the correct agent every time fetch is called.

But since we can't do that for the Undici agent we have to have all that !global.__NEXT_UNDICI_AGENT_SET logic.

protocol === 'http:'
? global.__NEXT_HTTP_AGENT
: global.__NEXT_HTTPS_AGENT

if (!args[1]) {
args[1] = { agent }
} else if (!args[1].agent) {
args[1].agent = agent
}

return fetchImpl(...args)
}
global.fetch = fetchWithAgent
global.Headers = Headers
global.Request = Request
global.Response = Response

Object.defineProperties(global, {
Headers: {
get() {
return getFetchImpl().Headers
},
},
Request: {
get() {
return getFetchImpl().Request
},
},
Response: {
get() {
return getFetchImpl().Response
},
},
})
}
11 changes: 11 additions & 0 deletions packages/next/taskfile.js
Expand Up @@ -86,6 +86,8 @@ const externals = {
'next/dist/build/webpack/plugins/terser-webpack-plugin',

// TODO: Add @swc/helpers to externals once @vercel/ncc switch to swc-loader

undici: 'undici',
balazsorban44 marked this conversation as resolved.
Show resolved Hide resolved
}
// eslint-disable-next-line camelcase
externals['node-html-parser'] = 'next/dist/compiled/node-html-parser'
Expand Down Expand Up @@ -167,6 +169,14 @@ export async function ncc_node_fetch(task, opts) {
.target('compiled/node-fetch')
}

externals['undici'] = 'next/dist/compiled/undici'
export async function ncc_undici(task, opts) {
await task
.source(opts.src || relative(__dirname, require.resolve('undici')))
.ncc({ packageName: 'undici', externals })
.target('compiled/undici')
}

// eslint-disable-next-line camelcase
export async function compile_config_schema(task, opts) {
const { configSchema } = require('./dist/server/config-schema')
Expand Down Expand Up @@ -1818,6 +1828,7 @@ export async function ncc(task, opts) {
'ncc_get_orientation',
'ncc_hapi_accept',
'ncc_node_fetch',
'ncc_undici',
'ncc_acorn',
'ncc_amphtml_validator',
'ncc_arg',
Expand Down
2 changes: 2 additions & 0 deletions packages/next/types/misc.d.ts
Expand Up @@ -28,6 +28,8 @@ declare module 'next/dist/compiled/node-fetch' {
export * from 'node-fetch'
}

declare module 'next/dist/compiled/undici' {}

declare module 'next/dist/compiled/jest-worker' {
export * from 'jest-worker'
}
Expand Down
10 changes: 10 additions & 0 deletions pnpm-lock.yaml

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