Skip to content

Middleware

Lee Yunjin edited this page Oct 7, 2026 · 1 revision

Middleware

Headers: <cwist/app.h> (chain API), <cwist/sys/app/middleware.h> (built-ins), <cwist/sys/app/compress.h>, <cwist/sys/app/csrf.h>, <cwist/sys/app/waf.h>, <cwist/net/http/session.h>

A middleware wraps the rest of the chain: it can act before and after the handler, change the request or response, or stop the chain by returning without calling next.

typedef void (*cwist_middleware_func)(cwist_http_request *req, cwist_http_response *res,
                                      cwist_handler_func next);

void cwist_app_use(cwist_app *app, cwist_middleware_func mw);
static void timing(cwist_http_request *req, cwist_http_response *res, cwist_handler_func next) {
    struct timespec a, b;
    clock_gettime(CLOCK_MONOTONIC, &a);
    next(req, res);                          /* rest of the chain + handler */
    clock_gettime(CLOCK_MONOTONIC, &b);
    char buf[32];
    snprintf(buf, sizeof buf, "%ld", (b.tv_nsec - a.tv_nsec) / 1000 + (b.tv_sec - a.tv_sec) * 1000000);
    cwist_http_header_add(&res->headers, "X-Elapsed-Us", buf);
}

cwist_app_use(app, timing);

Order and scope

  • Middleware runs in registration order: the first registered is the outermost.
  • The chain runs for requests that match a route, a content-hashed asset or a static file.
  • It does not run on a route miss. A request that matches nothing goes straight to the 404 error handler, so access logs do not record 404s and response headers added by middleware are absent from 404 responses. This includes CORS preflight: an OPTIONS request for a path that only has a GET route is a route miss and gets 404; register an OPTIONS route for paths that need preflight.
  • Handlers and middleware run concurrently on several threads; protect shared state.

Middleware with a context

typedef void (*cwist_middleware_func_ex)(cwist_http_request *req, cwist_http_response *res,
                                         cwist_handler_func next, void *user_ctx);
typedef void (*cwist_middleware_ctx_destroy_func)(void *user_ctx);

cwist_error_t cwist_app_use_ex(cwist_app *app, cwist_middleware_func mw,
                               cwist_middleware_func_ex mw_ex, void *user_ctx,
                               cwist_middleware_ctx_destroy_func destroy);

Pass exactly one of mw and mw_ex. user_ctx is passed to mw_ex on every call; destroy runs once when the app is destroyed or, if registration fails, before the call returns. Returns INT16 0 on success and -1 when app is NULL, not exactly one function is set, or allocation fails. Multiport sub-apps share the context without owning it. Language bindings use this to register closures.

Built-in middleware

Factory Behavior
cwist_mw_request_id(const char *header_name) Reuses the request's X-Request-Id or generates one, and sets it on the request and the response. The header_name argument is currently ignored; the header is always X-Request-Id.
cwist_mw_access_log(cwist_log_format_t format) Writes one line per request to stdout after the handler: CWIST_LOG_COMMON, CWIST_LOG_COMBINED (adds Referer and User-Agent) or CWIST_LOG_JSON. For a deferred response the status and size are logged as - (null in JSON).
cwist_mw_rate_limit_ip(int requests_per_minute) Per-client-IP token bucket; over the limit it answers 429 with Retry-After and stops the chain. See notes below.
cwist_mw_cors(void) Adds Access-Control-Allow-Origin: * to every response. For OPTIONS it adds Access-Control-Allow-Methods, Access-Control-Allow-Headers: Content-Type, Authorization, X-Request-Id and Access-Control-Max-Age: 86400, and answers 204 without calling the handler (subject to the route-miss rule above).
cwist_mw_metrics(void) Counts requests and total request duration into the metrics registry. It does not add a route; use cwist_app_enable_metrics() for GET /metrics.
cwist_mw_jwt_auth(const char *secret) Requires Authorization: Bearer <token> (scheme matched case-insensitively) signed with HS256 by secret; otherwise answers 401 with a JSON error body. Claims are available to later stages through cwist_mw_jwt_get_claims(req).
cwist_mw_compress(size_t min_body_size) Compresses bodies of at least min_body_size bytes with a registered backend chosen from Accept-Encoding. See Compression.
cwist_mw_csrf(cwist_app *app) Double-submit-cookie CSRF protection. See CSRF and WAF.
cwist_mw_waf_lite(void) Rejects requests matching the built-in attack signature set. See CSRF and WAF.
cwist_mw_session(cwist_app *app) Loads the signed session cookie before the handler and commits it after. See Sessions and Flash.
#include <cwist/sys/app/middleware.h>

cwist_app_use(app, cwist_mw_request_id(NULL));
cwist_app_use(app, cwist_mw_access_log(CWIST_LOG_JSON));
cwist_app_use(app, cwist_mw_rate_limit_ip(600));
cwist_app_use(app, cwist_mw_cors());
cwist_app_use(app, cwist_mw_metrics());

JWT claims

cwist_app_use(app, cwist_mw_jwt_auth("change-me"));   /* secret must outlive the app */

static void me(cwist_http_request *req, cwist_http_response *res) {
    const cwist_jwt_claims *claims = cwist_mw_jwt_get_claims(req);
    const char *sub = claims ? cwist_jwt_claims_get(claims, "sub") : NULL;
    cwist_sstring_assign(res->body, sub ? sub : "anonymous");
}

cwist_mw_jwt_get_claims() is valid only inside the chain behind the JWT middleware, on the thread running it; the claims are freed when the chain returns to the middleware. Copy anything you need later. One process supports eight distinct JWT secrets; cwist_mw_jwt_auth() returns NULL past that (and for a NULL secret). Registering the same secret again reuses its slot.

Rate limiter details

  • The per-IP table holds 1024 clients per process. When it is full, new clients are allowed through (fail-open) so spoofed addresses cannot lock everyone out.
  • The table is shared by every rate-limit middleware in the process, and a client's bucket is created with the rate of the first limiter that sees that client.
  • A process supports eight distinct requests_per_minute values. Past that, cwist_mw_rate_limit_ip() prints a warning to stderr and returns the limiter registered first, which uses that first rate.
  • cwist_mw_rate_limit_reset() clears the table (for tests).

Middleware and deferred responses

A handler may defer its response with cwist_async_defer() (Async Handlers). Middleware still unwinds normally after the handler returns, but the response no longer belongs to the chain: the access log records it with unknown status and size, and cwist_mw_compress() leaves it uncompressed.

Per-route middleware

App routes share one global chain. The standalone router offers per-route middleware through cwist_mux_route_use(); see Routing.

Clone this wiki locally