|
1 | 1 | import { |
2 | 2 | AppRouteRouteModule, |
| 3 | + type AppRouteRouteHandlerContext, |
3 | 4 | type AppRouteRouteModuleOptions, |
4 | 5 | } from '../../server/route-modules/app-route/module.compiled' |
5 | 6 | import { RouteKind } from '../../server/route-kind' |
6 | 7 | import { patchFetch as _patchFetch } from '../../server/lib/patch-fetch' |
7 | 8 |
|
8 | 9 | import * as userland from 'VAR_USERLAND' |
| 10 | +import { |
| 11 | + RouterServerContextSymbol, |
| 12 | + routerServerGlobal, |
| 13 | +} from '../../server/lib/router-utils/router-server-context' |
| 14 | +import type { IncomingMessage, ServerResponse } from 'node:http' |
| 15 | +import { getRequestMeta } from '../../server/request-meta' |
| 16 | +import { getTracer, type Span, SpanKind } from '../../server/lib/trace/tracer' |
| 17 | +import type { ServerOnInstrumentationRequestError } from '../../server/app-render/types' |
| 18 | +import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths' |
| 19 | +import { NodeNextRequest, NodeNextResponse } from '../../server/base-http/node' |
| 20 | +import { |
| 21 | + NextRequestAdapter, |
| 22 | + signalFromNodeResponse, |
| 23 | +} from '../../server/web/spec-extension/adapters/next-request' |
| 24 | +import { BaseServerSpan } from '../../server/lib/trace/constants' |
| 25 | +import { getRevalidateReason } from '../../server/instrumentation/utils' |
| 26 | +import { sendResponse } from '../../server/send-response' |
| 27 | +import { toNodeOutgoingHttpHeaders } from '../../server/web/utils' |
| 28 | +import { INFINITE_CACHE, NEXT_CACHE_TAGS_HEADER } from '../../lib/constants' |
| 29 | +import { |
| 30 | + CachedRouteKind, |
| 31 | + type ResponseCacheEntry, |
| 32 | +} from '../../server/response-cache' |
9 | 33 |
|
10 | 34 | // These are injected by the loader afterwards. This is injected as a variable |
11 | 35 | // instead of a replacement because this could also be `undefined` instead of |
@@ -50,3 +74,293 @@ export { |
50 | 74 | serverHooks, |
51 | 75 | patchFetch, |
52 | 76 | } |
| 77 | + |
| 78 | +export async function handler( |
| 79 | + req: IncomingMessage, |
| 80 | + res: ServerResponse, |
| 81 | + ctx: { |
| 82 | + waitUntil: (prom: Promise<void>) => void |
| 83 | + } |
| 84 | +) { |
| 85 | + let srcPage = 'VAR_DEFINITION_PAGE' |
| 86 | + |
| 87 | + // turbopack doesn't normalize `/index` in the page name |
| 88 | + // so we need to to process dynamic routes properly |
| 89 | + // TODO: fix turbopack providing differing value from webpack |
| 90 | + if (process.env.TURBOPACK) { |
| 91 | + srcPage = srcPage.replace(/\/index$/, '') || '/' |
| 92 | + } else if (srcPage === '/index') { |
| 93 | + // we always normalize /index specifically |
| 94 | + srcPage = '/' |
| 95 | + } |
| 96 | + const multiZoneDraftMode = process.env |
| 97 | + .__NEXT_MULTI_ZONE_DRAFT_MODE as any as boolean |
| 98 | + |
| 99 | + const prepareResult = await routeModule.prepare(req, res, { |
| 100 | + srcPage, |
| 101 | + multiZoneDraftMode, |
| 102 | + }) |
| 103 | + |
| 104 | + if (!prepareResult) { |
| 105 | + res.statusCode = 400 |
| 106 | + res.end('Bad Request') |
| 107 | + ctx.waitUntil?.(Promise.resolve()) |
| 108 | + return null |
| 109 | + } |
| 110 | + |
| 111 | + const { |
| 112 | + buildId, |
| 113 | + params, |
| 114 | + parsedUrl, |
| 115 | + serverFilesManifest, |
| 116 | + prerenderManifest, |
| 117 | + isOnDemandRevalidate, |
| 118 | + } = prepareResult |
| 119 | + |
| 120 | + const routerServerContext = |
| 121 | + routerServerGlobal[RouterServerContextSymbol]?.[ |
| 122 | + process.env.__NEXT_RELATIVE_PROJECT_DIR || '' |
| 123 | + ] |
| 124 | + |
| 125 | + const onInstrumentationRequestError = |
| 126 | + routeModule.instrumentationOnRequestError.bind(routeModule) |
| 127 | + |
| 128 | + const onError: ServerOnInstrumentationRequestError = ( |
| 129 | + err, |
| 130 | + _, |
| 131 | + errorContext |
| 132 | + ) => { |
| 133 | + if (routerServerContext?.logErrorWithOriginalStack) { |
| 134 | + routerServerContext.logErrorWithOriginalStack(err, 'app-dir') |
| 135 | + } else { |
| 136 | + console.error(err) |
| 137 | + } |
| 138 | + return onInstrumentationRequestError( |
| 139 | + req, |
| 140 | + err, |
| 141 | + { |
| 142 | + path: req.url || '/', |
| 143 | + headers: req.headers, |
| 144 | + method: req.method || 'GET', |
| 145 | + }, |
| 146 | + errorContext |
| 147 | + ) |
| 148 | + } |
| 149 | + |
| 150 | + const nextConfig = |
| 151 | + routerServerContext?.nextConfig || serverFilesManifest.config |
| 152 | + |
| 153 | + const pathname = parsedUrl.pathname || '/' |
| 154 | + const normalizedSrcPage = normalizeAppPath(srcPage) |
| 155 | + let isIsr = Boolean( |
| 156 | + prerenderManifest.dynamicRoutes[normalizedSrcPage] || |
| 157 | + prerenderManifest.routes[normalizedSrcPage] || |
| 158 | + prerenderManifest.routes[pathname] |
| 159 | + ) |
| 160 | + |
| 161 | + const supportsDynamicResponse: boolean = |
| 162 | + // If we're in development, we always support dynamic HTML |
| 163 | + routeModule.isDev === true || |
| 164 | + // If this is not SSG or does not have static paths, then it supports |
| 165 | + // dynamic HTML. |
| 166 | + !isIsr |
| 167 | + |
| 168 | + // This is a revalidation request if the request is for a static |
| 169 | + // page and it is not being resumed from a postponed render and |
| 170 | + // it is not a dynamic RSC request then it is a revalidation |
| 171 | + // request. |
| 172 | + const isRevalidate = isIsr && !supportsDynamicResponse |
| 173 | + |
| 174 | + const method = req.method || 'GET' |
| 175 | + const tracer = getTracer() |
| 176 | + const activeSpan = tracer.getActiveScopeSpan() |
| 177 | + |
| 178 | + const context: AppRouteRouteHandlerContext = { |
| 179 | + params, |
| 180 | + prerenderManifest, |
| 181 | + renderOpts: { |
| 182 | + experimental: { |
| 183 | + dynamicIO: Boolean(nextConfig.experimental.dynamicIO), |
| 184 | + authInterrupts: Boolean(nextConfig.experimental.authInterrupts), |
| 185 | + }, |
| 186 | + supportsDynamicResponse, |
| 187 | + incrementalCache: getRequestMeta(req, 'incrementalCache'), |
| 188 | + cacheLifeProfiles: nextConfig.experimental?.cacheLife, |
| 189 | + isRevalidate, |
| 190 | + waitUntil: ctx.waitUntil, |
| 191 | + onClose: (cb) => { |
| 192 | + res.on('close', cb) |
| 193 | + }, |
| 194 | + onAfterTaskError: undefined, |
| 195 | + onInstrumentationRequestError: onError, |
| 196 | + }, |
| 197 | + sharedContext: { |
| 198 | + buildId, |
| 199 | + }, |
| 200 | + } |
| 201 | + const nodeNextReq = new NodeNextRequest(req) |
| 202 | + const nodeNextRes = new NodeNextResponse(res) |
| 203 | + const nextReq = NextRequestAdapter.fromNodeNextRequest( |
| 204 | + nodeNextReq, |
| 205 | + signalFromNodeResponse(res) |
| 206 | + ) |
| 207 | + |
| 208 | + try { |
| 209 | + const invokeRouteModule = async (span?: Span) => { |
| 210 | + return routeModule.handle(nextReq, context).finally(() => { |
| 211 | + if (!span) return |
| 212 | + |
| 213 | + span.setAttributes({ |
| 214 | + 'http.status_code': res.statusCode, |
| 215 | + 'next.rsc': false, |
| 216 | + }) |
| 217 | + |
| 218 | + const rootSpanAttributes = tracer.getRootSpanAttributes() |
| 219 | + // We were unable to get attributes, probably OTEL is not enabled |
| 220 | + if (!rootSpanAttributes) { |
| 221 | + return |
| 222 | + } |
| 223 | + |
| 224 | + if ( |
| 225 | + rootSpanAttributes.get('next.span_type') !== |
| 226 | + BaseServerSpan.handleRequest |
| 227 | + ) { |
| 228 | + console.warn( |
| 229 | + `Unexpected root span type '${rootSpanAttributes.get( |
| 230 | + 'next.span_type' |
| 231 | + )}'. Please report this Next.js issue https://github.com/vercel/next.js` |
| 232 | + ) |
| 233 | + return |
| 234 | + } |
| 235 | + |
| 236 | + const route = rootSpanAttributes.get('next.route') |
| 237 | + if (route) { |
| 238 | + const name = `${method} ${route}` |
| 239 | + |
| 240 | + span.setAttributes({ |
| 241 | + 'next.route': route, |
| 242 | + 'http.route': route, |
| 243 | + 'next.span_name': name, |
| 244 | + }) |
| 245 | + span.updateName(name) |
| 246 | + } else { |
| 247 | + span.updateName(`${method} ${req.url}`) |
| 248 | + } |
| 249 | + }) |
| 250 | + } |
| 251 | + |
| 252 | + let response: Response |
| 253 | + |
| 254 | + // TODO: activeSpan code path is for when wrapped by |
| 255 | + // next-server can be removed when this is no longer used |
| 256 | + if (activeSpan) { |
| 257 | + response = await invokeRouteModule(activeSpan) |
| 258 | + } else { |
| 259 | + response = await tracer.withPropagatedContext(req.headers, () => |
| 260 | + tracer.trace( |
| 261 | + BaseServerSpan.handleRequest, |
| 262 | + { |
| 263 | + spanName: `${method} ${req.url}`, |
| 264 | + kind: SpanKind.SERVER, |
| 265 | + attributes: { |
| 266 | + 'http.method': method, |
| 267 | + 'http.target': req.url, |
| 268 | + }, |
| 269 | + }, |
| 270 | + invokeRouteModule |
| 271 | + ) |
| 272 | + ) |
| 273 | + } |
| 274 | + |
| 275 | + ;(req as any).fetchMetrics = (context.renderOpts as any).fetchMetrics |
| 276 | + |
| 277 | + const cacheTags = context.renderOpts.collectedTags |
| 278 | + |
| 279 | + // If the request is for a static response, we can cache it so long |
| 280 | + // as it's not edge. |
| 281 | + if (isIsr) { |
| 282 | + const blob = await response.blob() |
| 283 | + |
| 284 | + // Copy the headers from the response. |
| 285 | + const headers = toNodeOutgoingHttpHeaders(response.headers) |
| 286 | + |
| 287 | + if (cacheTags) { |
| 288 | + headers[NEXT_CACHE_TAGS_HEADER] = cacheTags |
| 289 | + } |
| 290 | + |
| 291 | + if (!headers['content-type'] && blob.type) { |
| 292 | + headers['content-type'] = blob.type |
| 293 | + } |
| 294 | + |
| 295 | + const revalidate = |
| 296 | + typeof context.renderOpts.collectedRevalidate === 'undefined' || |
| 297 | + context.renderOpts.collectedRevalidate >= INFINITE_CACHE |
| 298 | + ? false |
| 299 | + : context.renderOpts.collectedRevalidate |
| 300 | + |
| 301 | + const expire = |
| 302 | + typeof context.renderOpts.collectedExpire === 'undefined' || |
| 303 | + context.renderOpts.collectedExpire >= INFINITE_CACHE |
| 304 | + ? undefined |
| 305 | + : context.renderOpts.collectedExpire |
| 306 | + |
| 307 | + // Create the cache entry for the response. |
| 308 | + const cacheEntry: ResponseCacheEntry = { |
| 309 | + value: { |
| 310 | + kind: CachedRouteKind.APP_ROUTE, |
| 311 | + status: response.status, |
| 312 | + body: Buffer.from(await blob.arrayBuffer()), |
| 313 | + headers, |
| 314 | + }, |
| 315 | + cacheControl: { revalidate, expire }, |
| 316 | + } |
| 317 | + |
| 318 | + return cacheEntry |
| 319 | + } |
| 320 | + let pendingWaitUntil = context.renderOpts.pendingWaitUntil |
| 321 | + |
| 322 | + // Attempt using provided waitUntil if available |
| 323 | + // if it's not we fallback to sendResponse's handling |
| 324 | + if (pendingWaitUntil) { |
| 325 | + if (context.renderOpts.waitUntil) { |
| 326 | + context.renderOpts.waitUntil(pendingWaitUntil) |
| 327 | + pendingWaitUntil = undefined |
| 328 | + } |
| 329 | + } |
| 330 | + |
| 331 | + // Send the response now that we have copied it into the cache. |
| 332 | + await sendResponse( |
| 333 | + nodeNextReq, |
| 334 | + nodeNextRes, |
| 335 | + response, |
| 336 | + context.renderOpts.pendingWaitUntil |
| 337 | + ) |
| 338 | + return null |
| 339 | + } catch (err) { |
| 340 | + // if we aren't wrapped by base-server handle here |
| 341 | + if (!activeSpan) { |
| 342 | + await onError(err, req, { |
| 343 | + routerKind: 'App Router', |
| 344 | + routePath: normalizedSrcPage, |
| 345 | + routeType: 'route', |
| 346 | + revalidateReason: getRevalidateReason({ |
| 347 | + isRevalidate, |
| 348 | + isOnDemandRevalidate, |
| 349 | + }), |
| 350 | + }) |
| 351 | + } |
| 352 | + |
| 353 | + // rethrow so that we can handle serving error page |
| 354 | + |
| 355 | + // If this is during static generation, throw the error again. |
| 356 | + if (isIsr) throw err |
| 357 | + |
| 358 | + // Otherwise, send a 500 response. |
| 359 | + await sendResponse( |
| 360 | + nodeNextReq, |
| 361 | + nodeNextRes, |
| 362 | + new Response(null, { status: 500 }) |
| 363 | + ) |
| 364 | + return null |
| 365 | + } |
| 366 | +} |
0 commit comments