Middleware
Middleware in Junction is a linear pipeline — an ordered array of functions that run before route dispatch. Each middleware receives the Request object and can either short-circuit by returning a Response (e.g., an auth error), or return null to pass control to the next middleware in the chain.
Middleware Signature
Every middleware is a function with this signature:
fn my_middleware(req: Request) -> Response {
// Return null to continue to the next middleware or route handler
// Return a Response to short-circuit the pipeline
return null;
}Registering Middleware
use_middleware(app, logging_middleware);
use_middleware(app, compression_middleware());
use_middleware(app, session_middleware(app));Middleware runs in registration order. Use insert_middleware(app, index, middleware) to insert at a specific position.
Built-In Middleware
Logging / Tracing
logging_middleware() records a start timestamp on the request. After the response is generated, a structured log line is emitted containing method, path, status, duration, remote address, user agent, request ID, and trace ID. Log output goes to stderr.
junction.use_middleware(app, junction.logging_middleware());Compression (gzip / brotli / deflate)
compression_middleware() inspects the Accept-Encoding request header and sets X-Compression on the request. The router applies the chosen encoding to the response body. Images and videos are skipped automatically.
junction.use_middleware(app, junction.compression_middleware());CORS
Configurable cross-origin resource sharing with preflight handling. See CORS section for full configuration.
let cors_cfg = cors.config_from_object({
"allowed_origins": ["https://myapp.com", "https://*.subdomain.com"],
"allowed_methods": ["GET", "POST", "PUT", "DELETE", "PATCH"],
"allow_credentials": true,
"max_age": 86400
});
junction.use_middleware(app, cors.cors_middleware(cors_cfg));CSRF Protection
csrf_middleware(app) validates X-CSRF-Token header or _csrf_token form field against the session store for all non-idempotent methods (POST, PUT, PATCH, DELETE). See Security page for details.
junction.use_middleware(app, junction.csrf_middleware(app));Helmet Security Headers
Security headers are applied automatically by apply_security_headers() to every response. A configurable variant is available via security_headers_middleware(config).
Rate Limiting
rate_limit_middleware(app, config) implements a sliding-window counter per IP-route pair. Configurable limit and window.
junction.use_middleware(app, junction.rate_limit_middleware(app, {
"limit": 100,
"window_ms": 60000
}));Sessions
session_middleware(app) resolves or creates a session ID from the session_id cookie and attaches session data to req.auth_context["session_data"]. See Sessions page.
Static File Serving
static_middleware(root_dir, opts) serves static files from a directory. Supports SPA fallback, directory listing, MIME detection, and ETag-based caching. Place this middleware early in the pipeline so it short-circuits before the router.
let static_files = junction.static_middleware("./public", {
"serve_index": true,
"cache_max_age": 3600
});
junction.use_middleware(app, static_files);Timeout
timeout_middleware(ms) enforces a per-request deadline by checking a monotonic millisecond timestamp.
junction.use_middleware(app, junction.timeout_middleware(30000));Request and Response Objects
The Request struct provides:
req.method— HTTP method string (GET, POST, etc.)req.path— URL pathreq.query— Parsed query parameters as objectreq.params— Path parameters from the routerreq.headers— Raw headers objectreq.body— Raw body stringreq.json— Parsed JSON body (auto-populated forapplication/json)req.cookies— Parsed cookiesreq.auth_context— Auth data set by middleware (session, permissions, etc.)req.trace_context— Trace ID for distributed tracingreq.uploaded_files— Multipart file uploadsreq.remote_addr— Client IP addressreq.request_id— Unique request identifier
The Response struct provides:
res.status— HTTP status coderes.headers— Response headers objectres.body— Response body stringres.cookies— Cookies to set via Set-Cookieres.etag— ETag for conditional requestsres.last_modified— Last-Modified header valueres.stream— Stream handle for chunked responses
Response Helpers
// JSON response with auto-ETag
junction.json({"key": "value"}, 200);
// Plain text
junction.text("Hello", 200);
// HTML
junction.html("<h1>Hello</h1>", 200);
// XML
junction.xml("<doc><item/></doc>", 200);
// Binary bytes
junction.bytes(raw_data, "image/png", 200);
// File response with Content-Type, ETag, Last-Modified
junction.file_response("./public/logo.png", "image/png");
// Stream (chunked transfer)
junction.stream_response(stream_handle, "text/event-stream", 200);
// Redirects
junction.redirect("/new-location");
junction.redirect_permanent("/permanent-move");
// 204 No Content
junction.no_content();
// Error with RFC 9457 problem+json format
junction.error("NOT_FOUND", "Resource not found", 404);Content Negotiation
Use negotiate_content_type(req, available) to select a response format based on the Accept header. accepts_json(req) is a convenience shorthand.
fn handler(req: Request) -> Response {
let negotiated = junction.negotiate_content_type(req,
["application/json", "text/html", "text/plain"]);
if (negotiated == "application/json") {
return junction.json({"msg": "hello"}, 200);
}
return junction.text("hello", 200);
}Conditional Requests
check_conditional(req, last_modified, etag) returns a 304 Not Modified response when the client has a fresh copy. Use check_etag(req, etag) and check_last_modified(req, last_modified) for individual checks.
fn handler(req: Request) -> Response {
let not_modified = junction.check_conditional(req, "2024-01-01", "abc123");
if (not_modified != null) { return not_modified; }
return junction.json({"data": "fresh"}, 200);
}Cookie Helpers
let res = junction.json({"ok": true}, 200);
junction.set_cookie(res, "token", "abc123", {
"path": "/",
"http_only": true,
"secure": true,
"same_site": "Lax",
"max_age": 3600
});
junction.delete_cookie(res, "token", {"path": "/"});Custom Middleware Example
junction.use_middleware(app, fn(req: Request) -> Response {
req.request_id = "req_" + std.crypto.random_hex(8);
req.trace_context = {"trace_id": "trc_" + std.crypto.random_hex(8)};
return null;
});
junction.use_middleware(app, fn(req: Request) -> Response {
let start = std.time.now_millis();
// The middleware cannot intercept the response directly;
// use X-Start-Time pattern from logging_middleware instead
req.headers["X-Start-Time"] = std.time.now_iso8601();
return null;
});