Middleware Pipeline
Aegon features a coroutine-native, onion-model middleware architecture designed for zero per-request heap allocation. Middlewares can execute pre-handler logic, delegate down the chain via co_await next(ctx), inspect or mutate the response during the post-handler phase, or terminate the pipeline early (short-circuit).
The Onion Execution Model
In Aegon, middleware functions wrap downstream handlers like layers of an onion:
Inbound Request
│
▼
[Global Middleware (Pre-handler)]
│
▼
[RouteGroup Middleware (Pre-handler)]
│
▼
[Per-Route Middleware (Pre-handler)]
│
▼
[Target Route Handler (ctx)]
│
▲
[Per-Route Middleware (Post-handler)]
│
▲
[RouteGroup Middleware (Post-handler)]
│
▲
[Global Middleware (Post-handler)]
│
▼
Outbound ResponseCore Signatures
Middlewares are defined under #include "http/Middleware.h". The core type aliases are:
namespace aegon::http {
using Next = std::function<core::Task<void>(Context&)>;
using MiddlewareFn = std::function<core::Task<void>(Context&, Next)>;
}Aegon automatically adapts four callable forms:
- Asynchronous Onion:
Task<void>(Context&, Next)— full pre/post coroutine execution. - Synchronous with Next:
void(Context&, Next). - Asynchronous Pre-Handler:
Task<void>(Context&)— automatically invokesnext(ctx). - Synchronous Pre-Handler:
void(Context&)— executes beforenext(ctx)without coroutine overhead.
Writing a Middleware
1. Basic Onion Middleware
#include "http/Server.h"
#include "http/Middleware.h"
#include <chrono>
#include <iostream>
using namespace aegon::http;
auto timing_middleware() -> MiddlewareFn {
return [](Context& ctx, Next next) -> core::Task<void> {
auto start = std::chrono::steady_clock::now();
// 1. Pre-handler logic
ctx.res().header("X-Framework", "Aegon");
// 2. Delegate to next middleware or route handler
co_await next(ctx);
// 3. Post-handler logic (runs after route handler finishes)
auto elapsed = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now() - start
);
ctx.res().header("X-Response-Time", std::to_string(elapsed.count()) + "us");
};
}2. Short-Circuiting (Authentication / Guard)
To abort processing and bypass downstream handlers, simply write your response status/body and return without invoking co_await next(ctx):
auto require_api_key() -> MiddlewareFn {
return [](Context& ctx, Next next) -> core::Task<void> {
auto key = ctx.req().header("X-API-Key");
if (key != "secret-token-123") {
// Short-circuit: do NOT call next(ctx)!
ctx.res().status(StatusCode::Unauthorized).json(R"({"error":"Unauthorized"})");
co_return;
}
// Key is valid -> continue
co_await next(ctx);
};
}Attachment Scopes
Middlewares can be attached at three distinct granularities:
1. Global Scope (Server::use / Router::use)
Global middlewares apply to all incoming requests, including preflights and error responses:
Server app;
// Applied globally to all requests
app.use(timing_middleware())
.use(cors());2. RouteGroup Scope (RouteGroup::use) & Inheritance
Group-scoped middlewares apply only to routes within that group. Nested sub-groups automatically inherit the parent group's middleware chain:
auto api = app.router().group("/api");
api.use(require_api_key()); // Applies to all /api routes
auto v1 = api.group("/v1"); // Automatically inherits require_api_key()!
v1.use(rate_limiter()); // Applies only to /api/v1 routes
v1.get("/users", [](Context& ctx) {
// Chain executed: Global -> api -> v1 -> handler
ctx.res().text("Users List");
});
api.get("/ping", [](Context& ctx) {
// Chain executed: Global -> api -> handler (v1 middleware does NOT run)
ctx.res().text("pong");
});3. Per-Route Scope
Individual routes can accept an explicit std::vector<MiddlewareFn> parameter:
std::vector<MiddlewareFn> admin_guards = {
require_api_key(),
require_admin_role()
};
// Route-level middleware runs after group middleware, right before handler
api.get("/admin/metrics", admin_guards, [](Context& ctx) {
ctx.res().json(R"({"metrics":"ok"})");
});Per-Request Typed Data Bag
Middlewares frequently need to pass state (such as decoded JWT claims, user sessions, or tracing IDs) to downstream handlers. Aegon provides a type-safe per-request store on Context.
TIP
This store is isolated per-request and has zero interaction with the long-lived singleton ServiceRegistry.
| Method | Return Type | Description |
|---|---|---|
ctx.set<T>(value) | void | Stores a typed value in the request context. |
ctx.get<T>() | T* (nullable) | Non-throwing retrieval; returns nullptr if not set. |
ctx.local<T>() | T& (reference) | Throws std::runtime_error if not set. |
ctx.has<T>() | bool | Returns true if type T is present. |
Example: Setting & Consuming Typed State
struct AuthUser {
std::string user_id;
std::string role;
};
// Auth middleware sets state
auto auth_middleware() -> MiddlewareFn {
return [](Context& ctx, Next next) -> core::Task<void> {
// Authenticate user...
ctx.set<AuthUser>(AuthUser{
.user_id = "usr_10482",
.role = "admin"
});
co_await next(ctx);
};
}
// Handler reads typed state
app.router().get("/dashboard", { auth_middleware() }, [](Context& ctx) {
// Non-throwing check
if (auto* user = ctx.get<AuthUser>()) {
ctx.res().text("Welcome, " + user->user_id);
return;
}
// Or throwing access:
// AuthUser& user = ctx.local<AuthUser>();
});Error Propagation
Exceptions thrown inside a middleware or route handler propagate naturally through standard C++ coroutine unwinding:
- Any middleware wrapping
next(ctx)can catch exceptions usingtry / catch:cpp[](Context& ctx, Next next) -> core::Task<void> { try { co_await next(ctx); } catch (const std::exception& e) { std::cerr << "Caught error in middleware: " << e.what() << "\n"; ctx.res().status(StatusCode::InternalServerError).body("Error"); } }; - Uncaught exceptions automatically propagate to the router's
set_error_handleror emit a standardized RFC 7807 500 JSON response.
Performance Guarantees
- Zero Heap Allocation on Dispatch: The chain runner (
run_chain) operates over contiguousstd::spanviews of pre-registered middleware vectors. - Zero-Overhead Fast Paths: Routes without group or route middleware store the raw
Handlerdirectly in the radix tree without lambda wrapper overhead. - Unmodified Radix Tree: Group and route middleware are pre-composed into the terminal handler at route registration time, ensuring SIMD exact-match lookups remain completely untouched.
Built-in Middlewares
Aegon ships with a suite of high-performance, production-ready built-in middlewares:
| Middleware | Header | Description |
|---|---|---|
| Access Logger | #include "http/middleware/AccessLog.h" | Structured access logging (Combined, Common, Json, Custom), slow request detection, and contextual RequestLogger. |
| Compression | #include "http/middleware/Compress.h" | SIMD-accelerated on-the-fly HTTP response compression (gzip, deflate) powered by libdeflate. |
| CORS | #include "http/middleware/Cors.h" | W3C Fetch specification CORS handling with origin validation, subdomain wildcards, and preflight caching. |
| Security Headers | #include "http/middleware/SecurityHeaders.h" | Defense-in-depth HTTP headers (CSP, HSTS, X-Content-Type-Options, Frame-Options, Referrer-Policy). |
| Authentication & RBAC | #include "http/middleware/Auth.h" | Bearer, Cookie, API Key, and Basic auth extractors with compile-time type-safe RBAC role guards. |