Middleware and route handlers are compiled into a single function with compileHandler (src/http/handler/compile.ts).
export function compileHandler<T = object>(...handlers: Handler<T>[]): CompiledHandler<T>0handlers →(_ctx, next) => next?.()(compile.ts:8-10).1handler → returned as-is, unwrapped (compile.ts:12-14) — a single synchronous handler stays synchronous, which matters forServer.run's promise check.>1handlers → code generation: a source string is built and instantiated withnew Function(...params, source)(...handlers)(compile.ts:44-46).
For h0, h1, h2:
var nextCalled = false;
function advance(){nextCalled=true;}
var r0=h0(ctx,advance);if(r0!=null&&typeof r0.then==="function")await r0;
if(!nextCalled)return;nextCalled=false;
var r1=h1(ctx,advance);if(r1!=null&&typeof r1.then==="function")await r1;
if(!nextCalled)return;nextCalled=false;
var r2=h2(ctx,advance);if(r2!=null&&typeof r2.then==="function")await r2;
if(nextCalled)return next?.();- Conditional await — a handler is
awaited only when it actually returned a thenable (compile.ts:31-33). Sync handlers pay no microtask per request. This is the single most important performance decision in the pipeline. - Shared
next—advance()is one closure that flips a flag; no per-call function allocation. - Short-circuit —
if(!nextCalled)return;before each handler: if any handler never callednext(), the chain stops. - Tail propagation — after the last handler,
if(nextCalled)return next?.();forwards to the outernext. For top-level routes that outernextisNO_OP(http/server/constants.ts:1). - Minimal source —
var, declared function, minimal indentation (compile.ts:16-17,43) so the generated function stays small.
Next = () => void (handler/types.ts). Calling next() just flips nextCalled. NOT calling it is how a handler terminates the chain — this is what makes cors() preflight short-circuiting work (it sets status 204, ends the response, and never calls next()).
Because of the await r{i} before the next handler, a middleware that returns a promise and calls next() serializes the chain: the following handlers run only after that promise settles.
- This is a sequential pipeline, NOT Express semantics (Express does not await middleware).
- A sync middleware calling
next()and returningundefineddoes not block —typeof undefined.then !== "function", so no await. - An async middleware (
async (ctx, next) => { await something(); next(); }) WILL block untilsomething()settles — by design.
Router.addRoutecomposes...middlewares, handleronce per route at registration (router.ts:39-42).Router.addWebSocketRoutedoes the same for WS chains (router.ts:50-56).- The compiled handler is what
Server.runinvokes ashandler.store(context, NO_OP).
See Benchmarking bench/src/middleware.bench.ts — compiled chains beat plain-array loops across 1/3/5/10-handler cases.