import { RouteModule } from "../route-module"; import { RequestAsyncStorageWrapper } from "../../../async-storage/request-async-storage-wrapper"; import { StaticGenerationAsyncStorageWrapper } from "../../../async-storage/static-generation-async-storage-wrapper"; import { handleBadRequestResponse, handleInternalServerErrorResponse } from "../helpers/response-handlers"; import { HTTP_METHODS, isHTTPMethod } from "../../../web/http"; import { addImplicitTags, patchFetch } from "../../../lib/patch-fetch"; import { getTracer } from "../../../lib/trace/tracer"; import { AppRouteRouteHandlersSpan } from "../../../lib/trace/constants"; import { getPathnameFromAbsolutePath } from "./helpers/get-pathname-from-absolute-path"; import { proxyRequest } from "./helpers/proxy-request"; import { resolveHandlerError } from "./helpers/resolve-handler-error"; import * as Log from "../../../../build/output/log"; import { autoImplementMethods } from "./helpers/auto-implement-methods"; import { getNonStaticMethods } from "./helpers/get-non-static-methods"; import { appendMutableCookies } from "../../../web/spec-extension/adapters/request-cookies"; import { RouteKind } from "../../route-kind"; import { parsedUrlQueryToParams } from "./helpers/parsed-url-query-to-params"; import * as serverHooks from "../../../../client/components/hooks-server-context"; import * as headerHooks from "../../../../client/components/headers"; import { staticGenerationBailout } from "../../../../client/components/static-generation-bailout"; import { requestAsyncStorage } from "../../../../client/components/request-async-storage.external"; import { staticGenerationAsyncStorage } from "../../../../client/components/static-generation-async-storage.external"; import { actionAsyncStorage } from "../../../../client/components/action-async-storage.external"; import * as sharedModules from "./shared-modules"; /** * AppRouteRouteHandler is the handler for app routes. */ export class AppRouteRouteModule extends RouteModule { static #_ = this.sharedModules = sharedModules; static is(route) { return route.definition.kind === RouteKind.APP_ROUTE; } constructor({ userland, definition, resolvedPagePath, nextConfigOutput }){ super({ userland, definition }); /** * A reference to the request async storage. */ this.requestAsyncStorage = requestAsyncStorage; /** * A reference to the static generation async storage. */ this.staticGenerationAsyncStorage = staticGenerationAsyncStorage; /** * An interface to call server hooks which interact with the underlying * storage. */ this.serverHooks = serverHooks; /** * An interface to call header hooks which interact with the underlying * request storage. */ this.headerHooks = headerHooks; /** * An interface to call static generation bailout hooks which interact with * the underlying static generation storage. */ this.staticGenerationBailout = staticGenerationBailout; /** * A reference to the mutation related async storage, such as mutations of * cookies. */ this.actionAsyncStorage = actionAsyncStorage; this.resolvedPagePath = resolvedPagePath; this.nextConfigOutput = nextConfigOutput; // Automatically implement some methods if they aren't implemented by the // userland module. this.methods = autoImplementMethods(userland); // Get the non-static methods for this route. this.nonStaticMethods = getNonStaticMethods(userland); // Get the dynamic property from the userland module. this.dynamic = this.userland.dynamic; if (this.nextConfigOutput === "export") { if (!this.dynamic || this.dynamic === "auto") { this.dynamic = "error"; } else if (this.dynamic === "force-dynamic") { throw new Error(`export const dynamic = "force-dynamic" on page "${definition.pathname}" cannot be used with "output: export". See more info here: https://nextjs.org/docs/advanced-features/static-html-export`); } } // We only warn in development after here, so return if we're not in // development. if (process.env.NODE_ENV === "development") { // Print error in development if the exported handlers are in lowercase, only // uppercase handlers are supported. const lowercased = HTTP_METHODS.map((method)=>method.toLowerCase()); for (const method of lowercased){ if (method in this.userland) { Log.error(`Detected lowercase method '${method}' in '${this.resolvedPagePath}'. Export the uppercase '${method.toUpperCase()}' method name to fix this error.`); } } // Print error if the module exports a default handler, they must use named // exports for each HTTP method. if ("default" in this.userland) { Log.error(`Detected default export in '${this.resolvedPagePath}'. Export a named export for each HTTP method instead.`); } // If there is no methods exported by this module, then return a not found // response. if (!HTTP_METHODS.some((method)=>method in this.userland)) { Log.error(`No HTTP methods exported in '${this.resolvedPagePath}'. Export a named export for each HTTP method.`); } } } /** * Resolves the handler function for the given method. * * @param method the requested method * @returns the handler function for the given method */ resolve(method) { // Ensure that the requested method is a valid method (to prevent RCE's). if (!isHTTPMethod(method)) return handleBadRequestResponse; // Return the handler. return this.methods[method]; } /** * Executes the route handler. */ async execute(request, context) { // Get the handler function for the given method. const handler = this.resolve(request.method); // Get the context for the request. const requestContext = { req: request }; requestContext.renderOpts = { previewProps: context.prerenderManifest.preview }; // Get the context for the static generation. const staticGenerationContext = { urlPathname: request.nextUrl.pathname, renderOpts: // If the staticGenerationContext is not provided then we default to // the default values. context.staticGenerationContext ?? { supportsDynamicHTML: false, originalPathname: this.definition.pathname } }; // Add the fetchCache option to the renderOpts. staticGenerationContext.renderOpts.fetchCache = this.userland.fetchCache; // Run the handler with the request AsyncLocalStorage to inject the helper // support. We set this to `unknown` because the type is not known until // runtime when we do a instanceof check below. const response = await this.actionAsyncStorage.run({ isAppRoute: true }, ()=>RequestAsyncStorageWrapper.wrap(this.requestAsyncStorage, requestContext, ()=>StaticGenerationAsyncStorageWrapper.wrap(this.staticGenerationAsyncStorage, staticGenerationContext, (staticGenerationStore)=>{ var _getTracer_getRootSpanAttributes; // Check to see if we should bail out of static generation based on // having non-static methods. if (this.nonStaticMethods) { this.staticGenerationBailout(`non-static methods used ${this.nonStaticMethods.join(", ")}`); } // Update the static generation store based on the dynamic property. switch(this.dynamic){ case "force-dynamic": // The dynamic property is set to force-dynamic, so we should // force the page to be dynamic. staticGenerationStore.forceDynamic = true; this.staticGenerationBailout(`force-dynamic`, { dynamic: this.dynamic }); break; case "force-static": // The dynamic property is set to force-static, so we should // force the page to be static. staticGenerationStore.forceStatic = true; break; case "error": // The dynamic property is set to error, so we should throw an // error if the page is being statically generated. staticGenerationStore.dynamicShouldError = true; break; default: break; } // If the static generation store does not have a revalidate value // set, then we should set it the revalidate value from the userland // module or default to false. staticGenerationStore.revalidate ??= this.userland.revalidate ?? false; // Wrap the request so we can add additional functionality to cases // that might change it's output or affect the rendering. const wrappedRequest = proxyRequest(request, { dynamic: this.dynamic }, { headerHooks: this.headerHooks, serverHooks: this.serverHooks, staticGenerationBailout: this.staticGenerationBailout }); // TODO: propagate this pathname from route matcher const route = getPathnameFromAbsolutePath(this.resolvedPagePath); (_getTracer_getRootSpanAttributes = getTracer().getRootSpanAttributes()) == null ? void 0 : _getTracer_getRootSpanAttributes.set("next.route", route); return getTracer().trace(AppRouteRouteHandlersSpan.runHandler, { spanName: `executing api route (app) ${route}`, attributes: { "next.route": route } }, async ()=>{ var _staticGenerationStore_tags; // Patch the global fetch. patchFetch({ serverHooks: this.serverHooks, staticGenerationAsyncStorage: this.staticGenerationAsyncStorage }); const res = await handler(wrappedRequest, { params: context.params ? parsedUrlQueryToParams(context.params) : undefined }); if (!(res instanceof Response)) { throw new Error(`No response is returned from route handler '${this.resolvedPagePath}'. Ensure you return a \`Response\` or a \`NextResponse\` in all branches of your handler.`); } context.staticGenerationContext.fetchMetrics = staticGenerationStore.fetchMetrics; await Promise.all(staticGenerationStore.pendingRevalidates || []); addImplicitTags(staticGenerationStore); context.staticGenerationContext.fetchTags = (_staticGenerationStore_tags = staticGenerationStore.tags) == null ? void 0 : _staticGenerationStore_tags.join(","); // It's possible cookies were set in the handler, so we need // to merge the modified cookies and the returned response // here. const requestStore = this.requestAsyncStorage.getStore(); if (requestStore && requestStore.mutableCookies) { const headers = new Headers(res.headers); if (appendMutableCookies(headers, requestStore.mutableCookies)) { return new Response(res.body, { status: res.status, statusText: res.statusText, headers }); } } return res; }); }))); // If the handler did't return a valid response, then return the internal // error response. if (!(response instanceof Response)) { // TODO: validate the correct handling behavior, maybe log something? return handleInternalServerErrorResponse(); } if (response.headers.has("x-middleware-rewrite")) { // TODO: move this error into the `NextResponse.rewrite()` function. // TODO-APP: re-enable support below when we can proxy these type of requests throw new Error("NextResponse.rewrite() was used in a app route handler, this is not currently supported. Please remove the invocation to continue."); // // This is a rewrite created via `NextResponse.rewrite()`. We need to send // // the response up so it can be handled by the backing server. // // If the server is running in minimal mode, we just want to forward the // // response (including the rewrite headers) upstream so it can perform the // // redirect for us, otherwise return with the special condition so this // // server can perform a rewrite. // if (!minimalMode) { // return { response, condition: 'rewrite' } // } // // Relativize the url so it's relative to the base url. This is so the // // outgoing headers upstream can be relative. // const rewritePath = response.headers.get('x-middleware-rewrite')! // const initUrl = getRequestMeta(req, '__NEXT_INIT_URL')! // const { pathname } = parseUrl(relativizeURL(rewritePath, initUrl)) // response.headers.set('x-middleware-rewrite', pathname) } if (response.headers.get("x-middleware-next") === "1") { // TODO: move this error into the `NextResponse.next()` function. throw new Error("NextResponse.next() was used in a app route handler, this is not supported. See here for more info: https://nextjs.org/docs/messages/next-response-next-in-app-route-handler"); } return response; } async handle(request, context) { try { // Execute the route to get the response. const response = await this.execute(request, context); // The response was handled, return it. return response; } catch (err) { // Try to resolve the error to a response, else throw it again. const response = resolveHandlerError(err); if (!response) throw err; // The response was resolved, return it. return response; } } } export default AppRouteRouteModule; //# sourceMappingURL=module.js.map