Better IPv6 support for next-server (#53131)

### What?
This PR makes it easier to use Next.js with IPv6 hostnames such as `::1` and `::`.

### How?
It does so by removing rewrites from `localhost` to `127.0.0.1` introduced in #52492. It also fixes the issue where Next.js tries to fetch something like `http://::1:3000` when `--hostname` is `::1` as it is not a valid URL (browsers' `URL` class throws an error when constructed with such hosts). It also fixes `NextURL` so that it doesn't accept `http://::1:3000` but refuse `http://[::1]:3000`. It also changes `next/src/server/lib/setup-server-worker.ts` so that it uses the server's `address` method to retrieve the host instead of our provided `opts.hostname`, ensuring that no matter what `opts.hostname` is we will always get the correct one.

### Note
I've verified that `next dev`, `next start` and `node .next/standalone/server.js` work with IPv6 hostnames (such as `::` and `::1`), IPv4 hostnames (such as `127.0.0.1`, `0.0.0.0`) and `localhost` - and with any of these hostnames fetching to `localhost` also works. Server Actions and middleware have no problems as well.

This also removes `.next/standalone/server.js`'s logging as we now use `start-server`'s logging to avoid duplicates. `start-server`'s logging has also been updated to report the actual hostname.
![image](https://github.com/vercel/next.js/assets/75556609/cefa5f23-ff09-4cef-a055-13eea7c11d89)
![image](https://github.com/vercel/next.js/assets/75556609/619e82ce-45d9-47b7-8644-f4ad083429db)
The above pictures also demonstrate using Server Actions with Next.js after this PR.
![image](https://github.com/vercel/next.js/assets/75556609/3d4166e9-f950-4390-bde9-af2547658148)

Fixes #53171
Fixes #49578
Closes NEXT-1510

Co-authored-by: Tim Neutkens <6324199+timneutkens@users.noreply.github.com>
Co-authored-by: Zack Tanner <1939140+ztanner@users.noreply.github.com>
This commit is contained in:
Ngô Đức Anh 2023-08-14 14:23:24 +07:00 committed by GitHub
parent df6ec96ab2
commit a4b430e6f1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 168 additions and 127 deletions

View file

@ -6,11 +6,12 @@ import {
import { NodeNextRequest } from 'next/dist/server/base-http/node'
import { BaseNextRequest } from 'next/dist/server/base-http'
import { getCloneableBody } from 'next/dist/server/body-streams'
import { formatHostname } from 'next/dist/server/lib/format-hostname'
export function attachRequestMeta(
req: BaseNextRequest,
parsedUrl: NextUrlWithParsedQuery,
host: string
hostname: string
) {
const protocol = (
(req as NodeNextRequest).originalRequest?.socket as TLSSocket
@ -18,7 +19,7 @@ export function attachRequestMeta(
? 'https'
: 'http'
const initUrl = `${protocol}://${host}${req.url}`
const initUrl = `${protocol}://${formatHostname(hostname)}${req.url}`
addRequestMeta(req, '__NEXT_INIT_URL', initUrl)
addRequestMeta(req, '__NEXT_INIT_QUERY', { ...parsedUrl.query })

View file

@ -1972,17 +1972,11 @@ startServer({
dir,
isDev: false,
config: nextConfig,
hostname: hostname === 'localhost' ? '0.0.0.0' : hostname,
hostname,
port: currentPort,
allowRetry: false,
keepAliveTimeout,
useWorkers: !!nextConfig.experimental?.appDir,
}).then(() => {
console.log(
'Listening on port',
currentPort,
'url: http://' + hostname + ':' + currentPort
)
}).catch((err) => {
console.error(err);
process.exit(1);

View file

@ -466,7 +466,7 @@ async function revalidate(
const ipcKey = process.env.__NEXT_PRIVATE_ROUTER_IPC_KEY
const res = await invokeRequest(
`http://${
context.hostname
context.hostname || 'localhost'
}:${ipcPort}?key=${ipcKey}&method=revalidate&args=${encodeURIComponent(
JSON.stringify([{ urlPath, revalidateHeaders, opts }])
)}`,

View file

@ -121,24 +121,6 @@ function getForwardedHeaders(
return new Headers(mergedHeaders)
}
function fetchIPv4v6(
url: URL,
init: RequestInit,
v6 = false
): Promise<Response> {
const hostname = url.hostname
if (!v6 && hostname === 'localhost') {
url.hostname = '127.0.0.1'
}
return fetch(url, init).catch((err) => {
if (err.code === 'ECONNREFUSED' && !v6) {
return fetchIPv4v6(url, init, true)
}
throw err
})
}
async function addRevalidationHeader(
res: ServerResponse,
{
@ -212,7 +194,7 @@ async function createRedirectRenderResult(
// }
try {
const headResponse = await fetchIPv4v6(fetchUrl, {
const headResponse = await fetch(fetchUrl, {
method: 'HEAD',
headers: forwardedHeaders,
next: {
@ -224,7 +206,7 @@ async function createRedirectRenderResult(
if (
headResponse.headers.get('content-type') === RSC_CONTENT_TYPE_HEADER
) {
const response = await fetchIPv4v6(fetchUrl, {
const response = await fetch(fetchUrl, {
method: 'GET',
headers: forwardedHeaders,
next: {

View file

@ -36,6 +36,7 @@ import type {
} from './future/route-modules/app-route/module'
import { format as formatUrl, parse as parseUrl } from 'url'
import { formatHostname } from './lib/format-hostname'
import { getRedirectStatus } from '../lib/redirect-status'
import { isEdgeRuntime } from '../lib/is-edge-runtime'
import {
@ -268,6 +269,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
protected clientReferenceManifest?: ClientReferenceManifest
protected nextFontManifest?: NextFontManifest
public readonly hostname?: string
public readonly fetchHostname?: string
public readonly port?: number
protected abstract getPublicDir(): string
@ -367,6 +369,10 @@ export default abstract class Server<ServerOptions extends Options = Options> {
// values from causing issues as this can be user provided
this.nextConfig = conf as NextConfigComplete
this.hostname = hostname
if (this.hostname) {
// we format the hostname so that it can be fetched
this.fetchHostname = formatHostname(this.hostname)
}
this.port = port
this.distDir =
process.env.NEXT_RUNTIME === 'edge'

View file

@ -476,7 +476,7 @@ export default class DevServer extends Server {
) {
if (this.isRenderWorker) {
await invokeIpcMethod({
hostname: this.hostname,
fetchHostname: this.fetchHostname,
method: 'logErrorWithOriginalStack',
args: [errorToJSON(err as Error), type],
ipcPort: process.env.__NEXT_PRIVATE_ROUTER_IPC_PORT,
@ -732,7 +732,7 @@ export default class DevServer extends Server {
}) {
if (this.isRenderWorker) {
await invokeIpcMethod({
hostname: this.hostname,
fetchHostname: this.fetchHostname,
method: 'ensurePage',
args: [opts],
ipcPort: process.env.__NEXT_PRIVATE_ROUTER_IPC_PORT,
@ -797,7 +797,7 @@ export default class DevServer extends Server {
protected async getFallbackErrorComponents(): Promise<LoadComponentsReturnType | null> {
if (this.isRenderWorker) {
await invokeIpcMethod({
hostname: this.hostname,
fetchHostname: this.fetchHostname,
method: 'getFallbackErrorComponents',
args: [],
ipcPort: process.env.__NEXT_PRIVATE_ROUTER_IPC_PORT,
@ -813,7 +813,7 @@ export default class DevServer extends Server {
async getCompilationError(page: string): Promise<any> {
if (this.isRenderWorker) {
const err = await invokeIpcMethod({
hostname: this.hostname,
fetchHostname: this.fetchHostname,
method: 'getCompilationError',
args: [page],
ipcPort: process.env.__NEXT_PRIVATE_ROUTER_IPC_PORT,

View file

@ -0,0 +1,12 @@
import { isIPv6 } from './is-ipv6'
/**
* Formats a hostname so that it is a valid host that can be fetched by wrapping
* IPv6 hosts with brackets.
* @param hostname
* @returns
*/
export function formatHostname(hostname: string): string {
return isIPv6(hostname) ? `[${hostname}]` : hostname
}

View file

@ -0,0 +1,42 @@
// Regex from `node/lib/internal/net.js`: https://github.com/nodejs/node/blob/9fc57006c27564ed7f75eee090eca86786508f51/lib/internal/net.js#L19-L29
// License included below:
// Copyright Joyent, Inc. and other Node 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.
const v4Seg = '(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])'
const v4Str = `(${v4Seg}[.]){3}${v4Seg}`
const v6Seg = '(?:[0-9a-fA-F]{1,4})'
const IPv6Reg = new RegExp(
'^(' +
`(?:${v6Seg}:){7}(?:${v6Seg}|:)|` +
`(?:${v6Seg}:){6}(?:${v4Str}|:${v6Seg}|:)|` +
`(?:${v6Seg}:){5}(?::${v4Str}|(:${v6Seg}){1,2}|:)|` +
`(?:${v6Seg}:){4}(?:(:${v6Seg}){0,1}:${v4Str}|(:${v6Seg}){1,3}|:)|` +
`(?:${v6Seg}:){3}(?:(:${v6Seg}){0,2}:${v4Str}|(:${v6Seg}){1,4}|:)|` +
`(?:${v6Seg}:){2}(?:(:${v6Seg}){0,3}:${v4Str}|(:${v6Seg}){1,5}|:)|` +
`(?:${v6Seg}:){1}(?:(:${v6Seg}){0,4}:${v4Str}|(:${v6Seg}){1,6}|:)|` +
`(?::((?::${v6Seg}){0,5}:${v4Str}|(?::${v6Seg}){1,7}|:))` +
')(%[0-9a-zA-Z-.:]{1,})?$'
)
export function isIPv6(s: string) {
return IPv6Reg.test(s)
}

View file

@ -2,6 +2,7 @@ import type { RequestHandler } from '../next'
// this must come first as it includes require hooks
import { initializeServerWorker } from './setup-server-worker'
import { formatHostname } from './format-hostname'
import next from '../next'
import { PropagateToWorkersField } from './router-utils/types'
@ -98,7 +99,7 @@ export async function initialize(opts: {
...opts,
_routerWorker: opts.workerType === 'router',
_renderWorker: opts.workerType === 'render',
hostname: hostname === '0.0.0.0' ? 'localhost' : hostname,
hostname,
customServer: false,
httpServer: server,
port: opts.port,
@ -111,7 +112,8 @@ export async function initialize(opts: {
result = {
port,
hostname: hostname === '0.0.0.0' ? '127.0.0.1' : hostname,
hostname: formatHostname(hostname),
}
return result
}

View file

@ -17,8 +17,10 @@ import { proxyRequest } from './router-utils/proxy-request'
import { getResolveRoutes } from './router-utils/resolve-routes'
import { PERMANENT_REDIRECT_STATUS } from '../../shared/lib/constants'
import { splitCookiesString, toNodeOutgoingHttpHeaders } from '../web/utils'
import { formatHostname } from './format-hostname'
import { signalFromNodeResponse } from '../web/spec-extension/adapters/next-request'
import { getMiddlewareRouteMatcher } from '../../shared/lib/router/utils/middleware-route-matcher'
import type { RenderWorker } from './router-server'
import { pipeReadable } from '../pipe-readable'
type RouteResult =
@ -56,7 +58,7 @@ export async function makeResolver(
dir: string,
nextConfig: NextConfigComplete,
middleware: MiddlewareConfig,
serverAddr: Partial<ServerAddress>
{ hostname = 'localhost', port = 3000 }: Partial<ServerAddress>
) {
const fsChecker = await setupFsCheck({
dir,
@ -68,6 +70,8 @@ export async function makeResolver(
dir,
!!nextConfig.experimental.appDir
)
// we format the hostname so that it can be fetched
const fetchHostname = formatHostname(hostname)
fsChecker.ensureCallback(async (item) => {
let result: string | null = null
@ -108,7 +112,10 @@ export async function makeResolver(
}
: {}
const middlewareServerPort = await new Promise((resolve) => {
const middlewareServerAddr = await new Promise<{
hostname: string
port: number
}>((resolve) => {
const srv = http.createServer(async (req, res) => {
const cloneableBody = getCloneableBody(req)
try {
@ -128,9 +135,7 @@ export async function makeResolver(
basePath: nextConfig.basePath,
trailingSlash: nextConfig.trailingSlash,
},
url: `http://${serverAddr.hostname || 'localhost'}:${
serverAddr.port || 3000
}${req.url}`,
url: `http://${fetchHostname}:${port}${req.url}`,
body: cloneableBody,
signal: signalFromNodeResponse(res),
},
@ -172,7 +177,14 @@ export async function makeResolver(
}
})
srv.on('listening', () => {
resolve((srv.address() as any).port)
const srvAddr = srv.address()
if (!srvAddr || typeof srvAddr === 'string') {
throw new Error("Failed to determine middleware's host/port.")
}
resolve({
hostname: srvAddr.address,
port: srvAddr.port,
})
})
srv.listen(0)
})
@ -191,8 +203,8 @@ export async function makeResolver(
nextConfig,
{
dir,
port: serverAddr.port || 3000,
hostname: serverAddr.hostname,
port,
hostname,
isNodeDebugging: false,
dev: true,
workerType: 'render',
@ -201,15 +213,15 @@ export async function makeResolver(
pages: {
async initialize() {
return {
port: middlewareServerPort,
hostname: '127.0.0.1',
port: middlewareServerAddr.port,
hostname: formatHostname(middlewareServerAddr.hostname),
}
},
async deleteCache() {},
async clearModuleContext() {},
async deleteAppClientCache() {},
async propagateServerField() {},
} as any,
} as Partial<RenderWorker> as any,
},
{} as any
)

View file

@ -11,6 +11,7 @@ import { getCloneableBody } from '../../body-streams'
import { filterReqHeaders, ipcForbiddenHeaders } from '../server-ipc/utils'
import { Header } from '../../../lib/load-custom-routes'
import { stringifyQuery } from '../../server-route-utils'
import { formatHostname } from '../format-hostname'
import { toNodeOutgoingHttpHeaders } from '../../web/utils'
import { invokeRequest } from '../server-ipc/invoke-request'
import { isAbortError } from '../../pipe-readable'
@ -137,7 +138,9 @@ export function getResolveRoutes(
const initUrl = (config.experimental as any).trustHostHeader
? `https://${req.headers.host || 'localhost'}${req.url}`
: opts.port
? `${protocol}://${opts.hostname || 'localhost'}:${opts.port}${req.url}`
? `${protocol}://${formatHostname(opts.hostname || 'localhost')}:${
opts.port
}${req.url}`
: req.url || ''
addRequestMeta(req, '__NEXT_INIT_URL', initUrl)

View file

@ -66,7 +66,7 @@ export async function createIpcServer(
)
const ipcPort = await new Promise<number>((resolveIpc) => {
ipcServer.listen(0, '0.0.0.0', () => {
ipcServer.listen(0, server.hostname, () => {
const addr = ipcServer.address()
if (addr && typeof addr === 'object') {

View file

@ -11,11 +11,6 @@ export const invokeRequest = async (
},
readableBody?: Readable | ReadableStream
) => {
// force to 127.0.0.1 as IPC always runs on this hostname
// to avoid localhost issues
const parsedTargetUrl = new URL(targetUrl)
parsedTargetUrl.hostname = '127.0.0.1'
const invokeHeaders = filterReqHeaders(
{
'cache-control': '',
@ -24,7 +19,7 @@ export const invokeRequest = async (
ipcForbiddenHeaders
) as IncomingMessage['headers']
return await fetch(parsedTargetUrl.toString(), {
return await fetch(targetUrl, {
headers: invokeHeaders as any as Headers,
method: requestInit.method,
redirect: 'manual',

View file

@ -29,13 +29,13 @@ export const deserializeErr = (serializedErr: any) => {
}
export async function invokeIpcMethod({
hostname = '127.0.0.1',
fetchHostname = 'localhost',
method,
args,
ipcPort,
ipcKey,
}: {
hostname?: string
fetchHostname?: string
method: string
args: any[]
ipcPort?: string
@ -43,7 +43,7 @@ export async function invokeIpcMethod({
}): Promise<any> {
if (ipcPort) {
const res = await invokeRequest(
`http://${hostname}:${ipcPort}?key=${ipcKey}&method=${
`http://${fetchHostname}:${ipcPort}?key=${ipcKey}&method=${
method as string
}&args=${encodeURIComponent(JSON.stringify(args))}`,
{

View file

@ -87,25 +87,30 @@ export async function initializeServerWorker(
upgradeHandler(req, socket, upgrade)
})
}
const hostname =
!opts.hostname || opts.hostname === 'localhost'
? '0.0.0.0'
: opts.hostname
let hostname = opts.hostname || 'localhost'
server.on('listening', async () => {
try {
const addr = server.address()
const host = addr
? typeof addr === 'object'
? addr.address
: addr
: undefined
const port = addr && typeof addr === 'object' ? addr.port : 0
if (!port) {
console.error(`Invariant failed to detect render worker port`, addr)
if (!port || !host) {
console.error(
`Invariant failed to detect render worker host/port`,
addr
)
process.exit(1)
}
resolve({
server,
port,
hostname,
hostname: host,
})
} catch (err) {
return reject(err)

View file

@ -3,10 +3,10 @@ import '../node-polyfill-fetch'
import type { IncomingMessage, ServerResponse } from 'http'
import http from 'http'
import { isIPv6 } from 'net'
import * as Log from '../../build/output/log'
import setupDebug from 'next/dist/compiled/debug'
import { getDebugPort } from './utils'
import { formatHostname } from './format-hostname'
import { initialize } from './router-server'
import {
WorkerRequestHandler,
@ -147,25 +147,26 @@ export async function startServer({
}
})
let targetHost = hostname
const isNodeDebugging = checkIsNodeDebugging()
await new Promise<void>((resolve) => {
server.on('listening', async () => {
const addr = server.address()
const actualHostname = formatHostname(
typeof addr === 'object'
? addr?.address || hostname || 'localhost'
: addr
)
const formattedHostname =
!hostname || hostname === '0.0.0.0'
? 'localhost'
: actualHostname === '[::]'
? '[::1]'
: actualHostname
port = typeof addr === 'object' ? addr?.port || port : port
let host = !hostname || hostname === '0.0.0.0' ? 'localhost' : hostname
let normalizedHostname = hostname || '0.0.0.0'
if (isIPv6(hostname)) {
host = host === '::' ? '[::1]' : `[${host}]`
normalizedHostname = `[${hostname}]`
}
targetHost = host
const appUrl = `http://${host}:${port}`
const appUrl = `http://${formattedHostname}:${port}`
if (isNodeDebugging) {
const debugPort = getDebugPort()
@ -177,11 +178,7 @@ export async function startServer({
}
if (logReady) {
Log.ready(
`started server on ${normalizedHostname}${
(port + '').startsWith(':') ? '' : ':'
}${port}, url: ${appUrl}`
)
Log.ready(`started server on ${actualHostname}:${port}, url: ${appUrl}`)
// expose the main port to render workers
process.env.PORT = port + ''
}
@ -202,7 +199,7 @@ export async function startServer({
dir,
port,
isDev,
hostname: targetHost,
hostname,
minimalMode,
isNodeDebugging: Boolean(isNodeDebugging),
keepAliveTimeout,
@ -219,6 +216,6 @@ export async function startServer({
resolve()
})
server.listen(port, hostname === 'localhost' ? '0.0.0.0' : hostname)
server.listen(port, hostname)
})
}

View file

@ -425,7 +425,7 @@ export default class NextNodeServer extends BaseServer {
trustHostHeader: this.nextConfig.experimental.trustHostHeader,
allowedRevalidateHeaderKeys:
this.nextConfig.experimental.allowedRevalidateHeaderKeys,
hostname: this.hostname,
hostname: this.fetchHostname,
minimalMode: this.minimalMode,
dev: this.renderOpts.dev === true,
query,
@ -515,7 +515,7 @@ export default class NextNodeServer extends BaseServer {
if (this.isRenderWorker) {
const invokeRes = await invokeRequest(
`http://${this.hostname || '127.0.0.1'}:${this.port}${
`http://${this.fetchHostname || 'localhost'}:${this.port}${
newReq.url || ''
}`,
{
@ -1496,7 +1496,7 @@ export default class NextNodeServer extends BaseServer {
const locale = params.parsed.query.__nextLocale
url = `${getRequestMeta(params.request, '_protocol')}://${
this.hostname
this.fetchHostname
}:${this.port}${locale ? `/${locale}` : ''}${params.parsed.pathname}${
query ? `?${query}` : ''
}`
@ -1747,9 +1747,9 @@ export default class NextNodeServer extends BaseServer {
// When there are hostname and port we build an absolute URL
const initUrl =
this.hostname && this.port
? `${protocol}://${this.hostname}:${this.port}${req.url}`
: (this.nextConfig.experimental as any).trustHostHeader
this.fetchHostname && this.port
? `${protocol}://${this.fetchHostname}:${this.port}${req.url}`
: this.nextConfig.experimental.trustHostHeader
? `https://${req.headers.host || 'localhost'}${req.url}`
: req.url

View file

@ -20,7 +20,7 @@ interface Options {
}
const REGEX_LOCALHOST_HOSTNAME =
/(?!^https?:\/\/)(127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}|::1|localhost)/
/(?!^https?:\/\/)(127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}|\[::1\]|localhost)/
function parseURL(url: string | URL, base?: string | URL) {
return new URL(

View file

@ -67,7 +67,7 @@ if (!(globalThis as any).isNextStart) {
const appPort = await findPort()
server = await initNextServerScript(
testServer,
/Listening on/,
/ready started server on/,
{
...process.env,
PORT: appPort.toString(),

View file

@ -363,7 +363,7 @@ describe('CLI Usage', () => {
}
)
try {
await check(() => output, new RegExp(`on 0.0.0.0:${port}`))
await check(() => output, new RegExp(`on \\[::\\]:${port}`))
await check(() => output, new RegExp(`http://localhost:${port}`))
} finally {
await killApp(app)
@ -383,12 +383,12 @@ describe('CLI Usage', () => {
}
)
try {
await check(() => output, new RegExp(`on 0.0.0.0:${port}`))
await check(() => output, new RegExp(`on \\[::\\]:${port}`))
await check(() => output, new RegExp(`http://localhost:${port}`))
} finally {
await killApp(app)
}
const matches = /on 0.0.0.0:(\d+)/.exec(output)
const matches = /on \[::\]:(\d+)/.exec(output)
expect(matches).not.toBe(null)
const _port = parseInt(matches[1])
@ -408,8 +408,8 @@ describe('CLI Usage', () => {
},
})
try {
await check(() => output, /on 0.0.0.0:(\d+)/)
const matches = /on 0.0.0.0:(\d+)/.exec(output)
await check(() => output, /on \[::\]:(\d+)/)
const matches = /on \[::\]:(\d+)/.exec(output)
const _port = parseInt(matches[1])
expect(matches).not.toBe(null)
// Regression test: port 0 was interpreted as if no port had been
@ -434,7 +434,7 @@ describe('CLI Usage', () => {
}
)
try {
await check(() => output, new RegExp(`on 0.0.0.0:${port}`))
await check(() => output, new RegExp(`on \\[::\\]:${port}`))
await check(() => output, new RegExp(`http://localhost:${port}`))
} finally {
await killApp(app)
@ -451,7 +451,7 @@ describe('CLI Usage', () => {
env: { NODE_OPTIONS: '--inspect' },
})
try {
await check(() => output, new RegExp(`on 0.0.0.0:${port}`))
await check(() => output, new RegExp(`on \\[::\\]:${port}`))
await check(() => output, new RegExp(`http://localhost:${port}`))
} finally {
await killApp(app)

View file

@ -8,7 +8,6 @@ import {
} from 'fs'
import { promisify } from 'util'
import http from 'http'
import https from 'https'
import path from 'path'
import spawn from 'cross-spawn'
@ -162,19 +161,7 @@ export function fetchViaHTTP(
opts?: RequestInit
): Promise<Response> {
const url = query ? withQuery(pathname, query) : pathname
return fetch(getFullUrl(appPort, url), {
// in node.js v17 fetch favors IPv6 but Next.js is
// listening on IPv4 by default so force IPv4 DNS resolving
agent: (parsedUrl) => {
if (parsedUrl.protocol === 'https:') {
return new https.Agent({ family: 4 })
}
if (parsedUrl.protocol === 'http:') {
return new http.Agent({ family: 4 })
}
},
...opts,
})
return fetch(getFullUrl(appPort, url), opts)
}
export function renderViaHTTP(

View file

@ -103,7 +103,7 @@ describe('pnpm support', () => {
)
server = await initNextServerScript(
path.join(standaloneDir, 'server.js'),
/Listening/,
/ready started server on/,
{
...process.env,
PORT: appPort,

View file

@ -76,7 +76,7 @@ describe('should set-up next', () => {
appPort = await findPort()
server = await initNextServerScript(
testServer,
/Listening on/,
/ready started server on/,
{
...process.env,
PORT: appPort,

View file

@ -112,7 +112,7 @@ describe('should set-up next', () => {
appPort = await findPort()
server = await initNextServerScript(
testServer,
/Listening on/,
/ready started server on/,
{
...process.env,
PORT: appPort,

View file

@ -119,7 +119,7 @@ describe('should set-up next', () => {
appPort = await findPort()
server = await initNextServerScript(
testServer,
/Listening on/,
/ready started server on/,
{
...process.env,
PORT: appPort,
@ -1289,7 +1289,7 @@ describe('should set-up next', () => {
appPort = await findPort()
server = await initNextServerScript(
testServer,
/Listening on/,
/ready started server on/,
{
...process.env,
PORT: appPort,

View file

@ -63,7 +63,7 @@ describe('minimal-mode-response-cache', () => {
appPort = await findPort()
server = await initNextServerScript(
testServer,
/Listening on/,
/ready started server on/,
{
...process.env,
HOSTNAME: '',
@ -141,9 +141,12 @@ describe('minimal-mode-response-cache', () => {
expect(res2.headers.get('content-type')).toContain('text/html')
})
it('should have correct "Listening on" log', async () => {
expect(output).toContain(`Listening on port`)
expect(output).toContain(`url: http://localhost:${appPort}`)
it('should have correct "Started server on" log', async () => {
expect(output).toContain(`started server on`)
let pattern = new RegExp(
`url: http://localhost:${appPort}|url: http://127.0.0.1:${appPort}|url: http://\\[::1\\]:${appPort}`
)
expect(output).toMatch(pattern)
})
it('should have correct responses', async () => {

View file

@ -43,7 +43,7 @@ describe('type-module', () => {
const appPort = await findPort()
const server = await initNextServerScript(
serverFile,
/Listening on/,
/ready started server on/,
{ ...process.env, PORT: appPort.toString() },
undefined,
{ cwd: next.testDir }

View file

@ -197,12 +197,12 @@ it('consider 127.0.0.1 and variations as localhost', () => {
const httpUrl = new NextURL('http://localhost:3000/hello')
expect(new NextURL('http://127.0.0.1:3000/hello')).toStrictEqual(httpUrl)
expect(new NextURL('http://127.0.1.0:3000/hello')).toStrictEqual(httpUrl)
expect(new NextURL('http://::1:3000/hello')).toStrictEqual(httpUrl)
expect(new NextURL('http://[::1]:3000/hello')).toStrictEqual(httpUrl)
const httpsUrl = new NextURL('https://localhost:3000/hello')
expect(new NextURL('https://127.0.0.1:3000/hello')).toStrictEqual(httpsUrl)
expect(new NextURL('https://127.0.1.0:3000/hello')).toStrictEqual(httpsUrl)
expect(new NextURL('https://::1:3000/hello')).toStrictEqual(httpsUrl)
expect(new NextURL('https://[::1]:3000/hello')).toStrictEqual(httpsUrl)
})
it('allows to change the port', () => {