@@ -8,7 +8,6 @@ import { patchFetch as _patchFetch } from '../../server/lib/patch-fetch'
8
8
import type { IncomingMessage , ServerResponse } from 'node:http'
9
9
import { getRequestMeta } from '../../server/request-meta'
10
10
import { getTracer , type Span , SpanKind } from '../../server/lib/trace/tracer'
11
- import type { ServerOnInstrumentationRequestError } from '../../server/app-render/types'
12
11
import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths'
13
12
import { NodeNextRequest , NodeNextResponse } from '../../server/base-http/node'
14
13
import {
@@ -18,11 +17,17 @@ import {
18
17
import { BaseServerSpan } from '../../server/lib/trace/constants'
19
18
import { getRevalidateReason } from '../../server/instrumentation/utils'
20
19
import { sendResponse } from '../../server/send-response'
21
- import { toNodeOutgoingHttpHeaders } from '../../server/web/utils'
20
+ import {
21
+ fromNodeOutgoingHttpHeaders ,
22
+ toNodeOutgoingHttpHeaders ,
23
+ } from '../../server/web/utils'
24
+ import { decodePathParams } from '../../server/lib/router-utils/decode-path-params'
25
+ import { getCacheControlHeader } from '../../server/lib/cache-control'
22
26
import { INFINITE_CACHE , NEXT_CACHE_TAGS_HEADER } from '../../lib/constants'
23
27
import {
24
28
CachedRouteKind ,
25
29
type ResponseCacheEntry ,
30
+ type ResponseGenerator ,
26
31
} from '../../server/response-cache'
27
32
28
33
import * as userland from 'VAR_USERLAND'
@@ -107,46 +112,43 @@ export async function handler(
107
112
const {
108
113
buildId,
109
114
params,
110
- parsedUrl,
111
115
nextConfig,
116
+ parsedUrl,
112
117
prerenderManifest,
113
118
routerServerContext,
114
119
isOnDemandRevalidate,
120
+ revalidateOnlyGenerated,
115
121
} = prepareResult
116
122
117
- const onInstrumentationRequestError =
118
- routeModule . instrumentationOnRequestError . bind ( routeModule )
123
+ const normalizedSrcPage = normalizeAppPath ( srcPage )
119
124
120
- const onError : ServerOnInstrumentationRequestError = (
121
- err ,
122
- _ ,
123
- errorContext
124
- ) => {
125
- if ( routerServerContext ?. logErrorWithOriginalStack ) {
126
- routerServerContext . logErrorWithOriginalStack ( err , 'app-dir' )
127
- } else {
128
- console . error ( err )
129
- }
130
- return onInstrumentationRequestError (
131
- req ,
132
- err ,
133
- {
134
- path : req . url || '/' ,
135
- headers : req . headers ,
136
- method : req . method || 'GET' ,
137
- } ,
138
- errorContext
139
- )
125
+ // TODO: rework this to not be necessary as a middleware
126
+ // rewrite should not need to pass this context like this
127
+ // maybe we rely on rewrite header instead
128
+ let resolvedPathname = getRequestMeta ( req , 'rewroteURL' )
129
+
130
+ if ( ! resolvedPathname ) {
131
+ resolvedPathname = parsedUrl . pathname || '/'
140
132
}
141
133
142
- const pathname = parsedUrl . pathname || '/'
143
- const normalizedSrcPage = normalizeAppPath ( srcPage )
134
+ if ( resolvedPathname === '/index' ) {
135
+ resolvedPathname = '/'
136
+ }
137
+ resolvedPathname = decodePathParams ( resolvedPathname )
138
+
144
139
let isIsr = Boolean (
145
140
prerenderManifest . dynamicRoutes [ normalizedSrcPage ] ||
146
- prerenderManifest . routes [ normalizedSrcPage ] ||
147
- prerenderManifest . routes [ pathname ]
141
+ prerenderManifest . routes [ resolvedPathname ]
148
142
)
149
143
144
+ let cacheKey : string | null = null
145
+
146
+ if ( isIsr && ! routeModule . isDev ) {
147
+ cacheKey = resolvedPathname
148
+ // ensure /index and / is normalized to one key
149
+ cacheKey = cacheKey === '/index' ? '/' : cacheKey
150
+ }
151
+
150
152
const supportsDynamicResponse : boolean =
151
153
// If we're in development, we always support dynamic HTML
152
154
routeModule . isDev === true ||
@@ -181,7 +183,13 @@ export async function handler(
181
183
res . on ( 'close' , cb )
182
184
} ,
183
185
onAfterTaskError : undefined ,
184
- onInstrumentationRequestError : onError ,
186
+ onInstrumentationRequestError : ( error , _request , errorContext ) =>
187
+ routeModule . onRequestError (
188
+ req ,
189
+ error ,
190
+ errorContext ,
191
+ routerServerContext
192
+ ) ,
185
193
} ,
186
194
sharedContext : {
187
195
buildId,
@@ -238,14 +246,186 @@ export async function handler(
238
246
} )
239
247
}
240
248
241
- let response : Response
249
+ const handleResponse = async ( currentSpan ?: Span ) => {
250
+ const responseGenerator : ResponseGenerator = async ( {
251
+ previousCacheEntry,
252
+ } ) => {
253
+ try {
254
+ if (
255
+ ! getRequestMeta ( req , 'minimalMode' ) &&
256
+ isOnDemandRevalidate &&
257
+ revalidateOnlyGenerated &&
258
+ ! previousCacheEntry
259
+ ) {
260
+ res . statusCode = 404
261
+ // on-demand revalidate always sets this header
262
+ res . setHeader ( 'x-nextjs-cache' , 'REVALIDATED' )
263
+ res . end ( 'This page could not be found' )
264
+ return null
265
+ }
266
+
267
+ const response = await invokeRouteModule ( currentSpan )
268
+
269
+ ; ( req as any ) . fetchMetrics = ( context . renderOpts as any ) . fetchMetrics
270
+ let pendingWaitUntil = context . renderOpts . pendingWaitUntil
271
+
272
+ // Attempt using provided waitUntil if available
273
+ // if it's not we fallback to sendResponse's handling
274
+ if ( pendingWaitUntil ) {
275
+ if ( context . renderOpts . waitUntil ) {
276
+ context . renderOpts . waitUntil ( pendingWaitUntil )
277
+ pendingWaitUntil = undefined
278
+ }
279
+ }
280
+ const cacheTags = context . renderOpts . collectedTags
281
+
282
+ // If the request is for a static response, we can cache it so long
283
+ // as it's not edge.
284
+ if ( isIsr ) {
285
+ const blob = await response . blob ( )
286
+
287
+ // Copy the headers from the response.
288
+ const headers = toNodeOutgoingHttpHeaders ( response . headers )
289
+
290
+ if ( cacheTags ) {
291
+ headers [ NEXT_CACHE_TAGS_HEADER ] = cacheTags
292
+ }
293
+
294
+ if ( ! headers [ 'content-type' ] && blob . type ) {
295
+ headers [ 'content-type' ] = blob . type
296
+ }
297
+
298
+ const revalidate =
299
+ typeof context . renderOpts . collectedRevalidate === 'undefined' ||
300
+ context . renderOpts . collectedRevalidate >= INFINITE_CACHE
301
+ ? false
302
+ : context . renderOpts . collectedRevalidate
303
+
304
+ const expire =
305
+ typeof context . renderOpts . collectedExpire === 'undefined' ||
306
+ context . renderOpts . collectedExpire >= INFINITE_CACHE
307
+ ? undefined
308
+ : context . renderOpts . collectedExpire
309
+
310
+ // Create the cache entry for the response.
311
+ const cacheEntry : ResponseCacheEntry = {
312
+ value : {
313
+ kind : CachedRouteKind . APP_ROUTE ,
314
+ status : response . status ,
315
+ body : Buffer . from ( await blob . arrayBuffer ( ) ) ,
316
+ headers,
317
+ } ,
318
+ cacheControl : { revalidate, expire } ,
319
+ }
320
+
321
+ return cacheEntry
322
+ } else {
323
+ // send response without caching if not ISR
324
+ await sendResponse (
325
+ nodeNextReq ,
326
+ nodeNextRes ,
327
+ response ,
328
+ context . renderOpts . pendingWaitUntil
329
+ )
330
+ return null
331
+ }
332
+ } catch ( err ) {
333
+ // if this is a background revalidate we need to report
334
+ // the request error here as it won't be bubbled
335
+ if ( previousCacheEntry ?. isStale ) {
336
+ await routeModule . onRequestError (
337
+ req ,
338
+ err ,
339
+ {
340
+ routerKind : 'App Router' ,
341
+ routePath : srcPage ,
342
+ routeType : 'route' ,
343
+ revalidateReason : getRevalidateReason ( {
344
+ isRevalidate,
345
+ isOnDemandRevalidate,
346
+ } ) ,
347
+ } ,
348
+ routerServerContext
349
+ )
350
+ }
351
+ throw err
352
+ }
353
+ }
354
+
355
+ const cacheEntry = await routeModule . handleResponse ( {
356
+ req,
357
+ nextConfig,
358
+ cacheKey,
359
+ routeKind : RouteKind . APP_ROUTE ,
360
+ isFallback : false ,
361
+ prerenderManifest,
362
+ isRoutePPREnabled : false ,
363
+ isOnDemandRevalidate,
364
+ revalidateOnlyGenerated,
365
+ responseGenerator,
366
+ waitUntil : ctx . waitUntil ,
367
+ } )
368
+
369
+ // we don't create a cacheEntry for ISR
370
+ if ( ! isIsr ) {
371
+ return null
372
+ }
373
+
374
+ if ( cacheEntry ?. value ?. kind !== CachedRouteKind . APP_ROUTE ) {
375
+ throw new Error (
376
+ `Invariant: app-route received invalid cache entry ${ cacheEntry ?. value ?. kind } `
377
+ )
378
+ }
379
+
380
+ if ( ! getRequestMeta ( req , 'minimalMode' ) ) {
381
+ res . setHeader (
382
+ 'x-nextjs-cache' ,
383
+ isOnDemandRevalidate
384
+ ? 'REVALIDATED'
385
+ : cacheEntry . isMiss
386
+ ? 'MISS'
387
+ : cacheEntry . isStale
388
+ ? 'STALE'
389
+ : 'HIT'
390
+ )
391
+ }
392
+
393
+ const headers = fromNodeOutgoingHttpHeaders ( cacheEntry . value . headers )
394
+
395
+ if ( ! ( getRequestMeta ( req , 'minimalMode' ) && isIsr ) ) {
396
+ headers . delete ( NEXT_CACHE_TAGS_HEADER )
397
+ }
398
+
399
+ // If cache control is already set on the response we don't
400
+ // override it to allow users to customize it via next.config
401
+ if (
402
+ cacheEntry . cacheControl &&
403
+ ! res . getHeader ( 'Cache-Control' ) &&
404
+ ! headers . get ( 'Cache-Control' )
405
+ ) {
406
+ headers . set (
407
+ 'Cache-Control' ,
408
+ getCacheControlHeader ( cacheEntry . cacheControl )
409
+ )
410
+ }
411
+
412
+ await sendResponse (
413
+ nodeNextReq ,
414
+ nodeNextRes ,
415
+ new Response ( cacheEntry . value . body , {
416
+ headers,
417
+ status : cacheEntry . value . status || 200 ,
418
+ } )
419
+ )
420
+ return null
421
+ }
242
422
243
423
// TODO: activeSpan code path is for when wrapped by
244
424
// next-server can be removed when this is no longer used
245
425
if ( activeSpan ) {
246
- response = await invokeRouteModule ( activeSpan )
426
+ await handleResponse ( activeSpan )
247
427
} else {
248
- response = await tracer . withPropagatedContext ( req . headers , ( ) =>
428
+ await tracer . withPropagatedContext ( req . headers , ( ) =>
249
429
tracer . trace (
250
430
BaseServerSpan . handleRequest ,
251
431
{
@@ -256,79 +436,14 @@ export async function handler(
256
436
'http.target' : req . url ,
257
437
} ,
258
438
} ,
259
- invokeRouteModule
439
+ handleResponse
260
440
)
261
441
)
262
442
}
263
-
264
- ; ( req as any ) . fetchMetrics = ( context . renderOpts as any ) . fetchMetrics
265
-
266
- const cacheTags = context . renderOpts . collectedTags
267
-
268
- // If the request is for a static response, we can cache it so long
269
- // as it's not edge.
270
- if ( isIsr ) {
271
- const blob = await response . blob ( )
272
-
273
- // Copy the headers from the response.
274
- const headers = toNodeOutgoingHttpHeaders ( response . headers )
275
-
276
- if ( cacheTags ) {
277
- headers [ NEXT_CACHE_TAGS_HEADER ] = cacheTags
278
- }
279
-
280
- if ( ! headers [ 'content-type' ] && blob . type ) {
281
- headers [ 'content-type' ] = blob . type
282
- }
283
-
284
- const revalidate =
285
- typeof context . renderOpts . collectedRevalidate === 'undefined' ||
286
- context . renderOpts . collectedRevalidate >= INFINITE_CACHE
287
- ? false
288
- : context . renderOpts . collectedRevalidate
289
-
290
- const expire =
291
- typeof context . renderOpts . collectedExpire === 'undefined' ||
292
- context . renderOpts . collectedExpire >= INFINITE_CACHE
293
- ? undefined
294
- : context . renderOpts . collectedExpire
295
-
296
- // Create the cache entry for the response.
297
- const cacheEntry : ResponseCacheEntry = {
298
- value : {
299
- kind : CachedRouteKind . APP_ROUTE ,
300
- status : response . status ,
301
- body : Buffer . from ( await blob . arrayBuffer ( ) ) ,
302
- headers,
303
- } ,
304
- cacheControl : { revalidate, expire } ,
305
- }
306
-
307
- return cacheEntry
308
- }
309
- let pendingWaitUntil = context . renderOpts . pendingWaitUntil
310
-
311
- // Attempt using provided waitUntil if available
312
- // if it's not we fallback to sendResponse's handling
313
- if ( pendingWaitUntil ) {
314
- if ( context . renderOpts . waitUntil ) {
315
- context . renderOpts . waitUntil ( pendingWaitUntil )
316
- pendingWaitUntil = undefined
317
- }
318
- }
319
-
320
- // Send the response now that we have copied it into the cache.
321
- await sendResponse (
322
- nodeNextReq ,
323
- nodeNextRes ,
324
- response ,
325
- context . renderOpts . pendingWaitUntil
326
- )
327
- return null
328
443
} catch ( err ) {
329
444
// if we aren't wrapped by base-server handle here
330
445
if ( ! activeSpan ) {
331
- await onError ( err , req , {
446
+ await routeModule . onRequestError ( req , err , {
332
447
routerKind : 'App Router' ,
333
448
routePath : normalizedSrcPage ,
334
449
routeType : 'route' ,
0 commit comments