Skip to content

Commit

Permalink
Add x-forwarded-* headers to turbopack renders (#50012)
Browse files Browse the repository at this point in the history
This PR forwards the [new
`ServerInfo`](vercel/turbo#5018) struct to
Turbopack's rendering processes, allowing it to initialize the
requesting headers with the `x-forwarded-*` headers. These headers, and
`x-forwarded-proto` in particular, are necessary for [NextAuth to
work](https://github.com/nextauthjs/next-auth/blob/f62c0167/packages/next-auth/src/core/index.ts#L73-L76).

These headers are initialized in the non-turbo server via
[`http-proxy`](https://github.com/http-party/node-http-proxy/blob/9b96cd72/lib/http-proxy/passes/web-incoming.js#L58-L86).

Fixes WEB-1064
  • Loading branch information
jridgewell committed May 19, 2023
1 parent 702eb17 commit 3558208
Show file tree
Hide file tree
Showing 15 changed files with 108 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import type { RenderOpts } from 'next/dist/server/app-render/types'

import { renderToHTMLOrFlight } from 'next/dist/server/app-render/app-render'
import { RSC_VARY_HEADER } from 'next/dist/client/components/app-router-headers'
import { headersFromEntries } from '../internal/headers'
import { headersFromEntries, initProxiedHeaders } from '../internal/headers'
import { parse, ParsedUrlQuery } from 'node:querystring'
import { PassThrough } from 'node:stream'
;('TURBOPACK { transition: next-layout-entry; chunking-type: isolatedParallel }')
Expand Down Expand Up @@ -247,7 +247,10 @@ async function runOperation(renderData: RenderData) {
const req: IncomingMessage = {
url: renderData.originalUrl,
method: renderData.method,
headers: headersFromEntries(renderData.rawHeaders),
headers: initProxiedHeaders(
headersFromEntries(renderData.rawHeaders),
renderData.data?.serverInfo
),
} as any

const res = createServerResponse(req, renderData.path)
Expand Down
16 changes: 8 additions & 8 deletions packages/next-swc/crates/next-core/js/src/entry/router.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import type { Ipc, StructuredError } from '@vercel/turbopack-node/ipc/index'
import type { IncomingMessage, ServerResponse } from 'node:http'
import { Buffer } from 'node:buffer'
import { createServer, makeRequest } from '../internal/server'
import { createServer, makeRequest, type ServerInfo } from '../internal/server'
import { toPairs } from '../internal/headers'
import {
makeResolver,
RouteResult,
ServerAddress,
type RouteResult,
} from 'next/dist/server/lib/route-resolver'
import loadConfig from 'next/dist/server/config'
import { PHASE_DEVELOPMENT_SERVER } from 'next/dist/shared/lib/constants'
Expand Down Expand Up @@ -58,7 +57,7 @@ let resolveRouteMemo: Promise<

async function getResolveRoute(
dir: string,
serverAddr: Partial<ServerAddress>
serverInfo: ServerInfo
): ReturnType<
typeof import('next/dist/server/lib/route-resolver').makeResolver
> {
Expand All @@ -74,17 +73,17 @@ async function getResolveRoute(
matcher: middlewareConfig.matcher,
}

return await makeResolver(dir, nextConfig, middlewareCfg, serverAddr)
return await makeResolver(dir, nextConfig, middlewareCfg, serverInfo)
}

export default async function route(
ipc: Ipc<RouterRequest, IpcOutgoingMessage>,
routerRequest: RouterRequest,
dir: string,
serverAddr: Partial<ServerAddress>
serverInfo: ServerInfo
) {
const [resolveRoute, server] = await Promise.all([
(resolveRouteMemo ??= getResolveRoute(dir, serverAddr)),
(resolveRouteMemo ??= getResolveRoute(dir, serverInfo)),
createServer(),
])

Expand All @@ -99,7 +98,8 @@ export default async function route(
routerRequest.method,
routerRequest.pathname,
routerRequest.rawQuery,
routerRequest.rawHeaders
routerRequest.rawHeaders,
serverInfo
)

const body = Buffer.concat(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,8 @@ export default function startHandler(handler: Handler): void {
renderData.method,
renderData.path,
renderData.rawQuery,
renderData.rawHeaders
renderData.rawHeaders,
renderData.data?.serverInfo
)

return {
Expand Down
24 changes: 21 additions & 3 deletions packages/next-swc/crates/next-core/js/src/internal/headers.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { ServerInfo } from './server'

export type Headers = Record<string, string | string[]>
/**
* Converts an array of raw header entries to a map of header names to values.
*/
export function headersFromEntries(
entries: Array<[string, string]>
): Record<string, string | string[]> {
export function headersFromEntries(entries: Array<[string, string]>): Headers {
const headers: Record<string, string | string[]> = Object.create(null)
for (const [key, value] of entries) {
if (key in headers) {
Expand Down Expand Up @@ -41,3 +42,20 @@ export function toPairs<T>(arr: T[]): Array<[T, T]> {

return pairs
}

/**
* These headers are provided by default to match the http-proxy behavior
* https://github.com/http-party/node-http-proxy/blob/9b96cd72/lib/http-proxy/passes/web-incoming.js#L58-L86
*/
export function initProxiedHeaders(
headers: Headers,
proxiedFor: ServerInfo | null | undefined
): Headers {
const hostname = proxiedFor?.hostname || 'localhost'
const port = String(proxiedFor?.port || 3000)
headers['x-forwarded-for'] = proxiedFor?.ip || '::1'
headers['x-forwarded-host'] = `${hostname}:${port}`
headers['x-forwarded-port'] = port
headers['x-forwarded-proto'] = proxiedFor?.protocol || 'http'
return headers
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { buildStaticPaths } from 'next/dist/build/utils'
import type { BuildManifest } from 'next/dist/server/get-page-files'
import type { ReactLoadableManifest } from 'next/dist/server/load-components'

import { headersFromEntries } from './headers'
import { headersFromEntries, initProxiedHeaders } from './headers'
import { createServerResponse } from './http'
import type { Ipc } from '@vercel/turbopack-node/ipc/index'
import type { RenderData } from 'types/turbopack'
Expand Down Expand Up @@ -218,7 +218,10 @@ export default function startHandler({
const req: IncomingMessage = {
url: renderData.url,
method: 'GET',
headers: headersFromEntries(renderData.rawHeaders),
headers: initProxiedHeaders(
headersFromEntries(renderData.rawHeaders),
renderData.data?.serverInfo
),
} as any
const res: ServerResponse = createServerResponse(req, renderData.path)

Expand Down
17 changes: 14 additions & 3 deletions packages/next-swc/crates/next-core/js/src/internal/server.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import type { ClientRequest, IncomingMessage, Server } from 'node:http'
import type { AddressInfo } from 'node:net'
import http, { ServerResponse } from 'node:http'
import { headersFromEntries } from './headers'
import { headersFromEntries, initProxiedHeaders } from './headers'

export type ServerInfo = Partial<{
hostname: string
port: number
ip: string
protocol: string
}>

/**
* Creates a server that listens a random port.
Expand All @@ -24,7 +31,8 @@ export function makeRequest(
method: string,
path: string,
rawQuery?: string,
rawHeaders?: [string, string][]
rawHeaders?: [string, string][],
proxiedFor?: ServerInfo
): Promise<{
clientRequest: ClientRequest
clientResponsePromise: Promise<IncomingMessage>
Expand Down Expand Up @@ -101,13 +109,16 @@ export function makeRequest(

const address = server.address() as AddressInfo

const headers = headersFromEntries(rawHeaders ?? [])
initProxiedHeaders(headers, proxiedFor)

clientRequest = http.request({
host: 'localhost',
port: address.port,
method,
path:
rawQuery != null && rawQuery.length > 0 ? `${path}?${rawQuery}` : path,
headers: rawHeaders != null ? headersFromEntries(rawHeaders) : undefined,
headers,
})

// Otherwise Node.js waits for the first chunk of data to be written before sending the request.
Expand Down
2 changes: 2 additions & 0 deletions packages/next-swc/crates/next-core/js/types/turbopack.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ServerInfo } from '@vercel/turbopack-next/internal/server'
import type { RenderOptsPartial } from 'next/dist/server/render'

export type RenderData = {
Expand All @@ -10,5 +11,6 @@ export type RenderData = {
rawHeaders: Array<[string, string]>
data?: {
nextConfigOutput?: RenderOptsPartial['nextConfigOutput']
serverInfo?: ServerInfo
}
}
2 changes: 1 addition & 1 deletion packages/next-swc/crates/next-core/src/app_source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ pub async fn create_app_source(
client_compile_time_info,
next_config,
);
let render_data = render_data(next_config);
let render_data = render_data(next_config, server_addr);

let entrypoints = entrypoints.await?;
let mut sources: Vec<_> = entrypoints
Expand Down
2 changes: 1 addition & 1 deletion packages/next-swc/crates/next-core/src/page_source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ pub async fn create_page_source(
next_config,
);

let render_data = render_data(next_config);
let render_data = render_data(next_config, server_addr);
let page_extensions = next_config.page_extensions();

let mut sources = vec![];
Expand Down
7 changes: 2 additions & 5 deletions packages/next-swc/crates/next-core/src/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use turbopack_binding::{
changed::any_content_changed,
chunk::ChunkingContext,
context::{AssetContext, AssetContextVc},
environment::{EnvironmentIntention::Middleware, ServerAddrVc},
environment::{EnvironmentIntention::Middleware, ServerAddrVc, ServerInfo},
ident::AssetIdentVc,
issue::IssueVc,
reference_type::{EcmaScriptModulesReferenceSubType, ReferenceType},
Expand Down Expand Up @@ -388,10 +388,7 @@ async fn route_internal(
vec![
JsonValueVc::cell(request),
JsonValueVc::cell(dir.to_string_lossy().into()),
JsonValueVc::cell(json!({
"hostname": server_addr.hostname(),
"port": server_addr.port(),
})),
JsonValueVc::cell(serde_json::to_value(ServerInfo::try_from(&*server_addr)?)?),
],
CompletionsVc::all(vec![next_config_changed, routes_changed]),
/* debug */ false,
Expand Down
9 changes: 8 additions & 1 deletion packages/next-swc/crates/next-core/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use turbopack_binding::{
turbopack::{
core::{
asset::{Asset, AssetVc},
environment::{ServerAddrVc, ServerInfo},
ident::AssetIdentVc,
issue::{Issue, IssueSeverity, IssueSeverityVc, IssueVc, OptionIssueSourceVc},
reference_type::{EcmaScriptModulesReferenceSubType, ReferenceType},
Expand Down Expand Up @@ -356,17 +357,23 @@ pub async fn load_next_json<T: DeserializeOwned>(
}

#[turbo_tasks::function]
pub async fn render_data(next_config: NextConfigVc) -> Result<JsonValueVc> {
pub async fn render_data(
next_config: NextConfigVc,
server_addr: ServerAddrVc,
) -> Result<JsonValueVc> {
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct Data {
next_config_output: Option<OutputType>,
server_info: Option<ServerInfo>,
}

let config = next_config.await?;
let server_info = ServerInfo::try_from(&*server_addr.await?);

let value = serde_json::to_value(Data {
next_config_output: config.output.clone(),
server_info: server_info.ok(),
})?;
Ok(JsonValue(value).cell())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
const headers = request.headers
return NextResponse.json({
host: headers.get('x-forwarded-host'),
ip: headers.get('x-forwarded-ip'),
port: headers.get('x-forwarded-port'),
proto: headers.get('x-forwarded-proto'),
})
}

export const config = {
matcher: '/headers',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/** @type {import('next').NextConfig} */
module.exports = {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useTestHarness } from '@turbo/pack-test-harness'

export default function Foo() {
useTestHarness(runTests)

return 'index'
}

function runTests() {
it('should stream middleware response from node', async () => {
const res = await fetch('/headers')
const json = await res.json()

const { host, port, proto } = json
expect(host).toBe(`localhost:${port}`)
expect(port).toMatch(/\d+/)
expect(proto).toBe('http')
})
}
4 changes: 2 additions & 2 deletions packages/next/src/server/lib/route-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ export type MiddlewareConfig = {
}

export type ServerAddress = {
hostname: string
port: number
hostname?: string | null
port?: number | null
}

export type RouteResult =
Expand Down

0 comments on commit 3558208

Please sign in to comment.