cwist
Modern C web framework with libttak runtime
Loading...
Searching...
No Matches
Middleware API

Middleware allows you to intercept and process requests before they reach the final handler.

Functions

cwist_app_use

Registers a global middleware. Middlewares are executed in the order they are registered.

size_t cap
Definition otel.c:56
Main Application Context.
Definition app.h:155
void cwist_app_use(cwist_app *app, cwist_middleware_func mw)
Append a middleware callback to the application's execution chain.
Definition app.c:1376
void(* cwist_middleware_func)(cwist_http_request *req, cwist_http_response *res, cwist_handler_func next)
Middleware type that receives req/res pair and the next stage in the chain.
Definition app.h:115

Built-in Middlewares

Request ID Middleware

Generates a unique ID for each request and adds it to the X-Request-Id header in both request and response.

cwist_middleware_func cwist_mw_request_id(const char *header_name)
Request ID middleware factory.
Definition middleware.c:79
Built-in middleware factories.

Access Log Middleware

Logs request details (method, path, status, latency) to stdout. For a response deferred with cwist_async_defer() the status and size are not known when the chain returns; they are logged as - (null in the JSON format).

@ CWIST_LOG_COMBINED
Definition middleware.h:16
cwist_middleware_func cwist_mw_access_log(cwist_log_format_t format)
Access log middleware factory.
Definition middleware.c:296

Async log output

By default each access-log line, and each cwist_logger_* line, is written to stdout or stderr by the thread that handles the request, using block-buffered stdio (a TTY destination stays line-buffered), so lines may reach the fd in batches rather than one at a time. Call cwist_log_async_start() once at startup to have one background thread write them instead — that is also the way to get immediate per-line delivery. The request thread then only formats the line and copies it into a buffer.

cwist_log_async_start(0); /* 0 = 256 KiB buffer per stream */
cwist_app_listen(app, 8080);
cwist_log_async_stop(); /* writes what is still queued */
Application-level structured logging.
bool cwist_log_async_start(size_t buffer_bytes)
Start the background log writer.
Definition log_async.c:176
void cwist_log_async_stop(void)
Write out queued lines, stop the writer and return to synchronous output.
Definition log_async.c:229
int cwist_app_listen(cwist_app *app, int port)
Serve app on port until SIGTERM, SIGINT or cwist_shutdown_request(); blocks the calling thread.
Definition app.c:4628
  • Lines: each line stays whole, and lines on one stream keep the order they were logged in.
  • Full buffer: when the buffer is full, the caller waits for space. No line is dropped.
  • Long lines: a line larger than the buffer is written synchronously, so it can land out of order.
  • Exit: lines still queued at exit() are written by an atexit hook. Call cwist_log_async_flush() before _exit().
  • Workers: forked workers (CWIST_WORKERS) each start their own writer on their first line.
  • Mixing: output written with printf on the same stream is not ordered against queued lines.
  • WASM: not available. cwist_log_async_start() returns false and logging stays synchronous.

Rate Limiter Middleware

Limits the number of requests per minute from a single IP.

cwist_middleware_func cwist_mw_rate_limit_ip(int requests_per_minute)
Fixed-window rate limiter middleware (per-IP).
Definition middleware.c:467

Redis-backed shared rate limiter

The local limiter above keeps its buckets in one process. To share one limit across independent server processes, give each process its own dedicated pool on DB 0, with the default user and no AUTH, and use the same prefix and limit. Replacement connections start fresh: AUTH, SELECT, HELLO and other connection state are not replayed. Legacy calls that share this pool may also receive fresh connections after a timed call discards one. Use one prefix per rate policy: different limits with the same prefix share one counter, not independent buckets.

cwist_redis_pool_t *pool = cwist_redis_pool_create("127.0.0.1", 6379, 4);
.pool = pool,
.requests_per_minute = 60,
.key_prefix = "my-service:rate:",
.timeout_ms = 50,
.fail_closed = true,
};
/* Inside main: fail startup if registration did not succeed. */
if (!pool || !limit) return 1;
/* One process per instance: do not fork a live pool. */
cwist_app_listen_ex(app, 8080, 1, -1);
struct cwist_redis_pool cwist_redis_pool_t
Redis connection pool handle.
Definition cwist_redis.h:24
void cwist_redis_pool_destroy(cwist_redis_pool_t *pool)
Destroy the pool and close all connections.
cwist_redis_pool_t * cwist_redis_pool_create(const char *host, int port, size_t max_conns)
Create a Redis connection pool.
Redis-backed rate limiter configuration. Strings are copied.
Definition middleware.h:31
cwist_redis_pool_t * pool
Definition middleware.h:32
int cwist_app_listen_ex(cwist_app *app, int port, int workers_override, int c1m_override)
cwist_app_listen() with the worker count and server mode given explicitly.
Definition app.c:4636
cwist_middleware_func cwist_mw_rate_limit_ip_redis(const cwist_rate_limit_redis_cfg_t *cfg)
Shared per-IP 60-second window using one atomic Redis Lua command.

Each key is prefix + peer-IP. One Lua EVAL increments the counter and sets a 60-second expiry on the first request. It repairs missing expiry without extending a running window. The window starts at the first hit, not a process-local clock boundary. Allowed and rejected attempts count. Over-limit replies are 429 with a conservative Retry-After: 60.

With fail_closed = false (default), backend errors allow the request. With true, errors return 503, not 429. This includes timeout, pool exhaustion, malformed replies, and a missing socket peer. A failed command is never retried: it might already have incremented the counter. There is no local fallback counter.

The request-time budget covers pool wait, cached-peer reconnect, write, and reply I/O. Zero selects 50 ms; the maximum is 60000 ms. This is a blocking middleware, not an async reactor call. Scheduling delays are not a real-time guarantee. Initial pool creation and DNS are outside this budget. The first backend failure per config and process emits a warning with the error code and policy to stderr. Keys and credentials are not logged. The Redis command budget does not bound application logging. Failed initial pool creation or invalid config returns NULL.

Create pools after fork, separately in each serving process. Inherited pools are rejected by the timed API. Keep the pool alive until every call has stopped, then destroy it. Do not register a pre-fork pool for automatic CWIST_WORKERS children; this API does not add a worker-start hook. Eight distinct immutable configs fit per process; equal configs reuse a slot. Prefixes are copied and limited to 128 bytes.

Only the socket peer is used. Forwarded headers cannot choose the key. A request without a connected TCP peer fd (for example, a QUIC request) follows the missing-peer failure policy; this API does not extract QUIC path metadata. IPv6 keys use the full address (/128), not a /64 prefix; clients that rotate IPv6 addresses obtain separate buckets. Behind a reverse proxy, this limits the proxy address unless a trusted upstream preserves client source addresses. Redis RESP2 is plaintext: use loopback, a private network, or a separate TLS tunnel. The default user's ACL must allow EVAL, INCR, PTTL, and PEXPIRE. A server requiring AUTH makes every call fail; the configured failure policy then applies to every request. Eviction, failover, or external deletion can reset counters; this is not a global consistency guarantee across Redis data loss.

Run make redis-rate-limit-e2e with redis-server installed. The runner starts its own loopback Redis and never silently skips. Two C processes create separate pools and invoke the middleware with real TCP peer fds. Their 40 attempts share seven admissions. The test also checks TTL, expiry, namespaces, and failure policies. The normal unit target skips only the live-Redis part when no test port is supplied.

CORS Middleware

Enables Cross-Origin Resource Sharing (CORS) support.

  • Adds Access-Control-Allow-Origin: * to all responses.
  • Handles OPTIONS preflight requests with a 204 No Content status and appropriate headers, short-circuiting the request processing.
    cwist_middleware_func cwist_mw_cors(void)
    CORS middleware. Adds CORS headers and handles OPTIONS requests (returns 204).
    Definition middleware.c:525

Prometheus Metrics Middleware

Exposes a /metrics endpoint in Prometheus exposition format. Tracks request counts, latency histograms, and active connections.

cwist_middleware_func cwist_mw_metrics(void)
Prometheus metrics collection middleware.
Definition middleware.c:333

JWT Auth Middleware

Validates a Bearer token in the Authorization header using HMAC-SHA256. Responds with 401 Unauthorized and short-circuits the chain on failure. Decoded claims are available to downstream handlers via cwist_mw_jwt_get_claims().

cwist_app_use(app, cwist_mw_jwt_auth("my-secret"));
// Inside a handler behind the middleware:
struct cwist_jwt_claims cwist_jwt_claims
Opaque JWT claims object returned after successful verification.
Definition jwt.h:25
cwist_middleware_func cwist_mw_jwt_auth(const char *secret)
JWT authentication middleware factory.
Definition middleware.c:649
const cwist_jwt_claims * cwist_mw_jwt_get_claims(const cwist_http_request *req)
Retrieve JWT claims stored by cwist_mw_jwt_auth from a request.
Definition middleware.c:687

secret must be a null-terminated string that outlives the middleware invocations (typically a static or global string).

Compression Middleware

Inspects Accept-Encoding, compresses the response body with a registered backend (gzip/zstd), and adds Content-Encoding. Only compresses bodies at or above min_body_size bytes. A response deferred with cwist_async_defer() is sent uncompressed: it belongs to its completion once the handler defers it.

cwist_app_use(app, cwist_mw_compress(1024)); /* compress responses >= 1 KiB */
cwist_middleware_func cwist_mw_compress(size_t min_body_size)
Compression middleware factory.
Definition compress.c:713

Creating Custom Middleware

A middleware is a function that takes req, res, and a next callback.

Thread-Safety Note: Middlewares are executed in a multithreaded environment. Any access to shared resources must be synchronized.

static int total_requests = 0;
next(req, res);
}
HTTP Response Object. Supports standard string body or Zero-Copy pointer body.
Definition http.h:82
void(* cwist_handler_func)(cwist_http_request *req, cwist_http_response *res)
Function pointer type for HTTP route handlers.
Definition app.h:32