diff --git a/packages/next-swc/crates/next-core/js/src/entry/router.ts b/packages/next-swc/crates/next-core/js/src/entry/router.ts index d04491dc40d..3d003ce41ca 100644 --- a/packages/next-swc/crates/next-core/js/src/entry/router.ts +++ b/packages/next-swc/crates/next-core/js/src/entry/router.ts @@ -1,9 +1,9 @@ -import type { Ipc } from "@vercel/turbopack-next/ipc/index"; +import type { Ipc, StructuredError } from "@vercel/turbopack-next/ipc/index"; import type { IncomingMessage, ServerResponse } from "node:http"; import { Buffer } from "node:buffer"; import { createServer, makeRequest } from "@vercel/turbopack-next/ipc/server"; import { toPairs } from "@vercel/turbopack-next/internal/headers"; -import { makeResolver } from "next/dist/server/lib/route-resolver"; +import { makeResolver, 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"; @@ -22,16 +22,6 @@ type RouterRequest = { rawQuery: string; }; -type RouteResult = - | { - type: "rewrite"; - url: string; - headers: Record; - } - | { - type: "none"; - }; - type IpcOutgoingMessage = { type: "value"; data: string | Buffer; @@ -44,6 +34,10 @@ type MessageData = type: "rewrite"; data: RewriteResponse; } + | { + type: "error"; + error: StructuredError; + } | { type: "none" }; type RewriteResponse = { @@ -155,15 +149,24 @@ async function handleClientResponse( return { type: "none", }; + case "error": + return { + type: "error", + error: data.error, + }; case "rewrite": - default: return { type: "rewrite", data: { url: data.url, - headers: Object.entries(data.headers), + headers: Object.entries(data.headers) + .filter(([, val]) => val != null) + .map(([name, value]) => [name, value!.toString()]), }, }; + default: + // @ts-expect-error data.type is never + throw new Error(`unknown route result type: ${data.type}`); } } @@ -172,7 +175,7 @@ async function handleClientResponse( headers: toPairs(clientResponse.rawHeaders), }; - ipc.send({ + await ipc.send({ type: "value", data: JSON.stringify({ type: "middleware-headers", @@ -181,7 +184,7 @@ async function handleClientResponse( }); for await (const chunk of clientResponse) { - ipc.send({ + await ipc.send({ type: "value", data: JSON.stringify({ type: "middleware-body", diff --git a/packages/next-swc/crates/next-core/src/router.rs b/packages/next-swc/crates/next-core/src/router.rs index 529a42a1d51..f312b50188b 100644 --- a/packages/next-swc/crates/next-core/src/router.rs +++ b/packages/next-swc/crates/next-core/src/router.rs @@ -32,7 +32,7 @@ use turbopack_ecmascript::{ use turbopack_node::{ evaluate::evaluate, execution_context::{ExecutionContext, ExecutionContextVc}, - source_map::StructuredError, + source_map::{StructuredError, trace_stack}, }; use crate::{ @@ -108,7 +108,7 @@ enum RouterIncomingMessage { MiddlewareHeaders { data: MiddlewareHeadersResponse }, MiddlewareBody { data: Vec }, None, - Error(StructuredError), + Error { error: StructuredError }, } #[turbo_tasks::value(eq = "manual", cell = "new", serialization = "none")] @@ -120,13 +120,13 @@ pub struct MiddlewareResponse { pub body: Stream>, } -#[derive(Debug)] #[turbo_tasks::value(eq = "manual", cell = "new", serialization = "none")] +#[derive(Debug)] pub enum RouterResult { Rewrite(RewriteResponse), Middleware(MiddlewareResponse), None, - Error, + Error(String), } #[turbo_tasks::function] @@ -375,16 +375,36 @@ async fn route_internal( JsonValueVc::cell(dir.to_string_lossy().into()), ], CompletionsVc::all(vec![next_config_changed, routes_changed]), + // true, /* debug */ false, ) .await?; let mut read = result.read(); - let Some(Ok(first)) = read.next().await else { - return Ok(RouterResult::Error.cell()); + let first = match read.next().await { + Some(Ok(first)) => first, + Some(Err(e)) => { + return Ok( + RouterResult::Error(format!("received error from javascript stream: {}", e)).cell(), + ) + } + None => { + return Ok(RouterResult::Error( + "no message received from javascript stream".to_string(), + ) + .cell()) + } }; - let first: RouterIncomingMessage = parse_json_with_source_context(first.to_str()?)?; + let first: RouterIncomingMessage = parse_json_with_source_context(first.to_str()?) + .with_context(|| { + format!( + "parsing incoming message ({})", + first + .to_str() + .expect("this was already successfully converted") + ) + })?; let (res, read) = match first { RouterIncomingMessage::Rewrite { data } => (RouterResult::Rewrite(data), Some(read)), @@ -416,7 +436,23 @@ async fn route_internal( } RouterIncomingMessage::None => (RouterResult::None, Some(read)), - _ => (RouterResult::Error, Some(read)), + RouterIncomingMessage::Error { error } => { + bail!( + trace_stack( + error, + router_asset, + chunking_context.output_root(), + project_path + ) + .await? + ) + } + RouterIncomingMessage::MiddlewareBody { .. } => ( + RouterResult::Error( + "unexpected incoming middleware body without middleware headers".to_string(), + ), + Some(read), + ), }; // Middleware will naturally drain the full stream, but the rest only take a diff --git a/packages/next-swc/crates/next-core/src/router_source.rs b/packages/next-swc/crates/next-core/src/router_source.rs index c07271d9a8d..3a1e6d6274b 100644 --- a/packages/next-swc/crates/next-core/src/router_source.rs +++ b/packages/next-swc/crates/next-core/src/router_source.rs @@ -130,8 +130,8 @@ impl ContentSource for NextRouterContentSource { .with_context(|| anyhow!("failed to fetch /{path}{}", formated_query(raw_query)))?; Ok(match &*res { - RouterResult::Error => bail!( - "error during Next.js routing for /{path}{}", + RouterResult::Error(e) => bail!( + "error during Next.js routing for /{path}{}: {e}", formated_query(raw_query) ), RouterResult::None => this diff --git a/packages/next/src/server/base-http/index.ts b/packages/next/src/server/base-http/index.ts index f72cdb3f34a..786f5529305 100644 --- a/packages/next/src/server/base-http/index.ts +++ b/packages/next/src/server/base-http/index.ts @@ -1,4 +1,4 @@ -import type { IncomingHttpHeaders } from 'http' +import type { IncomingHttpHeaders, OutgoingHttpHeaders } from 'http' import type { I18NConfig } from '../config-shared' import { PERMANENT_REDIRECT_STATUS } from '../../shared/lib/constants' @@ -55,6 +55,8 @@ export abstract class BaseNextResponse { */ abstract getHeader(name: string): string | undefined + abstract getHeaders(): OutgoingHttpHeaders + abstract body(value: string): this abstract send(): void diff --git a/packages/next/src/server/base-http/node.ts b/packages/next/src/server/base-http/node.ts index 4cb033d0ca5..8bda81681a6 100644 --- a/packages/next/src/server/base-http/node.ts +++ b/packages/next/src/server/base-http/node.ts @@ -7,6 +7,7 @@ import { parseBody } from '../api-utils/node' import { NEXT_REQUEST_META, RequestMeta } from '../request-meta' import { BaseNextRequest, BaseNextResponse } from './index' +import { OutgoingHttpHeaders } from 'node:http' type Req = IncomingMessage & { [NEXT_REQUEST_META]?: RequestMeta @@ -103,6 +104,10 @@ export class NodeNextResponse extends BaseNextResponse { return Array.isArray(values) ? values.join(',') : undefined } + getHeaders(): OutgoingHttpHeaders { + return this._res.getHeaders() + } + appendHeader(name: string, value: string): this { const currentValues = this.getHeaderValues(name) ?? [] diff --git a/packages/next/src/server/base-http/web.ts b/packages/next/src/server/base-http/web.ts index e6705089c26..1fa9df5bd85 100644 --- a/packages/next/src/server/base-http/web.ts +++ b/packages/next/src/server/base-http/web.ts @@ -1,4 +1,4 @@ -import type { IncomingHttpHeaders } from 'http' +import type { IncomingHttpHeaders, OutgoingHttpHeaders } from 'http' import { BaseNextRequest, BaseNextResponse } from './index' @@ -74,6 +74,10 @@ export class WebNextResponse extends BaseNextResponse { return this.headers.get(name) ?? undefined } + getHeaders(): OutgoingHttpHeaders { + return Object.fromEntries(this.headers.entries()) + } + hasHeader(name: string): boolean { return this.headers.has(name) } diff --git a/packages/next/src/server/lib/route-resolver.ts b/packages/next/src/server/lib/route-resolver.ts index bfd74de8c68..19975b0be7f 100644 --- a/packages/next/src/server/lib/route-resolver.ts +++ b/packages/next/src/server/lib/route-resolver.ts @@ -1,4 +1,11 @@ import type { IncomingMessage, ServerResponse } from 'http' +import { join } from 'path' + +import { + StackFrame, + parse as parseStackTrace, +} from 'next/dist/compiled/stacktrace-parser' + import type { NextConfig } from '../config' import { RouteDefinition } from '../future/route-definitions/route-definition' import { RouteKind } from '../future/route-kind' @@ -7,19 +14,32 @@ import { RouteMatch } from '../future/route-matches/route-match' import type { PageChecker, Route } from '../router' import { getMiddlewareMatchers } from '../../build/analysis/get-page-static-info' import { getMiddlewareRouteMatcher } from '../../shared/lib/router/utils/middleware-route-matcher' -import { join } from 'path' +import { + CLIENT_STATIC_FILES_PATH, + DEV_CLIENT_PAGES_MANIFEST, +} from '../../shared/lib/constants' +import { BaseNextRequest } from '../base-http' type MiddlewareConfig = { matcher: string[] files: string[] } -type RouteResult = + +export type RouteResult = | { type: 'rewrite' url: string statusCode: number headers: Record } + | { + type: 'error' + error: { + name: string + message: string + stack: StackFrame[] + } + } | { type: 'none' } @@ -73,7 +93,37 @@ export async function makeResolver( const { default: loadCustomRoutes } = require('../../lib/load-custom-routes') as typeof import('../../lib/load-custom-routes') - const devServer = new DevServer({ + const routeResults = new WeakMap() + + class TurbopackDevServerProxy extends DevServer { + // make sure static files are served by turbopack + serveStatic(): Promise { + return Promise.resolve() + } + + // make turbopack handle errors + async renderError(err: Error | null, req: BaseNextRequest): Promise { + if (err != null) { + routeResults.set(req, { + type: 'error', + error: { + name: err.name, + message: err.message, + stack: parseStackTrace(err.stack!), + }, + }) + } + + return Promise.resolve() + } + + // make turbopack handle 404s + render404(): Promise { + return Promise.resolve() + } + } + + const devServer = new TurbopackDevServerProxy({ dir, conf: nextConfig, hostname: 'localhost', @@ -82,7 +132,10 @@ export async function makeResolver( await devServer.matchers.reload() - // @ts-expect-error + // @ts-expect-error private + devServer.setDevReady!() + + // @ts-expect-error protected devServer.customRoutes = await loadCustomRoutes(nextConfig) if (middleware.files?.length) { @@ -123,7 +176,6 @@ export async function makeResolver( devServer.hasMiddleware = () => true } - const routeResults = new WeakMap() const routes = devServer.generateRoutes() // @ts-expect-error protected const catchAllMiddleware = devServer.generateCatchAllMiddlewareRoute(true) @@ -133,13 +185,30 @@ export async function makeResolver( devServer.hasPage.bind(devServer) ) + // @ts-expect-error protected + const buildId = devServer.buildId + + const pagesManifestRoute = routes.fsRoutes.find( + (r) => + r.name === + `_next/${CLIENT_STATIC_FILES_PATH}/${buildId}/${DEV_CLIENT_PAGES_MANIFEST}` + ) + if (pagesManifestRoute) { + // make sure turbopack serves this + pagesManifestRoute.fn = () => { + return { + finished: true, + } + } + } + const router = new Router({ ...routes, catchAllMiddleware, catchAllRoute: { match: getPathMatch('/:path*'), name: 'catchall route', - fn: async (req, _res, _params, parsedUrl) => { + fn: async (req, res, _params, parsedUrl) => { // clean up internal query values for (const key of Object.keys(parsedUrl.query || {})) { if (key.startsWith('_next')) { @@ -147,14 +216,17 @@ export async function makeResolver( } } - routeResults.set( - req, - url.format({ + routeResults.set(req, { + type: 'rewrite', + url: url.format({ pathname: parsedUrl.pathname, query: parsedUrl.query, hash: parsedUrl.hash, - }) - ) + }), + statusCode: 200, + headers: res.getHeaders(), + }) + return { finished: true } }, } as Route, @@ -162,15 +234,14 @@ export async function makeResolver( // @ts-expect-error internal field router.compiledRoutes = router.compiledRoutes.filter((route: Route) => { - const matches = + return ( route.type === 'rewrite' || route.type === 'redirect' || route.type === 'header' || route.name === 'catchall route' || route.name === 'middleware catchall' || - (route.name?.includes('check') && - route.name !== 'dynamic route/page check') - return matches + route.name?.includes('check') + ) }) return async function resolveRoute( @@ -188,22 +259,13 @@ export async function makeResolver( if (!res.originalResponse.headersSent) { res.setHeader('x-nextjs-route-result', '1') - const resolvedUrl = routeResults.get(req) - routeResults.delete(req) - - const routeResult: RouteResult = - resolvedUrl == null - ? { - type: 'none', - } - : { - type: 'rewrite', - url: resolvedUrl, - statusCode: 200, - headers: res.originalResponse.getHeaders(), - } + const routeResult: RouteResult = routeResults.get(req) ?? { + type: 'none', + } res.body(JSON.stringify(routeResult)).send() } + + routeResults.delete(req) } }