Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 4 additions & 7 deletions crates/next-core/src/next_app/app_page_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,17 +145,14 @@ async fn wrap_edge_page(
) -> Result<Vc<Box<dyn Module>>> {
const INNER: &str = "INNER_PAGE_ENTRY";

let next_config_val = &*next_config.await?;

let source = load_next_js_template(
"edge-ssr-app.js",
project_root.clone(),
&[("VAR_USERLAND", INNER), ("VAR_PAGE", &page.to_string())],
&[
// TODO do we really need to pass the entire next config here?
// This is bad for invalidation as any config change will invalidate this
("nextConfig", &*serde_json::to_string(next_config_val)?),
],
&[(
"cacheMaxMemorySize",
&*serde_json::to_string(&next_config.cache_max_memory_size().await?)?,
)],
&[("incrementalCacheHandler", None)],
)
.await?;
Expand Down
7 changes: 4 additions & 3 deletions crates/next-core/src/next_app/app_route_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,13 +136,14 @@ async fn wrap_edge_route(
) -> Result<Vc<Box<dyn Module>>> {
let inner = rcstr!("INNER_ROUTE_ENTRY");

let next_config = &*next_config.await?;

let source = load_next_js_template(
"edge-app-route.js",
project_root.clone(),
&[("VAR_USERLAND", &*inner), ("VAR_PAGE", &page.to_string())],
&[("nextConfig", &*serde_json::to_string(next_config)?)],
&[(
"cacheLifeProfiles",
&*serde_json::to_string(&next_config.cache_life().await?)?,
)],
&[],
)
.await?;
Expand Down
71 changes: 12 additions & 59 deletions crates/next-core/src/next_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,11 @@ pub struct NextConfig {
config_file: Option<RcStr>,
config_file_name: RcStr,

cache_life: Option<JsonValue>,
/// In-memory cache size in bytes.
///
/// If `cache_max_memory_size: 0` disables in-memory caching.
cache_max_memory_size: Option<f64>,
cache_max_memory_size: Option<JsonValue>,
/// custom path to a cache handler to use
cache_handler: Option<RcStr>,
cache_handlers: Option<FxIndexMap<RcStr, RcStr>>,
Expand Down Expand Up @@ -835,7 +836,6 @@ pub struct ExperimentalConfig {
adjust_font_fallbacks_with_size_adjust: Option<bool>,
after: Option<bool>,
app_document_preloading: Option<bool>,
cache_life: Option<FxIndexMap<String, CacheLifeProfile>>,
Copy link
Member Author

Choose a reason for hiding this comment

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

An example of why this was a bad idea: This config was moved out of experimental, but Turbopack's config was never updated, so edge-app-route.ts​ was probably reading the wrong value when using Turbopack...

case_sensitive_routes: Option<bool>,
cpus: Option<f64>,
cra_compat: Option<bool>,
Expand Down Expand Up @@ -908,63 +908,6 @@ pub struct ExperimentalConfig {
devtool_segment_explorer: Option<bool>,
}

#[derive(
Clone, Debug, PartialEq, Serialize, Deserialize, TraceRawVcs, NonLocalValue, OperationValue,
)]
#[serde(rename_all = "camelCase")]
pub struct CacheLifeProfile {
#[serde(skip_serializing_if = "Option::is_none")]
pub stale: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub revalidate: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expire: Option<u32>,
}
Comment on lines -915 to -922
Copy link
Member Author

Choose a reason for hiding this comment

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

We don't actually care what the contents of CacheLifeProfile are, only that we can round-trip it. So serde_json::Value seems like a much simpler/better choice.


#[test]
fn test_cache_life_profiles() {
let json = serde_json::json!({
"cacheLife": {
"frequent": {
"stale": 19,
"revalidate": 100,
},
}
});

let config: ExperimentalConfig = serde_json::from_value(json).unwrap();
let mut expected_cache_life = FxIndexMap::default();

expected_cache_life.insert(
"frequent".to_string(),
CacheLifeProfile {
stale: Some(19),
revalidate: Some(100),
expire: None,
},
);

assert_eq!(config.cache_life, Some(expected_cache_life));
}

#[test]
fn test_cache_life_profiles_invalid() {
let json = serde_json::json!({
"cacheLife": {
"invalid": {
"stale": "invalid_value",
},
}
});

let result: Result<ExperimentalConfig, _> = serde_json::from_value(json);

assert!(
result.is_err(),
"Deserialization should fail due to invalid 'stale' value type"
);
}

#[derive(
Clone, Debug, PartialEq, Eq, Serialize, Deserialize, TraceRawVcs, NonLocalValue, OperationValue,
)]
Expand Down Expand Up @@ -1312,6 +1255,16 @@ impl NextConfig {
Vc::cell(self.base_path.clone())
}

#[turbo_tasks::function]
pub fn cache_life(&self) -> Vc<OptionJsonValue> {
Vc::cell(self.cache_life.clone())
}

#[turbo_tasks::function]
pub fn cache_max_memory_size(&self) -> Vc<OptionJsonValue> {
Vc::cell(self.cache_max_memory_size.clone())
}

#[turbo_tasks::function]
pub fn cache_handler(&self, project_path: FileSystemPath) -> Result<Vc<OptionFileSystemPath>> {
if let Some(handler) = &self.cache_handler {
Expand Down
9 changes: 4 additions & 5 deletions crates/next-core/src/next_pages/page_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,6 @@ async fn wrap_edge_page(
const INNER_ERROR: &str = "INNER_ERROR";
const INNER_ERROR_500: &str = "INNER_500";

let next_config_val = &*next_config.await?;

let source = load_next_js_template(
"edge-ssr.js",
project_root.clone(),
Expand All @@ -236,9 +234,10 @@ async fn wrap_edge_page(
("VAR_MODULE_GLOBAL_ERROR", INNER_ERROR),
],
&[
// TODO do we really need to pass the entire next config here?
// This is bad for invalidation as any config change will invalidate this
("nextConfig", &*serde_json::to_string(next_config_val)?),
(
"cacheMaxMemorySize",
&*serde_json::to_string(&next_config.cache_max_memory_size().await?)?,
),
(
"pageRouteModuleOptions",
&serde_json::to_string(&get_route_module_options(page.clone(), pathname.clone()))?,
Expand Down
8 changes: 4 additions & 4 deletions packages/next/src/build/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -616,7 +616,9 @@ export function getEdgeServerEntry(opts: {
absolutePagePath: opts.absolutePagePath,
page: opts.page,
appDirLoader: Buffer.from(opts.appDirLoader || '').toString('base64'),
nextConfig: Buffer.from(JSON.stringify(opts.config)).toString('base64'),
cacheLifeProfiles: Buffer.from(
JSON.stringify(opts.config.cacheLife)
).toString('base64'),
preferredRegion: opts.preferredRegion,
middlewareConfig: Buffer.from(
JSON.stringify(opts.middlewareConfig || {})
Expand Down Expand Up @@ -677,9 +679,7 @@ export function getEdgeServerEntry(opts: {
dev: opts.isDev,
isServerComponent: opts.isServerComponent,
page: opts.page,
stringifiedConfig: Buffer.from(JSON.stringify(opts.config)).toString(
'base64'
),
cacheMaxMemorySize: JSON.stringify(opts.config.cacheMaxMemorySize),
pagesType: opts.pagesType,
appDirLoader: Buffer.from(opts.appDirLoader || '').toString('base64'),
sriEnabled: !opts.isDev && !!opts.config.experimental.sri?.algorithm,
Expand Down
8 changes: 5 additions & 3 deletions packages/next/src/build/templates/edge-app-route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { EdgeRouteModuleWrapper } from '../../server/web/edge-route-module-wrapp
import * as module from 'VAR_USERLAND'

// injected by the loader afterwards.
declare const nextConfig: NextConfigComplete
// INJECT:nextConfig
declare const cacheLifeProfiles: NextConfigComplete['cacheLife']
// INJECT:cacheLifeProfiles

const maybeJSONParse = (str?: string) => (str ? JSON.parse(str) : undefined)

Expand All @@ -28,4 +28,6 @@ if (rscManifest && rscServerManifest) {

export const ComponentMod = module

export default EdgeRouteModuleWrapper.wrap(module.routeModule, { nextConfig })
export default EdgeRouteModuleWrapper.wrap(module.routeModule, {
cacheLifeProfiles,
})
8 changes: 4 additions & 4 deletions packages/next/src/build/templates/edge-ssr-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { IncrementalCache } from '../../server/lib/incremental-cache'
import * as pageMod from 'VAR_USERLAND'

import type { RequestData } from '../../server/web/types'
import type { NextConfigComplete } from '../../server/config-shared'
import { setReferenceManifestsSingleton } from '../../server/app-render/encryption-utils'
import { createServerModuleMap } from '../../server/app-render/action-utils'
import { initializeCacheHandlers } from '../../server/use-cache/handlers'
Expand All @@ -27,12 +26,12 @@ import { checkIsOnDemandRevalidate } from '../../server/api-utils'
import { CloseController } from '../../server/web/web-on-close'

declare const incrementalCacheHandler: any
declare const nextConfig: NextConfigComplete
declare const cacheMaxMemorySize: number
// OPTIONAL_IMPORT:incrementalCacheHandler
// INJECT:nextConfig
// INJECT:cacheMaxMemorySize

// Initialize the cache handlers interface.
initializeCacheHandlers(nextConfig.cacheMaxMemorySize)
initializeCacheHandlers(cacheMaxMemorySize)

const maybeJSONParse = (str?: string) => (str ? JSON.parse(str) : undefined)

Expand Down Expand Up @@ -77,6 +76,7 @@ async function requestHandler(
const {
query,
params,
nextConfig,
Copy link
Contributor

Choose a reason for hiding this comment

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

Similar to edge-ssr.ts, the edge-ssr-app.ts template extracts nextConfig from prepareResult but globalThis.nextConfig is no longer being set, causing nextConfig to be an empty object that will fail when accessing its properties.

View Details
📝 Patch Details
diff --git a/packages/next/src/build/entries.ts b/packages/next/src/build/entries.ts
index e48f0573b4..320561a8d7 100644
--- a/packages/next/src/build/entries.ts
+++ b/packages/next/src/build/entries.ts
@@ -680,6 +680,7 @@ export function getEdgeServerEntry(opts: {
     isServerComponent: opts.isServerComponent,
     page: opts.page,
     cacheMaxMemorySize: JSON.stringify(opts.config.cacheMaxMemorySize),
+    nextConfig: Buffer.from(JSON.stringify(opts.config)).toString('base64'),
     pagesType: opts.pagesType,
     appDirLoader: Buffer.from(opts.appDirLoader || '').toString('base64'),
     sriEnabled: !opts.isDev && !!opts.config.experimental.sri?.algorithm,
diff --git a/packages/next/src/build/templates/edge-ssr-app.ts b/packages/next/src/build/templates/edge-ssr-app.ts
index ba39c22871..b8d5a8c74a 100644
--- a/packages/next/src/build/templates/edge-ssr-app.ts
+++ b/packages/next/src/build/templates/edge-ssr-app.ts
@@ -24,11 +24,14 @@ import { interopDefault } from '../../lib/interop-default'
 import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths'
 import { checkIsOnDemandRevalidate } from '../../server/api-utils'
 import { CloseController } from '../../server/web/web-on-close'
+import type { NextConfigComplete } from '../../server/config-shared'
 
 declare const incrementalCacheHandler: any
 declare const cacheMaxMemorySize: number
+declare const nextConfig: NextConfigComplete
 // OPTIONAL_IMPORT:incrementalCacheHandler
 // INJECT:cacheMaxMemorySize
+// INJECT:nextConfig
 
 // Initialize the cache handlers interface.
 initializeCacheHandlers(cacheMaxMemorySize)
@@ -76,7 +79,6 @@ async function requestHandler(
   const {
     query,
     params,
-    nextConfig,
     buildId,
     buildManifest,
     prerenderManifest,
diff --git a/packages/next/src/build/templates/edge-ssr.ts b/packages/next/src/build/templates/edge-ssr.ts
index ad3fdd40c9..ceebc51115 100644
--- a/packages/next/src/build/templates/edge-ssr.ts
+++ b/packages/next/src/build/templates/edge-ssr.ts
@@ -25,13 +25,16 @@ import type { RenderResultMetadata } from '../../server/render-result'
 import { getTracer, SpanKind, type Span } from '../../server/lib/trace/tracer'
 import { BaseServerSpan } from '../../server/lib/trace/constants'
 import { HTML_CONTENT_TYPE_HEADER } from '../../lib/constants'
+import type { NextConfigComplete } from '../../server/config-shared'
 
 // injected by the loader afterwards.
 declare const cacheMaxMemorySize: number
+declare const nextConfig: NextConfigComplete
 declare const pageRouteModuleOptions: any
 declare const errorRouteModuleOptions: any
 declare const user500RouteModuleOptions: any
 // INJECT:cacheMaxMemorySize
+// INJECT:nextConfig
 // INJECT:pageRouteModuleOptions
 // INJECT:errorRouteModuleOptions
 // INJECT:user500RouteModuleOptions
@@ -102,7 +105,6 @@ async function requestHandler(
   const {
     query,
     params,
-    nextConfig,
     buildId,
     isNextDataRequest,
     buildManifest,
diff --git a/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/index.ts b/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/index.ts
index 85db21c2d5..637e2fa106 100644
--- a/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/index.ts
+++ b/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/index.ts
@@ -20,6 +20,7 @@ export type EdgeSSRLoaderQuery = {
   isServerComponent: boolean
   page: string
   cacheMaxMemorySize: string
+  nextConfig: string
   appDirLoader?: string
   pagesType: PAGE_TYPES
   sriEnabled: boolean
@@ -74,6 +75,7 @@ const edgeSSRLoader: webpack.LoaderDefinitionFunction<EdgeSSRLoaderQuery> =
       absoluteErrorPath,
       isServerComponent,
       cacheMaxMemorySize: cacheMaxMemorySizeStringified,
+      nextConfig: nextConfigBase64,
       appDirLoader: appDirLoaderBase64,
       pagesType,
       cacheHandler,
@@ -94,6 +96,10 @@ const edgeSSRLoader: webpack.LoaderDefinitionFunction<EdgeSSRLoaderQuery> =
       Buffer.from(middlewareConfigBase64, 'base64').toString()
     )
 
+    const nextConfig = JSON.parse(
+      Buffer.from(nextConfigBase64 || '', 'base64').toString()
+    )
+
     const appDirLoader = Buffer.from(
       appDirLoaderBase64 || '',
       'base64'
@@ -158,6 +164,7 @@ const edgeSSRLoader: webpack.LoaderDefinitionFunction<EdgeSSRLoaderQuery> =
         },
         {
           cacheMaxMemorySize: cacheMaxMemorySizeStringified,
+          nextConfig: JSON.stringify(nextConfig),
         },
         {
           incrementalCacheHandler: cacheHandler ?? null,
@@ -175,6 +182,7 @@ const edgeSSRLoader: webpack.LoaderDefinitionFunction<EdgeSSRLoaderQuery> =
         },
         {
           cacheMaxMemorySize: cacheMaxMemorySizeStringified,
+          nextConfig: JSON.stringify(nextConfig),
           pageRouteModuleOptions: JSON.stringify(getRouteModuleOptions(page)),
           errorRouteModuleOptions: JSON.stringify(
             getRouteModuleOptions('/_error')

Analysis

Missing nextConfig injection in edge SSR templates causes property access failures

What fails: The edge SSR templates (edge-ssr-app.ts and edge-ssr.ts) access properties on nextConfig that are undefined after incomplete refactoring, causing incorrect rendering context values and potential runtime errors for features like asset prefix, image optimization, cache configuration, and experimental features.

How to reproduce: Build and deploy a Next.js application using the edge runtime with app router features that depend on nextConfig properties:

# Build with edge runtime enabled
npm run build

# Any request to an app route at edge runtime will receive incorrect config values:
# - nextConfig.assetPrefix → undefined
# - nextConfig.experimental.taint → undefined
# - nextConfig.cacheLife → undefined
# - nextConfig.images → undefined
# etc.

Result: WIP commit e20fb2a ("perf: Don't inject the entire nextConfig in edge templates") removed the nextConfig injection from edge templates but failed to complete the migration. The templates still destructured nextConfig from prepareResult, which in edge runtime comes from globalThis.nextConfig || {} (set in route-module.ts line 227). Since globalThis.nextConfig is never initialized in edge runtime, it defaults to an empty object {}, causing all nextConfig property accesses to return undefined.

The affected code paths in edge-ssr-app.ts (lines 141-163) and edge-ssr.ts (lines 142-157) set renderContext properties using these undefined values:

  • assetPrefix: nextConfig.assetPrefix → undefined
  • nextConfigOutput: nextConfig.output → undefined
  • experimental.taint: nextConfig.experimental.taint → undefined
  • And 15+ other config properties

Expected: All nextConfig properties should have their actual values from next.config.js, not undefined. The edge template bundle should include the full nextConfig via proper injection (as in commit predecessor).

Fix: Restored the nextConfig injection pattern that was removed in the incomplete WIP commit:

  1. Re-added nextConfig parameter to EdgeSSRLoaderQuery type
  2. Updated entries.ts to pass base64-encoded nextConfig to the loader
  3. Updated next-edge-ssr-loader to decode and inject nextConfig
  4. Updated edge-ssr-app.ts and edge-ssr.ts templates to declare and use injected nextConfig instead of destructuring from prepareResult

buildId,
buildManifest,
prerenderManifest,
Expand Down
11 changes: 4 additions & 7 deletions packages/next/src/build/templates/edge-ssr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import RouteModule, {
import { WebNextRequest, WebNextResponse } from '../../server/base-http/web'

import type { RequestData } from '../../server/web/types'
import type { NextConfigComplete } from '../../server/config-shared'
import type { NextFetchEvent } from '../../server/web/spec-extension/fetch-event'
import type RenderResult from '../../server/render-result'
import type { RenderResultMetadata } from '../../server/render-result'
Expand All @@ -28,20 +27,17 @@ import { BaseServerSpan } from '../../server/lib/trace/constants'
import { HTML_CONTENT_TYPE_HEADER } from '../../lib/constants'

// injected by the loader afterwards.
declare const nextConfig: NextConfigComplete
declare const cacheMaxMemorySize: number
declare const pageRouteModuleOptions: any
declare const errorRouteModuleOptions: any
declare const user500RouteModuleOptions: any
// INJECT:nextConfig
// INJECT:cacheMaxMemorySize
// INJECT:pageRouteModuleOptions
// INJECT:errorRouteModuleOptions
// INJECT:user500RouteModuleOptions

// Initialize the cache handlers interface.
initializeCacheHandlers(nextConfig.cacheMaxMemorySize)

// expose this for the route-module
;(globalThis as any).nextConfig = nextConfig
initializeCacheHandlers(cacheMaxMemorySize)

const pageMod = {
...userlandPage,
Expand Down Expand Up @@ -106,6 +102,7 @@ async function requestHandler(
const {
query,
params,
nextConfig,
buildId,
isNextDataRequest,
buildManifest,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export type EdgeAppRouteLoaderQuery = {
page: string
appDirLoader: string
preferredRegion: string | string[] | undefined
nextConfig: string
cacheLifeProfiles: string
middlewareConfig: string
cacheHandlers: string
}
Expand All @@ -24,7 +24,7 @@ const EdgeAppRouteLoader: webpack.LoaderDefinitionFunction<EdgeAppRouteLoaderQue
preferredRegion,
appDirLoader: appDirLoaderBase64 = '',
middlewareConfig: middlewareConfigBase64 = '',
nextConfig: nextConfigBase64,
cacheLifeProfiles: cacheLifeProfilesBase64,
cacheHandlers: cacheHandlersStringified,
} = this.getOptions()

Expand Down Expand Up @@ -64,8 +64,8 @@ const EdgeAppRouteLoader: webpack.LoaderDefinitionFunction<EdgeAppRouteLoaderQue
stringifiedPagePath.length - 1
)}?${WEBPACK_RESOURCE_QUERIES.edgeSSREntry}`

const stringifiedConfig = Buffer.from(
nextConfigBase64 || '',
const stringifiedCacheLifeProfiles = Buffer.from(
cacheLifeProfilesBase64 || '',
'base64'
).toString()

Expand All @@ -76,7 +76,7 @@ const EdgeAppRouteLoader: webpack.LoaderDefinitionFunction<EdgeAppRouteLoaderQue
VAR_PAGE: page,
},
{
nextConfig: stringifiedConfig,
cacheLifeProfiles: stringifiedCacheLifeProfiles,
}
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export type EdgeSSRLoaderQuery = {
dev: boolean
isServerComponent: boolean
page: string
stringifiedConfig: string
cacheMaxMemorySize: string
appDirLoader?: string
pagesType: PAGE_TYPES
sriEnabled: boolean
Expand Down Expand Up @@ -73,7 +73,7 @@ const edgeSSRLoader: webpack.LoaderDefinitionFunction<EdgeSSRLoaderQuery> =
absolute500Path,
absoluteErrorPath,
isServerComponent,
stringifiedConfig: stringifiedConfigBase64,
cacheMaxMemorySize: cacheMaxMemorySizeStringified,
appDirLoader: appDirLoaderBase64,
pagesType,
cacheHandler,
Expand All @@ -94,10 +94,6 @@ const edgeSSRLoader: webpack.LoaderDefinitionFunction<EdgeSSRLoaderQuery> =
Buffer.from(middlewareConfigBase64, 'base64').toString()
)

const stringifiedConfig = Buffer.from(
stringifiedConfigBase64 || '',
'base64'
).toString()
const appDirLoader = Buffer.from(
appDirLoaderBase64 || '',
'base64'
Expand Down Expand Up @@ -161,7 +157,7 @@ const edgeSSRLoader: webpack.LoaderDefinitionFunction<EdgeSSRLoaderQuery> =
VAR_PAGE: page,
},
{
nextConfig: stringifiedConfig,
cacheMaxMemorySize: cacheMaxMemorySizeStringified,
},
{
incrementalCacheHandler: cacheHandler ?? null,
Expand All @@ -178,7 +174,7 @@ const edgeSSRLoader: webpack.LoaderDefinitionFunction<EdgeSSRLoaderQuery> =
VAR_MODULE_GLOBAL_ERROR: errorPath,
},
{
nextConfig: stringifiedConfig,
cacheMaxMemorySize: cacheMaxMemorySizeStringified,
pageRouteModuleOptions: JSON.stringify(getRouteModuleOptions(page)),
errorRouteModuleOptions: JSON.stringify(
getRouteModuleOptions('/_error')
Expand Down
Loading
Loading