diff --git a/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/render.ts b/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/render.ts index 40d377c9040fb0..d3f41256906127 100644 --- a/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/render.ts +++ b/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/render.ts @@ -21,7 +21,11 @@ import { normalizeAppPath } from '../../../../shared/lib/router/utils/app-paths' import type { SizeLimit } from '../../../../types' import { internal_getCurrentFunctionWaitUntil } from '../../../../server/web/internal-edge-wait-until' import type { PAGE_TYPES } from '../../../../lib/page-types' -import type { NextRequestHint } from '../../../../server/web/adapter' +import type { + HandlerExtraOpts, + NextRequestHint, +} from '../../../../server/web/adapter' +import { InvariantError } from '../../../../shared/lib/invariant-error' export function getRender({ dev, @@ -157,11 +161,31 @@ export function getRender({ return async function render( request: NextRequestHint, - event?: NextFetchEvent + event?: NextFetchEvent, + extraOpts?: HandlerExtraOpts ) { const extendedReq = new WebNextRequest(request) const extendedRes = new WebNextResponse(undefined) + if (dev) { + if (extraOpts) { + const { onAfterTaskError } = extraOpts + // in practice `onAfterTaskError` should be constant, so it's fine to set it like this + // but double check that it's not changing across request to prevent future bugs + if ( + server['afterTaskErrorHandler'] && + onAfterTaskError && + server['afterTaskErrorHandler'] !== onAfterTaskError + ) { + throw new InvariantError( + 'Expected `extraOpts.afterTaskErrorHandler` to not change' + ) + } + + server['afterTaskErrorHandler'] = onAfterTaskError + } + } + handler(extendedReq, extendedRes) const result = await extendedRes.toResponse() request.fetchMetrics = extendedReq.fetchMetrics diff --git a/packages/next/src/server/next-server.ts b/packages/next/src/server/next-server.ts index a56dc40a77d81c..ba224bf47a4235 100644 --- a/packages/next/src/server/next-server.ts +++ b/packages/next/src/server/next-server.ts @@ -1494,6 +1494,7 @@ export default class NextNodeServer extends BaseServer< body: getRequestMeta(params.request, 'clonableBody'), signal: signalFromNodeResponse(params.response.originalResponse), waitUntil: this.getWaitUntil(), + onAfterTaskError: this.afterTaskErrorHandler, }, useCache: true, onWarning: params.onWarning, @@ -1798,6 +1799,7 @@ export default class NextNodeServer extends BaseServer< body: getRequestMeta(params.req, 'clonableBody'), signal: signalFromNodeResponse(params.res.originalResponse), waitUntil: this.getWaitUntil(), + onAfterTaskError: this.afterTaskErrorHandler, }, useCache: true, onError: params.onError, diff --git a/packages/next/src/server/web/adapter.ts b/packages/next/src/server/web/adapter.ts index 1e589d4bd91ae2..5455780dd11c45 100644 --- a/packages/next/src/server/web/adapter.ts +++ b/packages/next/src/server/web/adapter.ts @@ -57,8 +57,13 @@ const headersGetter: TextMapGetter = { get: (headers, key) => headers.get(key) ?? undefined, } +export type HandlerExtraOpts = { onAfterTaskError?: (error: unknown) => void } export type AdapterOptions = { - handler: (req: NextRequestHint, event: NextFetchEvent) => Promise + handler: ( + req: NextRequestHint, + event: NextFetchEvent, + extraOpts?: HandlerExtraOpts + ) => Promise page: string request: RequestData IncrementalCache?: typeof import('../lib/incremental-cache').IncrementalCache @@ -212,6 +217,8 @@ export async function adapter( let response let cookiesFromResponse + const onAfterTaskError = params.request.onAfterTaskError + response = await propagator(request, () => { // we only care to make async storage available for middleware const isMiddleware = @@ -270,7 +277,7 @@ export async function adapter( supportsDynamicResponse: true, waitUntil, onClose: closeController.onClose.bind(closeController), - onAfterTaskError: undefined, + onAfterTaskError, }, requestEndedState: { ended: false }, isPrefetchRequest: request.headers.has( @@ -283,7 +290,8 @@ export async function adapter( requestStore, params.handler, request, - event + event, + { onAfterTaskError } ) ) } finally { @@ -298,7 +306,7 @@ export async function adapter( } ) } - return params.handler(request, event) + return params.handler(request, event, { onAfterTaskError }) }) // check if response is a Response object diff --git a/packages/next/src/server/web/edge-route-module-wrapper.ts b/packages/next/src/server/web/edge-route-module-wrapper.ts index 59e6879d1c6a54..f96aed90a14133 100644 --- a/packages/next/src/server/web/edge-route-module-wrapper.ts +++ b/packages/next/src/server/web/edge-route-module-wrapper.ts @@ -6,7 +6,7 @@ import type { import './globals' -import { adapter, type AdapterOptions } from './adapter' +import { adapter, type AdapterOptions, type HandlerExtraOpts } from './adapter' import { IncrementalCache } from '../lib/incremental-cache' import { RouteMatcher } from '../route-matchers/route-matcher' import type { NextFetchEvent } from './spec-extension/fetch-event' @@ -57,7 +57,7 @@ export class EdgeRouteModuleWrapper { const wrapper = new EdgeRouteModuleWrapper(routeModule, options.nextConfig) // Return the wrapping function. - return (opts: AdapterOptions) => { + return (opts: Omit) => { return adapter({ ...opts, IncrementalCache, @@ -69,7 +69,8 @@ export class EdgeRouteModuleWrapper { private async handler( request: NextRequest, - evt: NextFetchEvent + evt: NextFetchEvent, + extraOpts?: HandlerExtraOpts ): Promise { const utils = getUtils({ pageIsDynamic: this.matcher.isDynamic, @@ -107,7 +108,7 @@ export class EdgeRouteModuleWrapper { supportsDynamicResponse: true, waitUntil, onClose: closeController.onClose.bind(closeController), - onAfterTaskError: undefined, + onAfterTaskError: extraOpts?.onAfterTaskError, experimental: { after: isAfterEnabled, dynamicIO: !!process.env.__NEXT_DYNAMIC_IO, diff --git a/packages/next/src/server/web/types.ts b/packages/next/src/server/web/types.ts index 1bc4eca695d2f8..3cfb51792c04f2 100644 --- a/packages/next/src/server/web/types.ts +++ b/packages/next/src/server/web/types.ts @@ -29,6 +29,8 @@ export interface RequestData { signal: AbortSignal /** passed in when running in edge runtime sandbox */ waitUntil?: (promise: Promise) => void + /** passed in when running in edge runtime sandbox */ + onAfterTaskError?: (error: unknown) => void } export type NodejsRequestData = Omit & {