From bdacae003179786b3c487c0f3e49b86a48bb8f88 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Tue, 15 Sep 2026 08:30:46 +0000 Subject: [PATCH 1/6] feat(presets): apply server entry middleware and plugins on all presets Uses the srvx generic adapter for presets that do not start a srvx server. --- docs/1.docs/6.server-entry.md | 2 + src/presets/_nitro/runtime/service-worker.ts | 5 +- .../aws-amplify/runtime/aws-amplify.ts | 7 +-- .../runtime/aws-lambda-streaming.ts | 5 +- src/presets/aws-lambda/runtime/aws-lambda.ts | 5 +- src/presets/azure/runtime/azure-swa.ts | 5 +- .../cloudflare/runtime/_module-handler.ts | 5 +- .../cloudflare/runtime/cloudflare-durable.ts | 5 +- .../cloudflare/runtime/cloudflare-pages.ts | 5 +- src/presets/deno/runtime/deno-deploy.ts | 5 +- src/presets/edgeone/runtime/edgeone.ts | 5 +- src/presets/netlify/runtime/netlify-edge.ts | 5 +- src/presets/netlify/runtime/netlify.ts | 5 +- src/presets/node/runtime/node-middleware.ts | 5 +- src/presets/standard/runtime/server.ts | 5 +- src/presets/stormkit/runtime/stormkit.ts | 5 +- src/presets/vercel/runtime/vercel.node.ts | 5 +- src/presets/vercel/runtime/vercel.web.ts | 5 +- src/presets/winterjs/runtime/winterjs.ts | 5 +- src/presets/zeabur/runtime/zeabur.ts | 3 +- src/runtime/internal/serve.ts | 48 ++++++++++++++++++- test/fixture/server.ts | 1 + test/tests.ts | 10 ++-- 23 files changed, 111 insertions(+), 45 deletions(-) diff --git a/docs/1.docs/6.server-entry.md b/docs/1.docs/6.server-entry.md index 3649727c89..63a9d582e6 100644 --- a/docs/1.docs/6.server-entry.md +++ b/docs/1.docs/6.server-entry.md @@ -163,6 +163,8 @@ export default defineServerEntry({ ::read-more{to="https://srvx.h3.dev/guide/options" title="srvx server options"} :: +Other presets (serverless, edge and worker runtimes) do not start a server. They apply `middleware`, `plugins` and `error` to the request handler using the srvx generic adapter, so these behave the same everywhere. Listener and runtime specific options (such as `port`, `tls`, `maxRequestBodySize`, `trustProxy`, `gracefulShutdown`, `node`, `bun` and `deno`) have no effect there. + ::note - `NITRO_PORT`/`PORT`, `NITRO_HOST`/`HOST` and `NITRO_SSL_CERT`/`NITRO_SSL_KEY` environment variables take precedence over the `port`, `hostname` and `tls` options, so the server stays configurable at runtime. - During development (`nitro dev`), options are applied to the dev worker server, except listener options (`port`, `hostname`, `protocol`, `tls`, `silent`, `gracefulShutdown`) which are controlled by the dev server and CLI (`--port`, `--host`). The Vite dev server applies none of them. diff --git a/src/presets/_nitro/runtime/service-worker.ts b/src/presets/_nitro/runtime/service-worker.ts index dfe8138e54..18303fca53 100644 --- a/src/presets/_nitro/runtime/service-worker.ts +++ b/src/presets/_nitro/runtime/service-worker.ts @@ -1,9 +1,10 @@ import "#nitro/virtual/polyfills"; import { useNitroApp } from "nitro/app"; +import { withServerEntryOptions } from "#nitro/runtime/serve"; import { isPublicAssetURL } from "#nitro/virtual/public-assets"; import type { ServerRequest } from "srvx"; -const nitroApp = useNitroApp(); +const fetchHandler = withServerEntryOptions(useNitroApp().fetch); // @ts-expect-error addEventListener("fetch", (event: FetchEvent) => { @@ -18,7 +19,7 @@ addEventListener("fetch", (event: FetchEvent) => { req.runtime.serviceWorker ??= { event } as any; req.waitUntil = event.waitUntil.bind(event); - event.respondWith(nitroApp.fetch(req)); + event.respondWith(fetchHandler(req)); }); declare const self: ServiceWorkerGlobalScope; diff --git a/src/presets/aws-amplify/runtime/aws-amplify.ts b/src/presets/aws-amplify/runtime/aws-amplify.ts index 673fb81d68..88b5b016e0 100644 --- a/src/presets/aws-amplify/runtime/aws-amplify.ts +++ b/src/presets/aws-amplify/runtime/aws-amplify.ts @@ -1,13 +1,14 @@ import "#nitro/virtual/polyfills"; import { useNitroApp } from "nitro/app"; +import { withServerEntryOptions } from "#nitro/runtime/serve"; import { Server } from "node:http"; import type { NodeHttp1Handler } from "srvx"; import { toNodeHandler } from "srvx/node"; -const nitroApp = useNitroApp(); - -const server = new Server(toNodeHandler(nitroApp.fetch) as NodeHttp1Handler); +const server = new Server( + toNodeHandler(withServerEntryOptions(useNitroApp().fetch)) as NodeHttp1Handler +); // @ts-ignore server.listen(3000, (err) => { diff --git a/src/presets/aws-lambda/runtime/aws-lambda-streaming.ts b/src/presets/aws-lambda/runtime/aws-lambda-streaming.ts index 07034b7ad1..69db86a0db 100644 --- a/src/presets/aws-lambda/runtime/aws-lambda-streaming.ts +++ b/src/presets/aws-lambda/runtime/aws-lambda-streaming.ts @@ -1,9 +1,10 @@ import "#nitro/virtual/polyfills"; import { handleLambdaEventWithStream } from "srvx/aws-lambda"; import { useNitroApp } from "nitro/app"; +import { withServerEntryOptions } from "#nitro/runtime/serve"; -const nitroApp = useNitroApp(); +const fetchHandler = withServerEntryOptions(useNitroApp().fetch); export const handler = awslambda.streamifyResponse((event, responseStream, context) => - handleLambdaEventWithStream(nitroApp.fetch, event, responseStream, context) + handleLambdaEventWithStream(fetchHandler, event, responseStream, context) ); diff --git a/src/presets/aws-lambda/runtime/aws-lambda.ts b/src/presets/aws-lambda/runtime/aws-lambda.ts index a8220f8caf..e78fce2f62 100644 --- a/src/presets/aws-lambda/runtime/aws-lambda.ts +++ b/src/presets/aws-lambda/runtime/aws-lambda.ts @@ -1,12 +1,13 @@ import "#nitro/virtual/polyfills"; import { handleLambdaEvent } from "srvx/aws-lambda"; import { useNitroApp } from "nitro/app"; +import { withServerEntryOptions } from "#nitro/runtime/serve"; import type { AwsLambdaEvent } from "srvx/aws-lambda"; import type { Context } from "aws-lambda"; -const nitroApp = useNitroApp(); +const fetchHandler = withServerEntryOptions(useNitroApp().fetch); export async function handler(event: AwsLambdaEvent, context: Context) { - return handleLambdaEvent(nitroApp.fetch, event, context); + return handleLambdaEvent(fetchHandler, event, context); } diff --git a/src/presets/azure/runtime/azure-swa.ts b/src/presets/azure/runtime/azure-swa.ts index 9d4e178da1..75d562f210 100644 --- a/src/presets/azure/runtime/azure-swa.ts +++ b/src/presets/azure/runtime/azure-swa.ts @@ -1,11 +1,12 @@ import "#nitro/virtual/polyfills"; import { parseURL } from "ufo"; import { useNitroApp } from "nitro/app"; +import { withServerEntryOptions } from "#nitro/runtime/serve"; import { getAzureParsedCookiesFromHeaders, resolveBaseUrl } from "./_utils.ts"; import type { HttpRequest, HttpResponse, HttpResponseSimple } from "@azure/functions"; -const nitroApp = useNitroApp(); +const fetchHandler = withServerEntryOptions(useNitroApp().fetch); export async function handle(context: { res: HttpResponse }, req: HttpRequest) { let url: string; @@ -27,7 +28,7 @@ export async function handle(context: { res: HttpResponse }, req: HttpRequest) { body: req.bufferBody ?? req.rawBody, }); - const response = await nitroApp.fetch(request); + const response = await fetchHandler(request); // (v3 - current) https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-node?tabs=typescript%2Cwindows%2Cazure-cli&pivots=nodejs-model-v3#http-response // (v4) https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-node?tabs=typescript%2Cwindows%2Cazure-cli&pivots=nodejs-model-v4#http-response diff --git a/src/presets/cloudflare/runtime/_module-handler.ts b/src/presets/cloudflare/runtime/_module-handler.ts index 8d7eeefb07..8a28af5625 100644 --- a/src/presets/cloudflare/runtime/_module-handler.ts +++ b/src/presets/cloudflare/runtime/_module-handler.ts @@ -4,6 +4,7 @@ import type { ServerRequest, ServerRuntimeContext } from "srvx"; import { runCronTasks } from "#nitro/runtime/task"; import { useNitroApp, useNitroHooks } from "nitro/app"; +import { withServerEntryOptions } from "#nitro/runtime/serve"; type MaybePromise = T | Promise; @@ -16,7 +17,7 @@ export function createHandler(hooks: { ] ) => MaybePromise; }) { - const nitroApp = useNitroApp(); + const fetchHandler = withServerEntryOptions(useNitroApp().fetch); const nitroHooks = useNitroHooks(); return { @@ -35,7 +36,7 @@ export function createHandler(hooks: { } } - return (await nitroApp.fetch(request)) as any; + return (await fetchHandler(request as any)) as any; }, scheduled(controller, env, context) { diff --git a/src/presets/cloudflare/runtime/cloudflare-durable.ts b/src/presets/cloudflare/runtime/cloudflare-durable.ts index a2d8b50f09..1eb80390e9 100644 --- a/src/presets/cloudflare/runtime/cloudflare-durable.ts +++ b/src/presets/cloudflare/runtime/cloudflare-durable.ts @@ -7,6 +7,7 @@ import { createHandler, augmentReq } from "./_module-handler.ts"; import { useNitroApp, useNitroHooks } from "nitro/app"; import { isPublicAssetURL } from "#nitro/virtual/public-assets"; import { resolveWebsocketHooks } from "#nitro/runtime/app"; +import { withServerEntryOptions } from "#nitro/runtime/serve"; const DURABLE_BINDING = "$DurableObject"; const DURABLE_INSTANCE = "server"; @@ -16,7 +17,7 @@ interface Env { [DURABLE_BINDING]?: CF.DurableObjectNamespace; } -const nitroApp = useNitroApp(); +const fetchHandler = withServerEntryOptions(useNitroApp().fetch); const nitroHooks = useNitroHooks(); const getDurableStub = (env: Env) => { @@ -78,7 +79,7 @@ export class $DurableObject extends DurableObject { return ws!.handleDurableUpgrade(this, request); } - return nitroApp.fetch(request); + return fetchHandler(request); } override alarm(): void | Promise { diff --git a/src/presets/cloudflare/runtime/cloudflare-pages.ts b/src/presets/cloudflare/runtime/cloudflare-pages.ts index 695f63f117..6d066915d5 100644 --- a/src/presets/cloudflare/runtime/cloudflare-pages.ts +++ b/src/presets/cloudflare/runtime/cloudflare-pages.ts @@ -10,6 +10,7 @@ import { useNitroApp } from "nitro/app"; import { isPublicAssetURL } from "#nitro/virtual/public-assets"; import { runCronTasks } from "#nitro/runtime/task"; import { resolveWebsocketHooks } from "#nitro/runtime/app"; +import { withServerEntryOptions } from "#nitro/runtime/serve"; import { augmentReq } from "./_module-handler.ts"; @@ -26,7 +27,7 @@ interface CFPagesEnv { [key: string]: any; } -const nitroApp = useNitroApp(); +const fetchHandler = withServerEntryOptions(useNitroApp().fetch); const ws = import.meta._websocket ? wsAdapter({ resolve: resolveWebsocketHooks }) : undefined; @@ -48,7 +49,7 @@ export default { return env.ASSETS.fetch(cfReq); } - return nitroApp.fetch(cfReq as any); + return fetchHandler(cfReq as any); }, scheduled(event: any, env: CFPagesEnv, context: ExecutionContext) { if (import.meta._tasks) { diff --git a/src/presets/deno/runtime/deno-deploy.ts b/src/presets/deno/runtime/deno-deploy.ts index 5ab1bbdbc7..19e36be27c 100644 --- a/src/presets/deno/runtime/deno-deploy.ts +++ b/src/presets/deno/runtime/deno-deploy.ts @@ -5,12 +5,13 @@ import wsAdapter from "crossws/adapters/deno"; import { useNitroApp } from "nitro/app"; import { resolveWebsocketHooks } from "#nitro/runtime/app"; +import { withServerEntryOptions } from "#nitro/runtime/serve"; declare global { var Deno: typeof _Deno; } -const nitroApp = useNitroApp(); +const fetchHandler = withServerEntryOptions(useNitroApp().fetch); const ws = import.meta._websocket ? wsAdapter({ resolve: resolveWebsocketHooks }) : undefined; @@ -27,5 +28,5 @@ Deno.serve((denoReq: Request, info: _Deno.ServeHandlerInfo) => { return ws!.handleUpgrade(req, info); } - return nitroApp.fetch(req); + return fetchHandler(req); }); diff --git a/src/presets/edgeone/runtime/edgeone.ts b/src/presets/edgeone/runtime/edgeone.ts index 61c670fdb4..8ada62ec03 100644 --- a/src/presets/edgeone/runtime/edgeone.ts +++ b/src/presets/edgeone/runtime/edgeone.ts @@ -1,9 +1,10 @@ import "#nitro/virtual/polyfills"; import { NodeRequest } from "srvx/node"; import { useNitroApp } from "nitro/app"; +import { withServerEntryOptions } from "#nitro/runtime/serve"; import type { IncomingMessage } from "node:http"; -const nitroApp = useNitroApp(); +const fetchHandler = withServerEntryOptions(useNitroApp().fetch); interface EdgeOneRequest extends IncomingMessage { url: string; @@ -15,5 +16,5 @@ interface EdgeOneRequest extends IncomingMessage { export default async function handle(req: EdgeOneRequest) { // Use srvx NodeRequest to convert Node.js request to Web Request const request = new NodeRequest({ req }); - return nitroApp.fetch(request); + return fetchHandler(request); } diff --git a/src/presets/netlify/runtime/netlify-edge.ts b/src/presets/netlify/runtime/netlify-edge.ts index c93d46e498..5f3f318812 100644 --- a/src/presets/netlify/runtime/netlify-edge.ts +++ b/src/presets/netlify/runtime/netlify-edge.ts @@ -1,10 +1,11 @@ import "#nitro/virtual/polyfills"; import { useNitroApp } from "nitro/app"; +import { withServerEntryOptions } from "#nitro/runtime/serve"; import { isPublicAssetURL } from "#nitro/virtual/public-assets"; import type { Context } from "@netlify/edge-functions"; import type { ServerRequest } from "srvx"; -const nitroApp = useNitroApp(); +const fetchHandler = withServerEntryOptions(useNitroApp().fetch); // https://docs.netlify.com/edge-functions/api/ export default async function netlifyEdge(netlifyReq: Request, context: Context) { @@ -24,5 +25,5 @@ export default async function netlifyEdge(netlifyReq: Request, context: Context) req.headers.set("x-forwarded-proto", "https"); } - return nitroApp.fetch(req); + return fetchHandler(req); } diff --git a/src/presets/netlify/runtime/netlify.ts b/src/presets/netlify/runtime/netlify.ts index 4ecc22fe74..7f1d3df273 100644 --- a/src/presets/netlify/runtime/netlify.ts +++ b/src/presets/netlify/runtime/netlify.ts @@ -1,8 +1,9 @@ import "#nitro/virtual/polyfills"; import { useNitroApp } from "nitro/app"; +import { withServerEntryOptions } from "#nitro/runtime/serve"; import type { ServerRequest } from "srvx"; -const nitroApp = useNitroApp(); +const fetchHandler = withServerEntryOptions(useNitroApp().fetch); const ONE_YEAR_IN_SECONDS = 365 * 24 * 60 * 60; @@ -10,7 +11,7 @@ const handler = async (req: ServerRequest): Promise => { req.runtime ??= { name: "netlify" }; req.ip ??= req.headers.get("x-nf-client-connection-ip") || undefined; - const response = await nitroApp.fetch(req); + const response = await fetchHandler(req); const isr = req.context?.routeRules?.isr; if (isr) { diff --git a/src/presets/node/runtime/node-middleware.ts b/src/presets/node/runtime/node-middleware.ts index 8be7bcbed1..e6b191ba1f 100644 --- a/src/presets/node/runtime/node-middleware.ts +++ b/src/presets/node/runtime/node-middleware.ts @@ -5,10 +5,9 @@ import wsAdapter from "crossws/adapters/node"; import { useNitroApp } from "nitro/app"; import { startScheduleRunner } from "#nitro/runtime/task"; import { resolveWebsocketHooks } from "#nitro/runtime/app"; +import { withServerEntryOptions } from "#nitro/runtime/serve"; -const nitroApp = useNitroApp(); - -export const middleware = toNodeHandler(nitroApp.fetch); +export const middleware = toNodeHandler(withServerEntryOptions(useNitroApp().fetch)); const ws = import.meta._websocket ? wsAdapter({ resolve: resolveWebsocketHooks }) : undefined; diff --git a/src/presets/standard/runtime/server.ts b/src/presets/standard/runtime/server.ts index a67e6bb575..6a641d4f0a 100644 --- a/src/presets/standard/runtime/server.ts +++ b/src/presets/standard/runtime/server.ts @@ -1,8 +1,7 @@ import "#nitro/virtual/polyfills"; import { useNitroApp } from "nitro/app"; - -const nitroApp = useNitroApp(); +import { withServerEntryOptions } from "#nitro/runtime/serve"; export default { - fetch: nitroApp.fetch, + fetch: withServerEntryOptions(useNitroApp().fetch), }; diff --git a/src/presets/stormkit/runtime/stormkit.ts b/src/presets/stormkit/runtime/stormkit.ts index 8c5c09dda0..77d720706a 100644 --- a/src/presets/stormkit/runtime/stormkit.ts +++ b/src/presets/stormkit/runtime/stormkit.ts @@ -1,5 +1,6 @@ import "#nitro/virtual/polyfills"; import { useNitroApp } from "nitro/app"; +import { withServerEntryOptions } from "#nitro/runtime/serve"; import type { Handler } from "aws-lambda"; import type { ServerRequest } from "srvx"; @@ -23,7 +24,7 @@ type StormkitResponse = { errorStack?: string; }; -const nitroApp = useNitroApp(); +const fetchHandler = withServerEntryOptions(useNitroApp().fetch); export const handler: Handler = async function (event, context) { const req = new Request(event.url, { @@ -36,7 +37,7 @@ export const handler: Handler = async function req.runtime ??= { name: "stormkit" }; req.runtime.stormkit ??= { event, context } as any; - const response = await nitroApp.fetch(req); + const response = await fetchHandler(req); const { body, isBase64Encoded } = await encodeResponseBody(response); diff --git a/src/presets/vercel/runtime/vercel.node.ts b/src/presets/vercel/runtime/vercel.node.ts index ba06a0ae1b..43d8a8d6c3 100644 --- a/src/presets/vercel/runtime/vercel.node.ts +++ b/src/presets/vercel/runtime/vercel.node.ts @@ -5,6 +5,7 @@ import { toNodeHandler } from "srvx/node"; import wsAdapter from "crossws/adapters/vercel"; import { useNitroApp, getRouteRules } from "nitro/app"; import { resolveWebsocketHooks } from "#nitro/runtime/app"; +import { withServerEntryOptions } from "#nitro/runtime/serve"; import { isrRouteRewrite } from "./isr.ts"; interface VercelRequestContext { @@ -17,7 +18,7 @@ interface VercelRequestContextReader { const REQUEST_CONTEXT_SYMBOL = Symbol.for("@vercel/request-context"); -const nitroApp = useNitroApp(); +const fetchHandler = withServerEntryOptions(useNitroApp().fetch); // The Node runtime has no per-request `context` argument, so the request // context (`waitUntil`, ...) is read from the global symbol the runtime @@ -33,7 +34,7 @@ const handler = toNodeHandler((req: ServerRequest) => { req.waitUntil = context.waitUntil; } - return nitroApp.fetch(req); + return fetchHandler(req); }); const ws = import.meta._websocket ? wsAdapter({ resolve: resolveWebsocketHooks }) : undefined; diff --git a/src/presets/vercel/runtime/vercel.web.ts b/src/presets/vercel/runtime/vercel.web.ts index 77a4cd0451..9961024427 100644 --- a/src/presets/vercel/runtime/vercel.web.ts +++ b/src/presets/vercel/runtime/vercel.web.ts @@ -2,11 +2,12 @@ import "#nitro/virtual/polyfills"; import wsAdapter from "crossws/adapters/vercel"; import { useNitroApp, getRouteRules } from "nitro/app"; import { resolveWebsocketHooks } from "#nitro/runtime/app"; +import { withServerEntryOptions } from "#nitro/runtime/serve"; import type { ServerRequest } from "srvx"; import { isrRouteRewrite } from "./isr.ts"; -const nitroApp = useNitroApp(); +const fetchHandler = withServerEntryOptions(useNitroApp().fetch); const ws = import.meta._websocket ? wsAdapter({ resolve: resolveWebsocketHooks }) : undefined; @@ -46,6 +47,6 @@ export default { req.waitUntil = context?.waitUntil; - return nitroApp.fetch(req); + return fetchHandler(req); }, }; diff --git a/src/presets/winterjs/runtime/winterjs.ts b/src/presets/winterjs/runtime/winterjs.ts index ae88603eee..1430810cd5 100644 --- a/src/presets/winterjs/runtime/winterjs.ts +++ b/src/presets/winterjs/runtime/winterjs.ts @@ -3,13 +3,14 @@ import "./_polyfill.ts"; import type { ServerRequest, ServiceWorkerFetchEvent } from "srvx"; import { useNitroApp } from "nitro/app"; +import { withServerEntryOptions } from "#nitro/runtime/serve"; -const nitroApp = useNitroApp(); +const fetchHandler = withServerEntryOptions(useNitroApp().fetch); // WinterJS runs service worker scripts: https://github.com/wasmerio/winterjs addEventListener("fetch" as any, (event: ServiceWorkerFetchEvent) => { const request = event.request as ServerRequest; request.runtime ??= { name: "winterjs", serviceWorker: { event } }; request.waitUntil = (promise) => event.waitUntil(promise as Promise); - event.respondWith(nitroApp.fetch(request)); + event.respondWith(fetchHandler(request)); }); diff --git a/src/presets/zeabur/runtime/zeabur.ts b/src/presets/zeabur/runtime/zeabur.ts index 4a9b2db2e6..85e36458aa 100644 --- a/src/presets/zeabur/runtime/zeabur.ts +++ b/src/presets/zeabur/runtime/zeabur.ts @@ -1,5 +1,6 @@ import "#nitro/virtual/polyfills"; import { toNodeHandler } from "srvx/node"; import { useNitroApp } from "nitro/app"; +import { withServerEntryOptions } from "#nitro/runtime/serve"; -export default toNodeHandler(useNitroApp().fetch); +export default toNodeHandler(withServerEntryOptions(useNitroApp().fetch)); diff --git a/src/runtime/internal/serve.ts b/src/runtime/internal/serve.ts index e10c0c5469..d1954363ed 100644 --- a/src/runtime/internal/serve.ts +++ b/src/runtime/internal/serve.ts @@ -1,4 +1,5 @@ -import type { ServerOptions } from "srvx"; +import type { ServerOptions, ServerPlugin, ServerRequest } from "srvx"; +import { serve as serveGeneric } from "srvx/generic"; import { serverEntryOptions } from "#nitro/virtual/server-entry"; import { tracingSrvxPlugins } from "#nitro/virtual/tracing"; @@ -35,3 +36,48 @@ export function resolveServeOptions(opts: ServerOptions): ServerOptions { return resolved; } + +/** + * Apply server entry `middleware`, `plugins` and `error` options to the fetch handler of a preset + * that does not start a srvx server (serverless, edge and worker runtimes), using the srvx generic adapter. + * + * Listener options (`port`, `hostname`, `tls`, ...) and runtime specific options have no effect there. + */ +export function withServerEntryOptions< + T extends (req: ServerRequest) => Response | Promise, +>(fetch: T): T { + const { middleware, plugins, error } = serverEntryOptions; + if (!middleware?.length && !plugins?.length && !error) { + return fetch; + } + + // The generic adapter overrides `request.waitUntil` with its own (never awaited) implementation. + // Restore the one provided by the platform before server entry middleware runs. + const platformWaitUntil = new WeakMap(); + const restoreWaitUntil: ServerPlugin = (server) => { + server.options.middleware.unshift((req, next) => { + const waitUntil = platformWaitUntil.get(req); + if (waitUntil) { + Object.defineProperty(req, "waitUntil", { + value: waitUntil, + writable: true, + configurable: true, + }); + } + return next(); + }); + }; + + const server = serveGeneric({ + ...serverEntryOptions, + fetch, + plugins: [...(plugins || []), restoreWaitUntil], + }); + + return ((req: ServerRequest) => { + if (req.waitUntil) { + platformWaitUntil.set(req, req.waitUntil); + } + return server.fetch(req); + }) as T; +} diff --git a/test/fixture/server.ts b/test/fixture/server.ts index de257575ec..6c0b9f22ce 100644 --- a/test/fixture/server.ts +++ b/test/fixture/server.ts @@ -10,6 +10,7 @@ export default defineServerEntry({ }, // Passed to srvx (node, bun and deno servers) maxRequestBodySize: 64 * 1024, + // Applied by all presets middleware: [ (req, next) => { if (new URL(req.url).pathname === "/srvx-middleware") { diff --git a/test/tests.ts b/test/tests.ts index f0129bedd0..afed84bacb 100644 --- a/test/tests.ts +++ b/test/tests.ts @@ -249,13 +249,15 @@ export function testNitro( expect(headers["x-test"]).toBe("test"); }); + it("Server entry middleware and plugins are applied", async () => { + const { data, headers } = await callHandler({ url: "/srvx-middleware" }); + expect(data).toBe("server entry middleware works!"); + expect(headers["x-srvx-plugin"]).toBe("works"); + }); + it.runIf(["bun", "deno-server", "nitro-dev"].includes(ctx.preset))( "Server entry options are passed to srvx", async () => { - const { data, headers } = await callHandler({ url: "/srvx-middleware" }); - expect(data).toBe("server entry middleware works!"); - expect(headers["x-srvx-plugin"]).toBe("works"); - const small = await callHandler({ url: "/api/body-size", method: "POST", From de3b1c255a3ff498ba2aa712d989d44ed4dd509d Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Tue, 15 Sep 2026 08:34:47 +0000 Subject: [PATCH 2/6] refactor: compose server entry middleware without srvx/generic Keeps bundle size down and preserves the platform request.waitUntil. --- docs/1.docs/6.server-entry.md | 2 +- src/runtime/internal/serve.ts | 54 ++++++++++++++++------------------- 2 files changed, 26 insertions(+), 30 deletions(-) diff --git a/docs/1.docs/6.server-entry.md b/docs/1.docs/6.server-entry.md index 63a9d582e6..8a92e8b3a3 100644 --- a/docs/1.docs/6.server-entry.md +++ b/docs/1.docs/6.server-entry.md @@ -163,7 +163,7 @@ export default defineServerEntry({ ::read-more{to="https://srvx.h3.dev/guide/options" title="srvx server options"} :: -Other presets (serverless, edge and worker runtimes) do not start a server. They apply `middleware`, `plugins` and `error` to the request handler using the srvx generic adapter, so these behave the same everywhere. Listener and runtime specific options (such as `port`, `tls`, `maxRequestBodySize`, `trustProxy`, `gracefulShutdown`, `node`, `bun` and `deno`) have no effect there. +Other presets (serverless, edge and worker runtimes) do not start a server. They apply `middleware`, `plugins` and `error` to the request handler, so these behave the same everywhere. Listener and runtime specific options (such as `port`, `tls`, `maxRequestBodySize`, `trustProxy`, `gracefulShutdown`, `node`, `bun` and `deno`) have no effect there. ::note - `NITRO_PORT`/`PORT`, `NITRO_HOST`/`HOST` and `NITRO_SSL_CERT`/`NITRO_SSL_KEY` environment variables take precedence over the `port`, `hostname` and `tls` options, so the server stays configurable at runtime. diff --git a/src/runtime/internal/serve.ts b/src/runtime/internal/serve.ts index d1954363ed..d2eaa347ff 100644 --- a/src/runtime/internal/serve.ts +++ b/src/runtime/internal/serve.ts @@ -1,5 +1,4 @@ -import type { ServerOptions, ServerPlugin, ServerRequest } from "srvx"; -import { serve as serveGeneric } from "srvx/generic"; +import type { Server, ServerOptions, ServerRequest } from "srvx"; import { serverEntryOptions } from "#nitro/virtual/server-entry"; import { tracingSrvxPlugins } from "#nitro/virtual/tracing"; @@ -39,7 +38,7 @@ export function resolveServeOptions(opts: ServerOptions): ServerOptions { /** * Apply server entry `middleware`, `plugins` and `error` options to the fetch handler of a preset - * that does not start a srvx server (serverless, edge and worker runtimes), using the srvx generic adapter. + * that does not start a srvx server (serverless, edge and worker runtimes). * * Listener options (`port`, `hostname`, `tls`, ...) and runtime specific options have no effect there. */ @@ -51,33 +50,30 @@ export function withServerEntryOptions< return fetch; } - // The generic adapter overrides `request.waitUntil` with its own (never awaited) implementation. - // Restore the one provided by the platform before server entry middleware runs. - const platformWaitUntil = new WeakMap(); - const restoreWaitUntil: ServerPlugin = (server) => { - server.options.middleware.unshift((req, next) => { - const waitUntil = platformWaitUntil.get(req); - if (waitUntil) { - Object.defineProperty(req, "waitUntil", { - value: waitUntil, - writable: true, - configurable: true, - }); + const server = { + runtime: "generic", + options: { ...serverEntryOptions, fetch, middleware: [...(middleware || [])] }, + } as unknown as Server; + + for (const plugin of plugins || []) { + plugin(server); + } + + if (error) { + server.options.middleware.unshift(async (_req, next) => { + try { + return await next(); + } catch (error_) { + return error(error_); } - return next(); }); - }; - - const server = serveGeneric({ - ...serverEntryOptions, - fetch, - plugins: [...(plugins || []), restoreWaitUntil], - }); + } - return ((req: ServerRequest) => { - if (req.waitUntil) { - platformWaitUntil.set(req, req.waitUntil); - } - return server.fetch(req); - }) as T; + let handler = server.options.fetch; + for (let i = server.options.middleware.length - 1; i >= 0; i--) { + const mw = server.options.middleware[i]!; + const next = handler; + handler = (req) => mw(req, () => next(req)); + } + return handler as T; } From 7cee1c751db478900172f76be2b310c88d7800be Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Tue, 15 Sep 2026 08:53:29 +0000 Subject: [PATCH 3/6] refactor: apply server entry options in useNitroApp().fetch srvx server presets (node, bun, deno, nitro-dev) use the raw nitroApp["~fetch"] handler and pass options to srvx natively. --- docs/1.docs/6.server-entry.md | 4 ++-- src/build/virtual/app.ts | 11 ++++++++++- src/presets/_nitro/runtime/nitro-dev.ts | 2 +- src/presets/_nitro/runtime/service-worker.ts | 5 ++--- src/presets/aws-amplify/runtime/aws-amplify.ts | 7 +++---- .../aws-lambda/runtime/aws-lambda-streaming.ts | 5 ++--- src/presets/aws-lambda/runtime/aws-lambda.ts | 5 ++--- src/presets/azure/runtime/azure-swa.ts | 5 ++--- src/presets/bun/runtime/bun.ts | 4 ++-- .../cloudflare/runtime/_module-handler.ts | 5 ++--- .../cloudflare/runtime/cloudflare-durable.ts | 5 ++--- .../cloudflare/runtime/cloudflare-pages.ts | 5 ++--- src/presets/deno/runtime/deno-deploy.ts | 5 ++--- src/presets/deno/runtime/deno-server.ts | 4 ++-- src/presets/edgeone/runtime/edgeone.ts | 5 ++--- src/presets/netlify/runtime/netlify-edge.ts | 5 ++--- src/presets/netlify/runtime/netlify.ts | 5 ++--- src/presets/node/runtime/node-cluster.ts | 2 +- src/presets/node/runtime/node-middleware.ts | 5 +++-- src/presets/node/runtime/node-server.ts | 2 +- src/presets/standard/runtime/server.ts | 5 +++-- src/presets/stormkit/runtime/stormkit.ts | 5 ++--- src/presets/vercel/runtime/vercel.node.ts | 5 ++--- src/presets/vercel/runtime/vercel.web.ts | 5 ++--- src/presets/winterjs/runtime/winterjs.ts | 5 ++--- src/presets/zeabur/runtime/zeabur.ts | 3 +-- src/runtime/internal/app.ts | 2 +- src/runtime/internal/serve.ts | 16 +++++++++++----- src/runtime/virtual/app.ts | 12 +++++++----- src/types/runtime/nitro.ts | 7 +++++++ test/fixture/server/routes/api/app-fetch.ts | 7 +++++++ test/presets/vercel.test.ts | 5 +++++ test/tests.ts | 5 +++++ 33 files changed, 102 insertions(+), 76 deletions(-) create mode 100644 test/fixture/server/routes/api/app-fetch.ts diff --git a/docs/1.docs/6.server-entry.md b/docs/1.docs/6.server-entry.md index 8a92e8b3a3..b59dcf2d0f 100644 --- a/docs/1.docs/6.server-entry.md +++ b/docs/1.docs/6.server-entry.md @@ -163,11 +163,11 @@ export default defineServerEntry({ ::read-more{to="https://srvx.h3.dev/guide/options" title="srvx server options"} :: -Other presets (serverless, edge and worker runtimes) do not start a server. They apply `middleware`, `plugins` and `error` to the request handler, so these behave the same everywhere. Listener and runtime specific options (such as `port`, `tls`, `maxRequestBodySize`, `trustProxy`, `gracefulShutdown`, `node`, `bun` and `deno`) have no effect there. +`middleware`, `plugins` and `error` are applied by every preset, including serverless, edge and worker runtimes that do not start a server, and by `useNitroApp().fetch()`. Listener and runtime specific options (such as `port`, `tls`, `maxRequestBodySize`, `trustProxy`, `gracefulShutdown`, `node`, `bun` and `deno`) only apply to the Node.js, Bun and Deno servers. ::note - `NITRO_PORT`/`PORT`, `NITRO_HOST`/`HOST` and `NITRO_SSL_CERT`/`NITRO_SSL_KEY` environment variables take precedence over the `port`, `hostname` and `tls` options, so the server stays configurable at runtime. -- During development (`nitro dev`), options are applied to the dev worker server, except listener options (`port`, `hostname`, `protocol`, `tls`, `silent`, `gracefulShutdown`) which are controlled by the dev server and CLI (`--port`, `--host`). The Vite dev server applies none of them. +- During development (`nitro dev`), options are applied to the dev worker server, except listener options (`port`, `hostname`, `protocol`, `tls`, `silent`, `gracefulShutdown`) which are controlled by the dev server and CLI (`--port`, `--host`). The Vite dev server only applies `middleware`, `plugins` and `error`. - `manual` is not supported: presets start listening immediately. - Options are only read from plain object exports (not from framework instances like `export default app`) and never from Node.js format entries (`server.node.ts`). :: diff --git a/src/build/virtual/app.ts b/src/build/virtual/app.ts index a1500ab6c1..8bb5e34b39 100644 --- a/src/build/virtual/app.ts +++ b/src/build/virtual/app.ts @@ -11,6 +11,10 @@ export default function app(nitro: Nitro) { const hasPlugins = nitro.options.plugins.length > 0; const hasHooks = nitro.options.features?.runtimeHooks ?? hasPlugins; const hasAsyncContext = !!nitro.options.experimental.asyncContext; + const hasServerEntry = + !!nitro.options.serverEntry && + !!nitro.options.serverEntry.handler && + nitro.options.serverEntry.format !== "node"; const routingImports = [ hasRoutes && "findRoute", @@ -101,10 +105,15 @@ export default function app(nitro: Nitro) { ); } + if (hasServerEntry) { + imports.push(`import { withServerEntryOptions } from "#nitro/runtime/serve";`); + } + code.push( ``, ` return {`, - ` fetch: appHandler,`, + ` fetch: ${hasServerEntry ? "withServerEntryOptions(appHandler)" : "appHandler"},`, + ` "~fetch": appHandler,`, ` h3: h3App,`, ` hooks: ${hasHooks ? "hooks" : "undefined"},`, ` captureError,`, diff --git a/src/presets/_nitro/runtime/nitro-dev.ts b/src/presets/_nitro/runtime/nitro-dev.ts index 6bae75145b..ab6235bbf0 100644 --- a/src/presets/_nitro/runtime/nitro-dev.ts +++ b/src/presets/_nitro/runtime/nitro-dev.ts @@ -27,7 +27,7 @@ const ws = import.meta._websocket export default { ...serverEntryOptions, - fetch: nitroApp.fetch, + fetch: nitroApp["~fetch"], plugins: [...tracingSrvxPlugins, ...(serverEntryOptions.plugins || [])], upgrade: ws ? (context: { node: { req: any; socket: any; head: any } }) => { diff --git a/src/presets/_nitro/runtime/service-worker.ts b/src/presets/_nitro/runtime/service-worker.ts index 18303fca53..dfe8138e54 100644 --- a/src/presets/_nitro/runtime/service-worker.ts +++ b/src/presets/_nitro/runtime/service-worker.ts @@ -1,10 +1,9 @@ import "#nitro/virtual/polyfills"; import { useNitroApp } from "nitro/app"; -import { withServerEntryOptions } from "#nitro/runtime/serve"; import { isPublicAssetURL } from "#nitro/virtual/public-assets"; import type { ServerRequest } from "srvx"; -const fetchHandler = withServerEntryOptions(useNitroApp().fetch); +const nitroApp = useNitroApp(); // @ts-expect-error addEventListener("fetch", (event: FetchEvent) => { @@ -19,7 +18,7 @@ addEventListener("fetch", (event: FetchEvent) => { req.runtime.serviceWorker ??= { event } as any; req.waitUntil = event.waitUntil.bind(event); - event.respondWith(fetchHandler(req)); + event.respondWith(nitroApp.fetch(req)); }); declare const self: ServiceWorkerGlobalScope; diff --git a/src/presets/aws-amplify/runtime/aws-amplify.ts b/src/presets/aws-amplify/runtime/aws-amplify.ts index 88b5b016e0..673fb81d68 100644 --- a/src/presets/aws-amplify/runtime/aws-amplify.ts +++ b/src/presets/aws-amplify/runtime/aws-amplify.ts @@ -1,14 +1,13 @@ import "#nitro/virtual/polyfills"; import { useNitroApp } from "nitro/app"; -import { withServerEntryOptions } from "#nitro/runtime/serve"; import { Server } from "node:http"; import type { NodeHttp1Handler } from "srvx"; import { toNodeHandler } from "srvx/node"; -const server = new Server( - toNodeHandler(withServerEntryOptions(useNitroApp().fetch)) as NodeHttp1Handler -); +const nitroApp = useNitroApp(); + +const server = new Server(toNodeHandler(nitroApp.fetch) as NodeHttp1Handler); // @ts-ignore server.listen(3000, (err) => { diff --git a/src/presets/aws-lambda/runtime/aws-lambda-streaming.ts b/src/presets/aws-lambda/runtime/aws-lambda-streaming.ts index 69db86a0db..07034b7ad1 100644 --- a/src/presets/aws-lambda/runtime/aws-lambda-streaming.ts +++ b/src/presets/aws-lambda/runtime/aws-lambda-streaming.ts @@ -1,10 +1,9 @@ import "#nitro/virtual/polyfills"; import { handleLambdaEventWithStream } from "srvx/aws-lambda"; import { useNitroApp } from "nitro/app"; -import { withServerEntryOptions } from "#nitro/runtime/serve"; -const fetchHandler = withServerEntryOptions(useNitroApp().fetch); +const nitroApp = useNitroApp(); export const handler = awslambda.streamifyResponse((event, responseStream, context) => - handleLambdaEventWithStream(fetchHandler, event, responseStream, context) + handleLambdaEventWithStream(nitroApp.fetch, event, responseStream, context) ); diff --git a/src/presets/aws-lambda/runtime/aws-lambda.ts b/src/presets/aws-lambda/runtime/aws-lambda.ts index e78fce2f62..a8220f8caf 100644 --- a/src/presets/aws-lambda/runtime/aws-lambda.ts +++ b/src/presets/aws-lambda/runtime/aws-lambda.ts @@ -1,13 +1,12 @@ import "#nitro/virtual/polyfills"; import { handleLambdaEvent } from "srvx/aws-lambda"; import { useNitroApp } from "nitro/app"; -import { withServerEntryOptions } from "#nitro/runtime/serve"; import type { AwsLambdaEvent } from "srvx/aws-lambda"; import type { Context } from "aws-lambda"; -const fetchHandler = withServerEntryOptions(useNitroApp().fetch); +const nitroApp = useNitroApp(); export async function handler(event: AwsLambdaEvent, context: Context) { - return handleLambdaEvent(fetchHandler, event, context); + return handleLambdaEvent(nitroApp.fetch, event, context); } diff --git a/src/presets/azure/runtime/azure-swa.ts b/src/presets/azure/runtime/azure-swa.ts index 75d562f210..9d4e178da1 100644 --- a/src/presets/azure/runtime/azure-swa.ts +++ b/src/presets/azure/runtime/azure-swa.ts @@ -1,12 +1,11 @@ import "#nitro/virtual/polyfills"; import { parseURL } from "ufo"; import { useNitroApp } from "nitro/app"; -import { withServerEntryOptions } from "#nitro/runtime/serve"; import { getAzureParsedCookiesFromHeaders, resolveBaseUrl } from "./_utils.ts"; import type { HttpRequest, HttpResponse, HttpResponseSimple } from "@azure/functions"; -const fetchHandler = withServerEntryOptions(useNitroApp().fetch); +const nitroApp = useNitroApp(); export async function handle(context: { res: HttpResponse }, req: HttpRequest) { let url: string; @@ -28,7 +27,7 @@ export async function handle(context: { res: HttpResponse }, req: HttpRequest) { body: req.bufferBody ?? req.rawBody, }); - const response = await fetchHandler(request); + const response = await nitroApp.fetch(request); // (v3 - current) https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-node?tabs=typescript%2Cwindows%2Cazure-cli&pivots=nodejs-model-v3#http-response // (v4) https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-node?tabs=typescript%2Cwindows%2Cazure-cli&pivots=nodejs-model-v4#http-response diff --git a/src/presets/bun/runtime/bun.ts b/src/presets/bun/runtime/bun.ts index 2b78fcb355..7daca4b835 100644 --- a/src/presets/bun/runtime/bun.ts +++ b/src/presets/bun/runtime/bun.ts @@ -12,7 +12,7 @@ import { setupCloseHooks } from "#nitro/runtime/shutdown"; const nitroApp = useNitroApp(); -let _fetch = nitroApp.fetch; +let _fetch = nitroApp["~fetch"]; const ws = import.meta._websocket ? wsAdapter({ resolve: resolveWebsocketHooks }) : undefined; @@ -21,7 +21,7 @@ if (import.meta._websocket) { if (req.headers.get("upgrade") === "websocket") { return ws!.handleUpgrade(req, req.runtime!.bun!.server) as Promise; } - return nitroApp.fetch(req); + return nitroApp["~fetch"](req); }; } diff --git a/src/presets/cloudflare/runtime/_module-handler.ts b/src/presets/cloudflare/runtime/_module-handler.ts index 8a28af5625..8d7eeefb07 100644 --- a/src/presets/cloudflare/runtime/_module-handler.ts +++ b/src/presets/cloudflare/runtime/_module-handler.ts @@ -4,7 +4,6 @@ import type { ServerRequest, ServerRuntimeContext } from "srvx"; import { runCronTasks } from "#nitro/runtime/task"; import { useNitroApp, useNitroHooks } from "nitro/app"; -import { withServerEntryOptions } from "#nitro/runtime/serve"; type MaybePromise = T | Promise; @@ -17,7 +16,7 @@ export function createHandler(hooks: { ] ) => MaybePromise; }) { - const fetchHandler = withServerEntryOptions(useNitroApp().fetch); + const nitroApp = useNitroApp(); const nitroHooks = useNitroHooks(); return { @@ -36,7 +35,7 @@ export function createHandler(hooks: { } } - return (await fetchHandler(request as any)) as any; + return (await nitroApp.fetch(request)) as any; }, scheduled(controller, env, context) { diff --git a/src/presets/cloudflare/runtime/cloudflare-durable.ts b/src/presets/cloudflare/runtime/cloudflare-durable.ts index 1eb80390e9..a2d8b50f09 100644 --- a/src/presets/cloudflare/runtime/cloudflare-durable.ts +++ b/src/presets/cloudflare/runtime/cloudflare-durable.ts @@ -7,7 +7,6 @@ import { createHandler, augmentReq } from "./_module-handler.ts"; import { useNitroApp, useNitroHooks } from "nitro/app"; import { isPublicAssetURL } from "#nitro/virtual/public-assets"; import { resolveWebsocketHooks } from "#nitro/runtime/app"; -import { withServerEntryOptions } from "#nitro/runtime/serve"; const DURABLE_BINDING = "$DurableObject"; const DURABLE_INSTANCE = "server"; @@ -17,7 +16,7 @@ interface Env { [DURABLE_BINDING]?: CF.DurableObjectNamespace; } -const fetchHandler = withServerEntryOptions(useNitroApp().fetch); +const nitroApp = useNitroApp(); const nitroHooks = useNitroHooks(); const getDurableStub = (env: Env) => { @@ -79,7 +78,7 @@ export class $DurableObject extends DurableObject { return ws!.handleDurableUpgrade(this, request); } - return fetchHandler(request); + return nitroApp.fetch(request); } override alarm(): void | Promise { diff --git a/src/presets/cloudflare/runtime/cloudflare-pages.ts b/src/presets/cloudflare/runtime/cloudflare-pages.ts index 6d066915d5..695f63f117 100644 --- a/src/presets/cloudflare/runtime/cloudflare-pages.ts +++ b/src/presets/cloudflare/runtime/cloudflare-pages.ts @@ -10,7 +10,6 @@ import { useNitroApp } from "nitro/app"; import { isPublicAssetURL } from "#nitro/virtual/public-assets"; import { runCronTasks } from "#nitro/runtime/task"; import { resolveWebsocketHooks } from "#nitro/runtime/app"; -import { withServerEntryOptions } from "#nitro/runtime/serve"; import { augmentReq } from "./_module-handler.ts"; @@ -27,7 +26,7 @@ interface CFPagesEnv { [key: string]: any; } -const fetchHandler = withServerEntryOptions(useNitroApp().fetch); +const nitroApp = useNitroApp(); const ws = import.meta._websocket ? wsAdapter({ resolve: resolveWebsocketHooks }) : undefined; @@ -49,7 +48,7 @@ export default { return env.ASSETS.fetch(cfReq); } - return fetchHandler(cfReq as any); + return nitroApp.fetch(cfReq as any); }, scheduled(event: any, env: CFPagesEnv, context: ExecutionContext) { if (import.meta._tasks) { diff --git a/src/presets/deno/runtime/deno-deploy.ts b/src/presets/deno/runtime/deno-deploy.ts index 19e36be27c..5ab1bbdbc7 100644 --- a/src/presets/deno/runtime/deno-deploy.ts +++ b/src/presets/deno/runtime/deno-deploy.ts @@ -5,13 +5,12 @@ import wsAdapter from "crossws/adapters/deno"; import { useNitroApp } from "nitro/app"; import { resolveWebsocketHooks } from "#nitro/runtime/app"; -import { withServerEntryOptions } from "#nitro/runtime/serve"; declare global { var Deno: typeof _Deno; } -const fetchHandler = withServerEntryOptions(useNitroApp().fetch); +const nitroApp = useNitroApp(); const ws = import.meta._websocket ? wsAdapter({ resolve: resolveWebsocketHooks }) : undefined; @@ -28,5 +27,5 @@ Deno.serve((denoReq: Request, info: _Deno.ServeHandlerInfo) => { return ws!.handleUpgrade(req, info); } - return fetchHandler(req); + return nitroApp.fetch(req); }); diff --git a/src/presets/deno/runtime/deno-server.ts b/src/presets/deno/runtime/deno-server.ts index fe45ec783e..9b3650865a 100644 --- a/src/presets/deno/runtime/deno-server.ts +++ b/src/presets/deno/runtime/deno-server.ts @@ -12,7 +12,7 @@ import { setupCloseHooks } from "#nitro/runtime/shutdown"; const nitroApp = useNitroApp(); -let _fetch = nitroApp.fetch; +let _fetch = nitroApp["~fetch"]; if (import.meta._websocket) { const { handleUpgrade } = wsAdapter({ resolve: resolveWebsocketHooks }); @@ -20,7 +20,7 @@ if (import.meta._websocket) { if (req.headers.get("upgrade") === "websocket") { return handleUpgrade(req, req.runtime!.deno!.info); } - return nitroApp.fetch(req); + return nitroApp["~fetch"](req); }; } diff --git a/src/presets/edgeone/runtime/edgeone.ts b/src/presets/edgeone/runtime/edgeone.ts index 8ada62ec03..61c670fdb4 100644 --- a/src/presets/edgeone/runtime/edgeone.ts +++ b/src/presets/edgeone/runtime/edgeone.ts @@ -1,10 +1,9 @@ import "#nitro/virtual/polyfills"; import { NodeRequest } from "srvx/node"; import { useNitroApp } from "nitro/app"; -import { withServerEntryOptions } from "#nitro/runtime/serve"; import type { IncomingMessage } from "node:http"; -const fetchHandler = withServerEntryOptions(useNitroApp().fetch); +const nitroApp = useNitroApp(); interface EdgeOneRequest extends IncomingMessage { url: string; @@ -16,5 +15,5 @@ interface EdgeOneRequest extends IncomingMessage { export default async function handle(req: EdgeOneRequest) { // Use srvx NodeRequest to convert Node.js request to Web Request const request = new NodeRequest({ req }); - return fetchHandler(request); + return nitroApp.fetch(request); } diff --git a/src/presets/netlify/runtime/netlify-edge.ts b/src/presets/netlify/runtime/netlify-edge.ts index 5f3f318812..c93d46e498 100644 --- a/src/presets/netlify/runtime/netlify-edge.ts +++ b/src/presets/netlify/runtime/netlify-edge.ts @@ -1,11 +1,10 @@ import "#nitro/virtual/polyfills"; import { useNitroApp } from "nitro/app"; -import { withServerEntryOptions } from "#nitro/runtime/serve"; import { isPublicAssetURL } from "#nitro/virtual/public-assets"; import type { Context } from "@netlify/edge-functions"; import type { ServerRequest } from "srvx"; -const fetchHandler = withServerEntryOptions(useNitroApp().fetch); +const nitroApp = useNitroApp(); // https://docs.netlify.com/edge-functions/api/ export default async function netlifyEdge(netlifyReq: Request, context: Context) { @@ -25,5 +24,5 @@ export default async function netlifyEdge(netlifyReq: Request, context: Context) req.headers.set("x-forwarded-proto", "https"); } - return fetchHandler(req); + return nitroApp.fetch(req); } diff --git a/src/presets/netlify/runtime/netlify.ts b/src/presets/netlify/runtime/netlify.ts index 7f1d3df273..4ecc22fe74 100644 --- a/src/presets/netlify/runtime/netlify.ts +++ b/src/presets/netlify/runtime/netlify.ts @@ -1,9 +1,8 @@ import "#nitro/virtual/polyfills"; import { useNitroApp } from "nitro/app"; -import { withServerEntryOptions } from "#nitro/runtime/serve"; import type { ServerRequest } from "srvx"; -const fetchHandler = withServerEntryOptions(useNitroApp().fetch); +const nitroApp = useNitroApp(); const ONE_YEAR_IN_SECONDS = 365 * 24 * 60 * 60; @@ -11,7 +10,7 @@ const handler = async (req: ServerRequest): Promise => { req.runtime ??= { name: "netlify" }; req.ip ??= req.headers.get("x-nf-client-connection-ip") || undefined; - const response = await fetchHandler(req); + const response = await nitroApp.fetch(req); const isr = req.context?.routeRules?.isr; if (isr) { diff --git a/src/presets/node/runtime/node-cluster.ts b/src/presets/node/runtime/node-cluster.ts index 6fd840e6fb..accb1ae69e 100644 --- a/src/presets/node/runtime/node-cluster.ts +++ b/src/presets/node/runtime/node-cluster.ts @@ -19,7 +19,7 @@ const nitroApp = useNitroApp(); const server = serve( resolveServeOptions({ - fetch: nitroApp.fetch, + fetch: nitroApp["~fetch"], node: { exclusive: false }, ...(clusterId && clusterId !== "1" ? { silent: true } : {}), }) diff --git a/src/presets/node/runtime/node-middleware.ts b/src/presets/node/runtime/node-middleware.ts index e6b191ba1f..8be7bcbed1 100644 --- a/src/presets/node/runtime/node-middleware.ts +++ b/src/presets/node/runtime/node-middleware.ts @@ -5,9 +5,10 @@ import wsAdapter from "crossws/adapters/node"; import { useNitroApp } from "nitro/app"; import { startScheduleRunner } from "#nitro/runtime/task"; import { resolveWebsocketHooks } from "#nitro/runtime/app"; -import { withServerEntryOptions } from "#nitro/runtime/serve"; -export const middleware = toNodeHandler(withServerEntryOptions(useNitroApp().fetch)); +const nitroApp = useNitroApp(); + +export const middleware = toNodeHandler(nitroApp.fetch); const ws = import.meta._websocket ? wsAdapter({ resolve: resolveWebsocketHooks }) : undefined; diff --git a/src/presets/node/runtime/node-server.ts b/src/presets/node/runtime/node-server.ts index e9fd5226ea..d0d188c041 100644 --- a/src/presets/node/runtime/node-server.ts +++ b/src/presets/node/runtime/node-server.ts @@ -11,7 +11,7 @@ import { setupCloseHooks } from "#nitro/runtime/shutdown"; const nitroApp = useNitroApp(); -const server = serve(resolveServeOptions({ fetch: nitroApp.fetch })); +const server = serve(resolveServeOptions({ fetch: nitroApp["~fetch"] })); if (import.meta._websocket) { const { handleUpgrade } = wsAdapter({ resolve: resolveWebsocketHooks }); diff --git a/src/presets/standard/runtime/server.ts b/src/presets/standard/runtime/server.ts index 6a641d4f0a..a67e6bb575 100644 --- a/src/presets/standard/runtime/server.ts +++ b/src/presets/standard/runtime/server.ts @@ -1,7 +1,8 @@ import "#nitro/virtual/polyfills"; import { useNitroApp } from "nitro/app"; -import { withServerEntryOptions } from "#nitro/runtime/serve"; + +const nitroApp = useNitroApp(); export default { - fetch: withServerEntryOptions(useNitroApp().fetch), + fetch: nitroApp.fetch, }; diff --git a/src/presets/stormkit/runtime/stormkit.ts b/src/presets/stormkit/runtime/stormkit.ts index 77d720706a..8c5c09dda0 100644 --- a/src/presets/stormkit/runtime/stormkit.ts +++ b/src/presets/stormkit/runtime/stormkit.ts @@ -1,6 +1,5 @@ import "#nitro/virtual/polyfills"; import { useNitroApp } from "nitro/app"; -import { withServerEntryOptions } from "#nitro/runtime/serve"; import type { Handler } from "aws-lambda"; import type { ServerRequest } from "srvx"; @@ -24,7 +23,7 @@ type StormkitResponse = { errorStack?: string; }; -const fetchHandler = withServerEntryOptions(useNitroApp().fetch); +const nitroApp = useNitroApp(); export const handler: Handler = async function (event, context) { const req = new Request(event.url, { @@ -37,7 +36,7 @@ export const handler: Handler = async function req.runtime ??= { name: "stormkit" }; req.runtime.stormkit ??= { event, context } as any; - const response = await fetchHandler(req); + const response = await nitroApp.fetch(req); const { body, isBase64Encoded } = await encodeResponseBody(response); diff --git a/src/presets/vercel/runtime/vercel.node.ts b/src/presets/vercel/runtime/vercel.node.ts index 43d8a8d6c3..ba06a0ae1b 100644 --- a/src/presets/vercel/runtime/vercel.node.ts +++ b/src/presets/vercel/runtime/vercel.node.ts @@ -5,7 +5,6 @@ import { toNodeHandler } from "srvx/node"; import wsAdapter from "crossws/adapters/vercel"; import { useNitroApp, getRouteRules } from "nitro/app"; import { resolveWebsocketHooks } from "#nitro/runtime/app"; -import { withServerEntryOptions } from "#nitro/runtime/serve"; import { isrRouteRewrite } from "./isr.ts"; interface VercelRequestContext { @@ -18,7 +17,7 @@ interface VercelRequestContextReader { const REQUEST_CONTEXT_SYMBOL = Symbol.for("@vercel/request-context"); -const fetchHandler = withServerEntryOptions(useNitroApp().fetch); +const nitroApp = useNitroApp(); // The Node runtime has no per-request `context` argument, so the request // context (`waitUntil`, ...) is read from the global symbol the runtime @@ -34,7 +33,7 @@ const handler = toNodeHandler((req: ServerRequest) => { req.waitUntil = context.waitUntil; } - return fetchHandler(req); + return nitroApp.fetch(req); }); const ws = import.meta._websocket ? wsAdapter({ resolve: resolveWebsocketHooks }) : undefined; diff --git a/src/presets/vercel/runtime/vercel.web.ts b/src/presets/vercel/runtime/vercel.web.ts index 9961024427..77a4cd0451 100644 --- a/src/presets/vercel/runtime/vercel.web.ts +++ b/src/presets/vercel/runtime/vercel.web.ts @@ -2,12 +2,11 @@ import "#nitro/virtual/polyfills"; import wsAdapter from "crossws/adapters/vercel"; import { useNitroApp, getRouteRules } from "nitro/app"; import { resolveWebsocketHooks } from "#nitro/runtime/app"; -import { withServerEntryOptions } from "#nitro/runtime/serve"; import type { ServerRequest } from "srvx"; import { isrRouteRewrite } from "./isr.ts"; -const fetchHandler = withServerEntryOptions(useNitroApp().fetch); +const nitroApp = useNitroApp(); const ws = import.meta._websocket ? wsAdapter({ resolve: resolveWebsocketHooks }) : undefined; @@ -47,6 +46,6 @@ export default { req.waitUntil = context?.waitUntil; - return fetchHandler(req); + return nitroApp.fetch(req); }, }; diff --git a/src/presets/winterjs/runtime/winterjs.ts b/src/presets/winterjs/runtime/winterjs.ts index 1430810cd5..ae88603eee 100644 --- a/src/presets/winterjs/runtime/winterjs.ts +++ b/src/presets/winterjs/runtime/winterjs.ts @@ -3,14 +3,13 @@ import "./_polyfill.ts"; import type { ServerRequest, ServiceWorkerFetchEvent } from "srvx"; import { useNitroApp } from "nitro/app"; -import { withServerEntryOptions } from "#nitro/runtime/serve"; -const fetchHandler = withServerEntryOptions(useNitroApp().fetch); +const nitroApp = useNitroApp(); // WinterJS runs service worker scripts: https://github.com/wasmerio/winterjs addEventListener("fetch" as any, (event: ServiceWorkerFetchEvent) => { const request = event.request as ServerRequest; request.runtime ??= { name: "winterjs", serviceWorker: { event } }; request.waitUntil = (promise) => event.waitUntil(promise as Promise); - event.respondWith(fetchHandler(request)); + event.respondWith(nitroApp.fetch(request)); }); diff --git a/src/presets/zeabur/runtime/zeabur.ts b/src/presets/zeabur/runtime/zeabur.ts index 85e36458aa..4a9b2db2e6 100644 --- a/src/presets/zeabur/runtime/zeabur.ts +++ b/src/presets/zeabur/runtime/zeabur.ts @@ -1,6 +1,5 @@ import "#nitro/virtual/polyfills"; import { toNodeHandler } from "srvx/node"; import { useNitroApp } from "nitro/app"; -import { withServerEntryOptions } from "#nitro/runtime/serve"; -export default toNodeHandler(withServerEntryOptions(useNitroApp().fetch)); +export default toNodeHandler(useNitroApp().fetch); diff --git a/src/runtime/internal/app.ts b/src/runtime/internal/app.ts index 51c6d3df3c..5f72818b2e 100644 --- a/src/runtime/internal/app.ts +++ b/src/runtime/internal/app.ts @@ -45,7 +45,7 @@ export function serverFetch( ): Promise { const req = toRequest(resource, init); req.context = { ...req.context, ...context } as ServerRequestContext; - const appHandler = useNitroApp().fetch; + const appHandler = useNitroApp()["~fetch"]; try { return Promise.resolve(appHandler(req)); } catch (error) { diff --git a/src/runtime/internal/serve.ts b/src/runtime/internal/serve.ts index d2eaa347ff..216e1a7837 100644 --- a/src/runtime/internal/serve.ts +++ b/src/runtime/internal/serve.ts @@ -1,4 +1,4 @@ -import type { Server, ServerOptions, ServerRequest } from "srvx"; +import type { Server, ServerHandler, ServerOptions, ServerRequest } from "srvx"; import { serverEntryOptions } from "#nitro/virtual/server-entry"; import { tracingSrvxPlugins } from "#nitro/virtual/tracing"; @@ -37,10 +37,10 @@ export function resolveServeOptions(opts: ServerOptions): ServerOptions { } /** - * Apply server entry `middleware`, `plugins` and `error` options to the fetch handler of a preset - * that does not start a srvx server (serverless, edge and worker runtimes). + * Apply server entry `middleware`, `plugins` and `error` options to the Nitro app fetch handler. * - * Listener options (`port`, `hostname`, `tls`, ...) and runtime specific options have no effect there. + * Plugins are called with a minimal server object (`runtime` and `options`) on first request. + * Presets starting a srvx server use the raw handler (`nitroApp["~fetch"]`) and pass options to srvx instead. */ export function withServerEntryOptions< T extends (req: ServerRequest) => Response | Promise, @@ -49,6 +49,12 @@ export function withServerEntryOptions< if (!middleware?.length && !plugins?.length && !error) { return fetch; } + let handler: ServerHandler | undefined; + return ((req: ServerRequest) => (handler ??= composeHandler(fetch))(req)) as T; +} + +function composeHandler(fetch: ServerHandler): ServerHandler { + const { middleware, plugins, error } = serverEntryOptions; const server = { runtime: "generic", @@ -75,5 +81,5 @@ export function withServerEntryOptions< const next = handler; handler = (req) => mw(req, () => next(req)); } - return handler as T; + return handler; } diff --git a/src/runtime/virtual/app.ts b/src/runtime/virtual/app.ts index a00f13b751..e21a240da0 100644 --- a/src/runtime/virtual/app.ts +++ b/src/runtime/virtual/app.ts @@ -18,12 +18,14 @@ export function createNitroApp(): NitroApp { } } }; + const appHandler = (req: ServerRequest) => { + req.context ||= {}; + req.context.nitro = req.context.nitro || { errors: [] }; + return h3App.fetch(req); + }; return { - fetch: (req: ServerRequest) => { - req.context ||= {}; - req.context.nitro = req.context.nitro || { errors: [] }; - return h3App.fetch(req); - }, + fetch: appHandler, + "~fetch": appHandler, h3: h3App, hooks: undefined, captureError, diff --git a/src/types/runtime/nitro.ts b/src/types/runtime/nitro.ts index c891816eaa..612ce5f268 100644 --- a/src/types/runtime/nitro.ts +++ b/src/types/runtime/nitro.ts @@ -8,7 +8,14 @@ import type { ServerRequest } from "srvx"; * @see https://nitro.build/docs/plugins */ export interface NitroApp { + /** + * Handle a request, including server entry `middleware`, `plugins` and `error` options. + */ fetch: (req: Request) => Response | Promise; + /** + * Handle a request without server entry options (used by srvx server presets, which apply them natively). + */ + "~fetch": (req: Request) => Response | Promise; h3?: H3Core; hooks?: HookableCore; captureError?: CaptureError; diff --git a/test/fixture/server/routes/api/app-fetch.ts b/test/fixture/server/routes/api/app-fetch.ts new file mode 100644 index 0000000000..1492ab073b --- /dev/null +++ b/test/fixture/server/routes/api/app-fetch.ts @@ -0,0 +1,7 @@ +import { defineHandler } from "nitro"; +import { useNitroApp } from "nitro/app"; + +export default defineHandler(async () => { + const res = await useNitroApp().fetch(new Request("http://localhost/srvx-middleware")); + return { body: await res.text(), plugin: res.headers.get("x-srvx-plugin") }; +}); diff --git a/test/presets/vercel.test.ts b/test/presets/vercel.test.ts index c76f46f63b..46a75cdc53 100644 --- a/test/presets/vercel.test.ts +++ b/test/presets/vercel.test.ts @@ -414,6 +414,10 @@ describe("nitro:preset:vercel:web", async () => { "dest": "/api/body-size", "src": "/api/body-size", }, + { + "dest": "/api/app-fetch", + "src": "/api/app-fetch", + }, { "dest": "/500", "src": "/500", @@ -568,6 +572,7 @@ describe("nitro:preset:vercel:web", async () => { "functions/__server.func", "functions/_vercel", "functions/_ws.func (symlink)", + "functions/api/app-fetch.func (symlink)", "functions/api/body-size.func (symlink)", "functions/api/cached.func (symlink)", "functions/api/db.func (symlink)", diff --git a/test/tests.ts b/test/tests.ts index afed84bacb..ea51ae1a35 100644 --- a/test/tests.ts +++ b/test/tests.ts @@ -255,6 +255,11 @@ export function testNitro( expect(headers["x-srvx-plugin"]).toBe("works"); }); + it("useNitroApp().fetch applies server entry middleware and plugins", async () => { + const { data } = await callHandler({ url: "/api/app-fetch" }); + expect(data).toEqual({ body: "server entry middleware works!", plugin: "works" }); + }); + it.runIf(["bun", "deno-server", "nitro-dev"].includes(ctx.preset))( "Server entry options are passed to srvx", async () => { From f5c0b96a711128f3ae1fd70bb606631d0764c1a5 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Tue, 15 Sep 2026 09:13:38 +0000 Subject: [PATCH 4/6] fix: use srvx server middleware for useNitroApp().fetch on server presets Avoids running entry plugins twice on node, bun, deno and nitro-dev, caches plugin failures and reads error after plugins. --- docs/1.docs/6.server-entry.md | 8 ++- src/presets/_nitro/runtime/nitro-dev.ts | 3 +- src/presets/bun/runtime/bun.ts | 3 +- src/presets/deno/runtime/deno-server.ts | 4 +- src/presets/node/runtime/node-cluster.ts | 3 +- src/presets/node/runtime/node-server.ts | 6 +- src/runtime/internal/serve.ts | 65 ++++++++++++++------- test/fixture/server.ts | 4 +- test/fixture/server/routes/api/app-fetch.ts | 7 ++- test/fixture/server/utils/srvx-plugin.ts | 1 + test/minimal/minimal.test.ts | 2 +- test/presets/node.test.ts | 6 ++ test/tests.ts | 6 +- 13 files changed, 84 insertions(+), 34 deletions(-) create mode 100644 test/fixture/server/utils/srvx-plugin.ts diff --git a/docs/1.docs/6.server-entry.md b/docs/1.docs/6.server-entry.md index b59dcf2d0f..f55b0870e3 100644 --- a/docs/1.docs/6.server-entry.md +++ b/docs/1.docs/6.server-entry.md @@ -137,7 +137,7 @@ For Node.js frameworks that use `(req, res)` style handlers (like [Express](http ## Server options -When the server entry's default export is a plain object, every property other than `fetch` is passed as-is to the [srvx](https://srvx.h3.dev/) server started by the [Node.js](/deploy/runtimes/node), [Bun](/deploy/runtimes/bun) and [Deno](/deploy/runtimes/deno) presets. This gives you control over the server itself: `middleware` and `plugins` that run for every request (before Nitro), `tls`, `maxRequestBodySize`, `trustProxy`, `gracefulShutdown`, runtime specific settings (`node`, `bun`, `deno`), and so on. +When the server entry's default export is a plain object, every property other than `fetch` is a [srvx](https://srvx.h3.dev/) server option. This gives you control over the server itself: `middleware` and `plugins` that run for every request (before Nitro), an `error` handler, `tls`, `maxRequestBodySize`, `trustProxy`, `gracefulShutdown`, runtime specific settings (`node`, `bun`, `deno`), and so on. Use the `defineServerEntry` helper for typed options: @@ -163,11 +163,15 @@ export default defineServerEntry({ ::read-more{to="https://srvx.h3.dev/guide/options" title="srvx server options"} :: -`middleware`, `plugins` and `error` are applied by every preset, including serverless, edge and worker runtimes that do not start a server, and by `useNitroApp().fetch()`. Listener and runtime specific options (such as `port`, `tls`, `maxRequestBodySize`, `trustProxy`, `gracefulShutdown`, `node`, `bun` and `deno`) only apply to the Node.js, Bun and Deno servers. +The [Node.js](/deploy/runtimes/node), [Bun](/deploy/runtimes/bun) and [Deno](/deploy/runtimes/deno) presets pass all options to the srvx server they start. + +`middleware`, `plugins` and `error` are also applied by every other preset, including serverless, edge and worker runtimes that do not start a server, and by direct `useNitroApp().fetch()` calls. Other options (such as `port`, `tls`, `maxRequestBodySize`, `trustProxy`, `gracefulShutdown`, `node`, `bun` and `deno`) have no effect there. ::note - `NITRO_PORT`/`PORT`, `NITRO_HOST`/`HOST` and `NITRO_SSL_CERT`/`NITRO_SSL_KEY` environment variables take precedence over the `port`, `hostname` and `tls` options, so the server stays configurable at runtime. - During development (`nitro dev`), options are applied to the dev worker server, except listener options (`port`, `hostname`, `protocol`, `tls`, `silent`, `gracefulShutdown`) which are controlled by the dev server and CLI (`--port`, `--host`). The Vite dev server only applies `middleware`, `plugins` and `error`. +- On presets that do not start a server, plugins are called once, on the first request, with a minimal server object that only has `runtime` (`"generic"`) and `options`. Plugins that depend on a specific runtime adapter (such as `srvx/mtls`, which requires Node.js) only work on presets that start that server. +- Internal requests (`serverFetch()`, `fetch("/...")` from server code) and WebSocket upgrades do not run server entry `middleware`. - `manual` is not supported: presets start listening immediately. - Options are only read from plain object exports (not from framework instances like `export default app`) and never from Node.js format entries (`server.node.ts`). :: diff --git a/src/presets/_nitro/runtime/nitro-dev.ts b/src/presets/_nitro/runtime/nitro-dev.ts index ab6235bbf0..3eb2fe300b 100644 --- a/src/presets/_nitro/runtime/nitro-dev.ts +++ b/src/presets/_nitro/runtime/nitro-dev.ts @@ -4,6 +4,7 @@ import { useNitroApp, useNitroHooks } from "nitro/app"; import { startScheduleRunner } from "#nitro/runtime/task"; import { trapUnhandledErrors } from "#nitro/runtime/error/hooks"; import { resolveWebsocketHooks } from "#nitro/runtime/app"; +import { appFetchPlugin } from "#nitro/runtime/serve"; import { tracingSrvxPlugins } from "#nitro/virtual/tracing"; import { serverEntryOptions } from "#nitro/virtual/server-entry"; @@ -28,7 +29,7 @@ const ws = import.meta._websocket export default { ...serverEntryOptions, fetch: nitroApp["~fetch"], - plugins: [...tracingSrvxPlugins, ...(serverEntryOptions.plugins || [])], + plugins: [...tracingSrvxPlugins, ...(serverEntryOptions.plugins || []), appFetchPlugin(nitroApp)], upgrade: ws ? (context: { node: { req: any; socket: any; head: any } }) => { ws.handleUpgrade(context.node.req, context.node.socket, context.node.head); diff --git a/src/presets/bun/runtime/bun.ts b/src/presets/bun/runtime/bun.ts index 7daca4b835..75f051f145 100644 --- a/src/presets/bun/runtime/bun.ts +++ b/src/presets/bun/runtime/bun.ts @@ -7,7 +7,7 @@ import { useNitroApp } from "nitro/app"; import { startScheduleRunner } from "#nitro/runtime/task"; import { trapUnhandledErrors } from "#nitro/runtime/error/hooks"; import { resolveWebsocketHooks } from "#nitro/runtime/app"; -import { resolveServeOptions } from "#nitro/runtime/serve"; +import { appFetchPlugin, resolveServeOptions } from "#nitro/runtime/serve"; import { setupCloseHooks } from "#nitro/runtime/shutdown"; const nitroApp = useNitroApp(); @@ -28,6 +28,7 @@ if (import.meta._websocket) { const server = serve( resolveServeOptions({ fetch: _fetch, + plugins: [appFetchPlugin(nitroApp)], ...(import.meta._websocket ? { bun: { websocket: ws!.websocket } } : {}), }) ); diff --git a/src/presets/deno/runtime/deno-server.ts b/src/presets/deno/runtime/deno-server.ts index 9b3650865a..047a6fcee6 100644 --- a/src/presets/deno/runtime/deno-server.ts +++ b/src/presets/deno/runtime/deno-server.ts @@ -7,7 +7,7 @@ import { useNitroApp } from "nitro/app"; import { startScheduleRunner } from "#nitro/runtime/task"; import { trapUnhandledErrors } from "#nitro/runtime/error/hooks"; import { resolveWebsocketHooks } from "#nitro/runtime/app"; -import { resolveServeOptions } from "#nitro/runtime/serve"; +import { appFetchPlugin, resolveServeOptions } from "#nitro/runtime/serve"; import { setupCloseHooks } from "#nitro/runtime/shutdown"; const nitroApp = useNitroApp(); @@ -24,7 +24,7 @@ if (import.meta._websocket) { }; } -const server = serve(resolveServeOptions({ fetch: _fetch })); +const server = serve(resolveServeOptions({ fetch: _fetch, plugins: [appFetchPlugin(nitroApp)] })); setupCloseHooks(server); diff --git a/src/presets/node/runtime/node-cluster.ts b/src/presets/node/runtime/node-cluster.ts index accb1ae69e..489bff12c9 100644 --- a/src/presets/node/runtime/node-cluster.ts +++ b/src/presets/node/runtime/node-cluster.ts @@ -7,7 +7,7 @@ import { useNitroApp } from "nitro/app"; import { startScheduleRunner } from "#nitro/runtime/task"; import { trapUnhandledErrors } from "#nitro/runtime/error/hooks"; import { resolveWebsocketHooks } from "#nitro/runtime/app"; -import { resolveServeOptions } from "#nitro/runtime/serve"; +import { appFetchPlugin, resolveServeOptions } from "#nitro/runtime/serve"; import { setupCloseHooks } from "#nitro/runtime/shutdown"; const clusterId = cluster.isWorker && process.env.WORKER_ID; @@ -20,6 +20,7 @@ const nitroApp = useNitroApp(); const server = serve( resolveServeOptions({ fetch: nitroApp["~fetch"], + plugins: [appFetchPlugin(nitroApp)], node: { exclusive: false }, ...(clusterId && clusterId !== "1" ? { silent: true } : {}), }) diff --git a/src/presets/node/runtime/node-server.ts b/src/presets/node/runtime/node-server.ts index d0d188c041..0df4b1157b 100644 --- a/src/presets/node/runtime/node-server.ts +++ b/src/presets/node/runtime/node-server.ts @@ -6,12 +6,14 @@ import { useNitroApp } from "nitro/app"; import { startScheduleRunner } from "#nitro/runtime/task"; import { trapUnhandledErrors } from "#nitro/runtime/error/hooks"; import { resolveWebsocketHooks } from "#nitro/runtime/app"; -import { resolveServeOptions } from "#nitro/runtime/serve"; +import { appFetchPlugin, resolveServeOptions } from "#nitro/runtime/serve"; import { setupCloseHooks } from "#nitro/runtime/shutdown"; const nitroApp = useNitroApp(); -const server = serve(resolveServeOptions({ fetch: nitroApp["~fetch"] })); +const server = serve( + resolveServeOptions({ fetch: nitroApp["~fetch"], plugins: [appFetchPlugin(nitroApp)] }) +); if (import.meta._websocket) { const { handleUpgrade } = wsAdapter({ resolve: resolveWebsocketHooks }); diff --git a/src/runtime/internal/serve.ts b/src/runtime/internal/serve.ts index 216e1a7837..18392120e8 100644 --- a/src/runtime/internal/serve.ts +++ b/src/runtime/internal/serve.ts @@ -1,4 +1,5 @@ -import type { Server, ServerHandler, ServerOptions, ServerRequest } from "srvx"; +import type { NitroApp } from "nitro/types"; +import type { Server, ServerHandler, ServerOptions, ServerPlugin, ServerRequest } from "srvx"; import { serverEntryOptions } from "#nitro/virtual/server-entry"; import { tracingSrvxPlugins } from "#nitro/virtual/tracing"; @@ -36,11 +37,24 @@ export function resolveServeOptions(opts: ServerOptions): ServerOptions { return resolved; } +/** + * srvx plugin for presets starting a srvx server: points `nitroApp.fetch` to the server middleware + * (including middleware added by plugins) around its fetch handler, so direct `nitroApp.fetch()` calls + * get the same options without running plugins again. + */ +export function appFetchPlugin(nitroApp: NitroApp): ServerPlugin { + return (server) => { + let handler: ServerHandler | undefined; + // Bun and Deno pass `error` to the native server, the Node.js adapter registers it as middleware. + nitroApp.fetch = (req) => (handler ??= composeFetch(server, server.runtime !== "node"))(req); + }; +} + /** * Apply server entry `middleware`, `plugins` and `error` options to the Nitro app fetch handler. * * Plugins are called with a minimal server object (`runtime` and `options`) on first request. - * Presets starting a srvx server use the raw handler (`nitroApp["~fetch"]`) and pass options to srvx instead. + * Presets starting a srvx server replace it using {@link appFetchPlugin}. */ export function withServerEntryOptions< T extends (req: ServerRequest) => Response | Promise, @@ -50,36 +64,45 @@ export function withServerEntryOptions< return fetch; } let handler: ServerHandler | undefined; - return ((req: ServerRequest) => (handler ??= composeHandler(fetch))(req)) as T; + return ((req: ServerRequest) => (handler ??= createGenericFetch(fetch))(req)) as T; } -function composeHandler(fetch: ServerHandler): ServerHandler { - const { middleware, plugins, error } = serverEntryOptions; - +function createGenericFetch(fetch: ServerHandler): ServerHandler { const server = { runtime: "generic", - options: { ...serverEntryOptions, fetch, middleware: [...(middleware || [])] }, + options: { + ...serverEntryOptions, + fetch, + middleware: [...(serverEntryOptions.middleware || [])], + }, } as unknown as Server; - - for (const plugin of plugins || []) { - plugin(server); + try { + for (const plugin of serverEntryOptions.plugins || []) { + plugin(server); + } + } catch (error) { + return () => Promise.reject(error); } + return composeFetch(server, true); +} - if (error) { - server.options.middleware.unshift(async (_req, next) => { +function composeFetch(server: Server, withError: boolean): ServerHandler { + const { middleware, error } = server.options; + let handler = server.options.fetch; + for (let i = middleware.length - 1; i >= 0; i--) { + const mw = middleware[i]!; + const next = handler; + handler = (req) => mw(req, () => next(req)); + } + if (withError && error) { + const next = handler; + handler = async (req) => { try { - return await next(); + return await next(req); } catch (error_) { return error(error_); } - }); - } - - let handler = server.options.fetch; - for (let i = server.options.middleware.length - 1; i >= 0; i--) { - const mw = server.options.middleware[i]!; - const next = handler; - handler = (req) => mw(req, () => next(req)); + }; } return handler; } diff --git a/test/fixture/server.ts b/test/fixture/server.ts index 6c0b9f22ce..6d5879dffd 100644 --- a/test/fixture/server.ts +++ b/test/fixture/server.ts @@ -1,4 +1,5 @@ import { defineServerEntry } from "nitro"; +import { srvxPluginRuns } from "./server/utils/srvx-plugin.ts"; export default defineServerEntry({ async fetch(req) { @@ -21,10 +22,11 @@ export default defineServerEntry({ ], plugins: [ (server) => { + srvxPluginRuns.count++; server.options.middleware.unshift(async (req, next) => { const res = await next(); if (new URL(req.url).pathname === "/srvx-middleware") { - res.headers.set("x-srvx-plugin", "works"); + res.headers.append("x-srvx-plugin", "works"); } return res; }); diff --git a/test/fixture/server/routes/api/app-fetch.ts b/test/fixture/server/routes/api/app-fetch.ts index 1492ab073b..a3c9fcb3ba 100644 --- a/test/fixture/server/routes/api/app-fetch.ts +++ b/test/fixture/server/routes/api/app-fetch.ts @@ -1,7 +1,12 @@ import { defineHandler } from "nitro"; import { useNitroApp } from "nitro/app"; +import { srvxPluginRuns } from "../../utils/srvx-plugin.ts"; export default defineHandler(async () => { const res = await useNitroApp().fetch(new Request("http://localhost/srvx-middleware")); - return { body: await res.text(), plugin: res.headers.get("x-srvx-plugin") }; + return { + body: await res.text(), + plugin: res.headers.get("x-srvx-plugin"), + pluginRuns: srvxPluginRuns.count, + }; }); diff --git a/test/fixture/server/utils/srvx-plugin.ts b/test/fixture/server/utils/srvx-plugin.ts new file mode 100644 index 0000000000..e7ff727e90 --- /dev/null +++ b/test/fixture/server/utils/srvx-plugin.ts @@ -0,0 +1 @@ +export const srvxPluginRuns = { count: 0 }; diff --git a/test/minimal/minimal.test.ts b/test/minimal/minimal.test.ts index 4d369d726d..9549f06fa0 100644 --- a/test/minimal/minimal.test.ts +++ b/test/minimal/minimal.test.ts @@ -11,7 +11,7 @@ const tmpDir = fileURLToPath(new URL(".tmp", import.meta.url)); // Rounded up const bundleSizes: Record = { rollup: [19, 10], - rolldown: [19, 10], + rolldown: [20, 10], vite: [19, 10], vite7: [19, 10], }; diff --git a/test/presets/node.test.ts b/test/presets/node.test.ts index 1b8c39d3aa..eede138501 100644 --- a/test/presets/node.test.ts +++ b/test/presets/node.test.ts @@ -61,6 +61,12 @@ describe("nitro:preset:node-server", async () => { const res = await fetch(`http://127.0.0.1:${port}/srvx-middleware`); expect(await res.text()).toBe("server entry middleware works!"); expect(res.headers.get("x-srvx-plugin")).toBe("works"); + const appFetch = await fetch(`http://127.0.0.1:${port}/api/app-fetch`); + expect(await appFetch.json()).toEqual({ + body: "server entry middleware works!", + plugin: "works", + pluginRuns: 1, + }); const large = await fetch(`http://127.0.0.1:${port}/api/body-size`, { method: "POST", body: "x".repeat(128 * 1024), diff --git a/test/tests.ts b/test/tests.ts index ea51ae1a35..fa7a034753 100644 --- a/test/tests.ts +++ b/test/tests.ts @@ -257,7 +257,11 @@ export function testNitro( it("useNitroApp().fetch applies server entry middleware and plugins", async () => { const { data } = await callHandler({ url: "/api/app-fetch" }); - expect(data).toEqual({ body: "server entry middleware works!", plugin: "works" }); + expect(data).toEqual({ + body: "server entry middleware works!", + plugin: "works", + pluginRuns: 1, + }); }); it.runIf(["bun", "deno-server", "nitro-dev"].includes(ctx.preset))( From fc9be05e91f7ef8eca3cfafc425b4386fdbba16c Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Tue, 15 Sep 2026 11:09:08 +0000 Subject: [PATCH 5/6] refactor: register app fetch plugin in resolveServeOptions --- src/build/virtual/app.ts | 2 +- src/presets/_nitro/runtime/nitro-dev.ts | 2 +- src/presets/bun/runtime/bun.ts | 3 +- src/presets/deno/runtime/deno-server.ts | 4 +- src/presets/node/runtime/node-cluster.ts | 3 +- src/presets/node/runtime/node-server.ts | 6 +- src/runtime/internal/app-fetch.ts | 73 ++++++++++++++++++++ src/runtime/internal/serve.ts | 84 ++++-------------------- 8 files changed, 92 insertions(+), 85 deletions(-) create mode 100644 src/runtime/internal/app-fetch.ts diff --git a/src/build/virtual/app.ts b/src/build/virtual/app.ts index 8bb5e34b39..248f725cc9 100644 --- a/src/build/virtual/app.ts +++ b/src/build/virtual/app.ts @@ -106,7 +106,7 @@ export default function app(nitro: Nitro) { } if (hasServerEntry) { - imports.push(`import { withServerEntryOptions } from "#nitro/runtime/serve";`); + imports.push(`import { withServerEntryOptions } from "#nitro/runtime/app-fetch";`); } code.push( diff --git a/src/presets/_nitro/runtime/nitro-dev.ts b/src/presets/_nitro/runtime/nitro-dev.ts index 3eb2fe300b..3f1c366c31 100644 --- a/src/presets/_nitro/runtime/nitro-dev.ts +++ b/src/presets/_nitro/runtime/nitro-dev.ts @@ -4,7 +4,7 @@ import { useNitroApp, useNitroHooks } from "nitro/app"; import { startScheduleRunner } from "#nitro/runtime/task"; import { trapUnhandledErrors } from "#nitro/runtime/error/hooks"; import { resolveWebsocketHooks } from "#nitro/runtime/app"; -import { appFetchPlugin } from "#nitro/runtime/serve"; +import { appFetchPlugin } from "#nitro/runtime/app-fetch"; import { tracingSrvxPlugins } from "#nitro/virtual/tracing"; import { serverEntryOptions } from "#nitro/virtual/server-entry"; diff --git a/src/presets/bun/runtime/bun.ts b/src/presets/bun/runtime/bun.ts index 75f051f145..7daca4b835 100644 --- a/src/presets/bun/runtime/bun.ts +++ b/src/presets/bun/runtime/bun.ts @@ -7,7 +7,7 @@ import { useNitroApp } from "nitro/app"; import { startScheduleRunner } from "#nitro/runtime/task"; import { trapUnhandledErrors } from "#nitro/runtime/error/hooks"; import { resolveWebsocketHooks } from "#nitro/runtime/app"; -import { appFetchPlugin, resolveServeOptions } from "#nitro/runtime/serve"; +import { resolveServeOptions } from "#nitro/runtime/serve"; import { setupCloseHooks } from "#nitro/runtime/shutdown"; const nitroApp = useNitroApp(); @@ -28,7 +28,6 @@ if (import.meta._websocket) { const server = serve( resolveServeOptions({ fetch: _fetch, - plugins: [appFetchPlugin(nitroApp)], ...(import.meta._websocket ? { bun: { websocket: ws!.websocket } } : {}), }) ); diff --git a/src/presets/deno/runtime/deno-server.ts b/src/presets/deno/runtime/deno-server.ts index 047a6fcee6..9b3650865a 100644 --- a/src/presets/deno/runtime/deno-server.ts +++ b/src/presets/deno/runtime/deno-server.ts @@ -7,7 +7,7 @@ import { useNitroApp } from "nitro/app"; import { startScheduleRunner } from "#nitro/runtime/task"; import { trapUnhandledErrors } from "#nitro/runtime/error/hooks"; import { resolveWebsocketHooks } from "#nitro/runtime/app"; -import { appFetchPlugin, resolveServeOptions } from "#nitro/runtime/serve"; +import { resolveServeOptions } from "#nitro/runtime/serve"; import { setupCloseHooks } from "#nitro/runtime/shutdown"; const nitroApp = useNitroApp(); @@ -24,7 +24,7 @@ if (import.meta._websocket) { }; } -const server = serve(resolveServeOptions({ fetch: _fetch, plugins: [appFetchPlugin(nitroApp)] })); +const server = serve(resolveServeOptions({ fetch: _fetch })); setupCloseHooks(server); diff --git a/src/presets/node/runtime/node-cluster.ts b/src/presets/node/runtime/node-cluster.ts index 489bff12c9..accb1ae69e 100644 --- a/src/presets/node/runtime/node-cluster.ts +++ b/src/presets/node/runtime/node-cluster.ts @@ -7,7 +7,7 @@ import { useNitroApp } from "nitro/app"; import { startScheduleRunner } from "#nitro/runtime/task"; import { trapUnhandledErrors } from "#nitro/runtime/error/hooks"; import { resolveWebsocketHooks } from "#nitro/runtime/app"; -import { appFetchPlugin, resolveServeOptions } from "#nitro/runtime/serve"; +import { resolveServeOptions } from "#nitro/runtime/serve"; import { setupCloseHooks } from "#nitro/runtime/shutdown"; const clusterId = cluster.isWorker && process.env.WORKER_ID; @@ -20,7 +20,6 @@ const nitroApp = useNitroApp(); const server = serve( resolveServeOptions({ fetch: nitroApp["~fetch"], - plugins: [appFetchPlugin(nitroApp)], node: { exclusive: false }, ...(clusterId && clusterId !== "1" ? { silent: true } : {}), }) diff --git a/src/presets/node/runtime/node-server.ts b/src/presets/node/runtime/node-server.ts index 0df4b1157b..d0d188c041 100644 --- a/src/presets/node/runtime/node-server.ts +++ b/src/presets/node/runtime/node-server.ts @@ -6,14 +6,12 @@ import { useNitroApp } from "nitro/app"; import { startScheduleRunner } from "#nitro/runtime/task"; import { trapUnhandledErrors } from "#nitro/runtime/error/hooks"; import { resolveWebsocketHooks } from "#nitro/runtime/app"; -import { appFetchPlugin, resolveServeOptions } from "#nitro/runtime/serve"; +import { resolveServeOptions } from "#nitro/runtime/serve"; import { setupCloseHooks } from "#nitro/runtime/shutdown"; const nitroApp = useNitroApp(); -const server = serve( - resolveServeOptions({ fetch: nitroApp["~fetch"], plugins: [appFetchPlugin(nitroApp)] }) -); +const server = serve(resolveServeOptions({ fetch: nitroApp["~fetch"] })); if (import.meta._websocket) { const { handleUpgrade } = wsAdapter({ resolve: resolveWebsocketHooks }); diff --git a/src/runtime/internal/app-fetch.ts b/src/runtime/internal/app-fetch.ts new file mode 100644 index 0000000000..4da82939f4 --- /dev/null +++ b/src/runtime/internal/app-fetch.ts @@ -0,0 +1,73 @@ +import type { NitroApp } from "nitro/types"; +import type { Server, ServerHandler, ServerPlugin, ServerRequest } from "srvx"; +import { serverEntryOptions } from "#nitro/virtual/server-entry"; + +/** + * srvx plugin for presets starting a srvx server: points `nitroApp.fetch` to the server middleware + * (including middleware added by plugins) around its fetch handler, so direct `nitroApp.fetch()` calls + * get the same options without running plugins again. + */ +export function appFetchPlugin(nitroApp: NitroApp): ServerPlugin { + return (server) => { + let handler: ServerHandler | undefined; + // Bun and Deno pass `error` to the native server, the Node.js adapter registers it as middleware. + nitroApp.fetch = (req) => (handler ??= composeFetch(server, server.runtime !== "node"))(req); + }; +} + +/** + * Apply server entry `middleware`, `plugins` and `error` options to the Nitro app fetch handler. + * + * Plugins are called with a minimal server object (`runtime` and `options`) on first request. + * Presets starting a srvx server replace it using {@link appFetchPlugin}. + */ +export function withServerEntryOptions< + T extends (req: ServerRequest) => Response | Promise, +>(fetch: T): T { + const { middleware, plugins, error } = serverEntryOptions; + if (!middleware?.length && !plugins?.length && !error) { + return fetch; + } + let handler: ServerHandler | undefined; + return ((req: ServerRequest) => (handler ??= createGenericFetch(fetch))(req)) as T; +} + +function createGenericFetch(fetch: ServerHandler): ServerHandler { + const server = { + runtime: "generic", + options: { + ...serverEntryOptions, + fetch, + middleware: [...(serverEntryOptions.middleware || [])], + }, + } as unknown as Server; + try { + for (const plugin of serverEntryOptions.plugins || []) { + plugin(server); + } + } catch (error) { + return () => Promise.reject(error); + } + return composeFetch(server, true); +} + +function composeFetch(server: Server, withError: boolean): ServerHandler { + const { middleware, error } = server.options; + let handler = server.options.fetch; + for (let i = middleware.length - 1; i >= 0; i--) { + const mw = middleware[i]!; + const next = handler; + handler = (req) => mw(req, () => next(req)); + } + if (withError && error) { + const next = handler; + handler = async (req) => { + try { + return await next(req); + } catch (error_) { + return error(error_); + } + }; + } + return handler; +} diff --git a/src/runtime/internal/serve.ts b/src/runtime/internal/serve.ts index 18392120e8..9de0c09539 100644 --- a/src/runtime/internal/serve.ts +++ b/src/runtime/internal/serve.ts @@ -1,7 +1,8 @@ -import type { NitroApp } from "nitro/types"; -import type { Server, ServerHandler, ServerOptions, ServerPlugin, ServerRequest } from "srvx"; +import type { ServerOptions } from "srvx"; import { serverEntryOptions } from "#nitro/virtual/server-entry"; import { tracingSrvxPlugins } from "#nitro/virtual/tracing"; +import { useNitroApp } from "./app.ts"; +import { appFetchPlugin } from "./app-fetch.ts"; /** * Resolve srvx `serve()` options for a server preset. @@ -9,6 +10,8 @@ import { tracingSrvxPlugins } from "#nitro/virtual/tracing"; * Options exported from the server entry (`export default { fetch, ...options }`) are the base. * `NITRO_PORT`/`PORT`, `NITRO_HOST`/`HOST` and `NITRO_SSL_CERT`/`NITRO_SSL_KEY` take precedence * over them, and the preset's own options (`fetch` and runtime specific settings) win last. + * + * `useNitroApp().fetch` is pointed to the started server's middleware (see {@link appFetchPlugin}). */ export function resolveServeOptions(opts: ServerOptions): ServerOptions { const { port, hostname, tls, plugins, ...entryOptions } = serverEntryOptions; @@ -25,7 +28,12 @@ export function resolveServeOptions(opts: ServerOptions): ServerOptions { hostname: env.NITRO_HOST || env.HOST || hostname, tls: cert && key ? { cert, key } : tls, ...opts, - plugins: [...tracingSrvxPlugins, ...(plugins || []), ...(opts.plugins || [])], + plugins: [ + ...tracingSrvxPlugins, + ...(plugins || []), + ...(opts.plugins || []), + appFetchPlugin(useNitroApp()), + ], }; for (const runtime of ["node", "bun", "deno"] as const) { @@ -36,73 +44,3 @@ export function resolveServeOptions(opts: ServerOptions): ServerOptions { return resolved; } - -/** - * srvx plugin for presets starting a srvx server: points `nitroApp.fetch` to the server middleware - * (including middleware added by plugins) around its fetch handler, so direct `nitroApp.fetch()` calls - * get the same options without running plugins again. - */ -export function appFetchPlugin(nitroApp: NitroApp): ServerPlugin { - return (server) => { - let handler: ServerHandler | undefined; - // Bun and Deno pass `error` to the native server, the Node.js adapter registers it as middleware. - nitroApp.fetch = (req) => (handler ??= composeFetch(server, server.runtime !== "node"))(req); - }; -} - -/** - * Apply server entry `middleware`, `plugins` and `error` options to the Nitro app fetch handler. - * - * Plugins are called with a minimal server object (`runtime` and `options`) on first request. - * Presets starting a srvx server replace it using {@link appFetchPlugin}. - */ -export function withServerEntryOptions< - T extends (req: ServerRequest) => Response | Promise, ->(fetch: T): T { - const { middleware, plugins, error } = serverEntryOptions; - if (!middleware?.length && !plugins?.length && !error) { - return fetch; - } - let handler: ServerHandler | undefined; - return ((req: ServerRequest) => (handler ??= createGenericFetch(fetch))(req)) as T; -} - -function createGenericFetch(fetch: ServerHandler): ServerHandler { - const server = { - runtime: "generic", - options: { - ...serverEntryOptions, - fetch, - middleware: [...(serverEntryOptions.middleware || [])], - }, - } as unknown as Server; - try { - for (const plugin of serverEntryOptions.plugins || []) { - plugin(server); - } - } catch (error) { - return () => Promise.reject(error); - } - return composeFetch(server, true); -} - -function composeFetch(server: Server, withError: boolean): ServerHandler { - const { middleware, error } = server.options; - let handler = server.options.fetch; - for (let i = middleware.length - 1; i >= 0; i--) { - const mw = middleware[i]!; - const next = handler; - handler = (req) => mw(req, () => next(req)); - } - if (withError && error) { - const next = handler; - handler = async (req) => { - try { - return await next(req); - } catch (error_) { - return error(error_); - } - }; - } - return handler; -} From d117bdb15511985cd36583dd5f1c1799ce3bdec4 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Tue, 15 Sep 2026 11:14:29 +0000 Subject: [PATCH 6/6] refactor: move appFetchPlugin to serve runtime --- src/presets/_nitro/runtime/nitro-dev.ts | 4 ++-- src/runtime/internal/app-fetch.ts | 20 +++----------------- src/runtime/internal/serve.ts | 22 ++++++++++++++-------- 3 files changed, 19 insertions(+), 27 deletions(-) diff --git a/src/presets/_nitro/runtime/nitro-dev.ts b/src/presets/_nitro/runtime/nitro-dev.ts index 3f1c366c31..4d23cd9adb 100644 --- a/src/presets/_nitro/runtime/nitro-dev.ts +++ b/src/presets/_nitro/runtime/nitro-dev.ts @@ -4,7 +4,7 @@ import { useNitroApp, useNitroHooks } from "nitro/app"; import { startScheduleRunner } from "#nitro/runtime/task"; import { trapUnhandledErrors } from "#nitro/runtime/error/hooks"; import { resolveWebsocketHooks } from "#nitro/runtime/app"; -import { appFetchPlugin } from "#nitro/runtime/app-fetch"; +import { appFetchPlugin } from "#nitro/runtime/serve"; import { tracingSrvxPlugins } from "#nitro/virtual/tracing"; import { serverEntryOptions } from "#nitro/virtual/server-entry"; @@ -29,7 +29,7 @@ const ws = import.meta._websocket export default { ...serverEntryOptions, fetch: nitroApp["~fetch"], - plugins: [...tracingSrvxPlugins, ...(serverEntryOptions.plugins || []), appFetchPlugin(nitroApp)], + plugins: [...tracingSrvxPlugins, ...(serverEntryOptions.plugins || []), appFetchPlugin], upgrade: ws ? (context: { node: { req: any; socket: any; head: any } }) => { ws.handleUpgrade(context.node.req, context.node.socket, context.node.head); diff --git a/src/runtime/internal/app-fetch.ts b/src/runtime/internal/app-fetch.ts index 4da82939f4..b747f2586f 100644 --- a/src/runtime/internal/app-fetch.ts +++ b/src/runtime/internal/app-fetch.ts @@ -1,25 +1,11 @@ -import type { NitroApp } from "nitro/types"; -import type { Server, ServerHandler, ServerPlugin, ServerRequest } from "srvx"; +import type { Server, ServerHandler, ServerRequest } from "srvx"; import { serverEntryOptions } from "#nitro/virtual/server-entry"; -/** - * srvx plugin for presets starting a srvx server: points `nitroApp.fetch` to the server middleware - * (including middleware added by plugins) around its fetch handler, so direct `nitroApp.fetch()` calls - * get the same options without running plugins again. - */ -export function appFetchPlugin(nitroApp: NitroApp): ServerPlugin { - return (server) => { - let handler: ServerHandler | undefined; - // Bun and Deno pass `error` to the native server, the Node.js adapter registers it as middleware. - nitroApp.fetch = (req) => (handler ??= composeFetch(server, server.runtime !== "node"))(req); - }; -} - /** * Apply server entry `middleware`, `plugins` and `error` options to the Nitro app fetch handler. * * Plugins are called with a minimal server object (`runtime` and `options`) on first request. - * Presets starting a srvx server replace it using {@link appFetchPlugin}. + * Presets starting a srvx server replace it (see `appFetchPlugin`). */ export function withServerEntryOptions< T extends (req: ServerRequest) => Response | Promise, @@ -51,7 +37,7 @@ function createGenericFetch(fetch: ServerHandler): ServerHandler { return composeFetch(server, true); } -function composeFetch(server: Server, withError: boolean): ServerHandler { +export function composeFetch(server: Server, withError: boolean): ServerHandler { const { middleware, error } = server.options; let handler = server.options.fetch; for (let i = middleware.length - 1; i >= 0; i--) { diff --git a/src/runtime/internal/serve.ts b/src/runtime/internal/serve.ts index 9de0c09539..2b517a43a8 100644 --- a/src/runtime/internal/serve.ts +++ b/src/runtime/internal/serve.ts @@ -1,8 +1,8 @@ -import type { ServerOptions } from "srvx"; +import type { ServerHandler, ServerOptions, ServerPlugin } from "srvx"; import { serverEntryOptions } from "#nitro/virtual/server-entry"; import { tracingSrvxPlugins } from "#nitro/virtual/tracing"; import { useNitroApp } from "./app.ts"; -import { appFetchPlugin } from "./app-fetch.ts"; +import { composeFetch } from "./app-fetch.ts"; /** * Resolve srvx `serve()` options for a server preset. @@ -28,12 +28,7 @@ export function resolveServeOptions(opts: ServerOptions): ServerOptions { hostname: env.NITRO_HOST || env.HOST || hostname, tls: cert && key ? { cert, key } : tls, ...opts, - plugins: [ - ...tracingSrvxPlugins, - ...(plugins || []), - ...(opts.plugins || []), - appFetchPlugin(useNitroApp()), - ], + plugins: [...tracingSrvxPlugins, ...(plugins || []), ...(opts.plugins || []), appFetchPlugin], }; for (const runtime of ["node", "bun", "deno"] as const) { @@ -44,3 +39,14 @@ export function resolveServeOptions(opts: ServerOptions): ServerOptions { return resolved; } + +/** + * srvx plugin for presets starting a srvx server: points `useNitroApp().fetch` to the server middleware + * (including middleware added by plugins) around its fetch handler, so direct calls get the same + * options without running plugins again. + */ +export const appFetchPlugin: ServerPlugin = (server) => { + let handler: ServerHandler | undefined; + // Bun and Deno pass `error` to the native server, the Node.js adapter registers it as middleware. + useNitroApp().fetch = (req) => (handler ??= composeFetch(server, server.runtime !== "node"))(req); +};