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

Better IPv6 support for next-server #53131

Merged
merged 29 commits into from
Aug 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
5a9b7c1
Better IPv6 support for next-server
DuCanhGH Jul 24, 2023
c977667
clean up
DuCanhGH Jul 24, 2023
6a9c346
Merge branch 'canary' into ducanhgh-fix-hostnames
DuCanhGH Jul 26, 2023
ed24851
Merge branch 'canary' into ducanhgh-fix-hostnames
DuCanhGH Jul 27, 2023
9cf58fd
clean up branch merging
DuCanhGH Jul 27, 2023
455f325
clean up
DuCanhGH Jul 27, 2023
944ab1e
Merge branch 'canary' into ducanhgh-fix-hostnames
DuCanhGH Jul 28, 2023
3015c66
Merge branch 'canary' into ducanhgh-fix-hostnames
DuCanhGH Jul 28, 2023
00a54b9
polish
DuCanhGH Jul 28, 2023
817ef02
removed hacky fetchIPv4v6
DuCanhGH Jul 28, 2023
d3a810a
force hostname to not be undefined in formatHostname
DuCanhGH Jul 28, 2023
7cefc46
fallback hostnames
DuCanhGH Jul 28, 2023
0f7a6f6
Merge branch 'canary' into ducanhgh-fix-hostnames
DuCanhGH Jul 28, 2023
8e38320
polish
DuCanhGH Jul 29, 2023
80f1fd9
Merge branch 'canary' into ducanhgh-fix-hostnames
DuCanhGH Jul 29, 2023
24250b3
Merge branch 'canary' into ducanhgh-fix-hostnames
DuCanhGH Jul 31, 2023
c0d5fae
Merge branch 'canary' of https://github.com/vercel/next.js into ducan…
timneutkens Aug 13, 2023
816d774
Move format-hostname to separate file
timneutkens Aug 13, 2023
384b89f
Format hostname using the same function in turbopack
timneutkens Aug 13, 2023
da289b2
Remove unused import
timneutkens Aug 13, 2023
f936a66
add isIPv6 implementation from node
ztanner Aug 13, 2023
0fa8f8d
lint
ztanner Aug 13, 2023
befd232
update formattedHostName & fix a few tests
ztanner Aug 13, 2023
fb9e214
woops
ztanner Aug 13, 2023
d32784f
undo standalone change
ztanner Aug 13, 2023
9e51dd9
remove forced ipv4 in fetchViaHTTP & test fixes
ztanner Aug 13, 2023
73e3271
more test updates
ztanner Aug 13, 2023
4b379c8
missed another spot
ztanner Aug 13, 2023
2b83fd9
Merge branch 'canary' into ducanhgh-fix-hostnames
timneutkens Aug 14, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,20 @@ 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
)?.encrypted
? '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 })
Expand Down
8 changes: 1 addition & 7 deletions packages/next/src/build/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/server/api-utils/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }])
)}`,
Expand Down
22 changes: 2 additions & 20 deletions packages/next/src/server/app-render/action-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
{
Expand Down Expand Up @@ -212,7 +194,7 @@ async function createRedirectRenderResult(
// }

try {
const headResponse = await fetchIPv4v6(fetchUrl, {
const headResponse = await fetch(fetchUrl, {
method: 'HEAD',
headers: forwardedHeaders,
next: {
Expand All @@ -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: {
Expand Down
6 changes: 6 additions & 0 deletions packages/next/src/server/base-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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'
Expand Down
8 changes: 4 additions & 4 deletions packages/next/src/server/dev/next-dev-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
12 changes: 12 additions & 0 deletions packages/next/src/server/lib/format-hostname.ts
Original file line number Diff line number Diff line change
@@ -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
}
42 changes: 42 additions & 0 deletions packages/next/src/server/lib/is-ipv6.ts
Original file line number Diff line number Diff line change
@@ -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)
}
6 changes: 4 additions & 2 deletions packages/next/src/server/lib/render-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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,
Expand All @@ -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
}
34 changes: 23 additions & 11 deletions packages/next/src/server/lib/route-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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),
},
Expand Down Expand Up @@ -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)
})
Expand All @@ -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',
Expand All @@ -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
)
Expand Down
5 changes: 4 additions & 1 deletion packages/next/src/server/lib/router-utils/resolve-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/server/lib/server-ipc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
7 changes: 1 addition & 6 deletions packages/next/src/server/lib/server-ipc/invoke-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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': '',
Expand All @@ -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',
Expand Down
Loading