IOXD_HTTP(3)libioxd Programmer's ManualIOXD_HTTP(3)
NAME
ioxd/http.h - the request, the response, the context a handler receives, the body read on demand, the reply written as you go, and the run.
SYNOPSIS
#include <ioxd.h>
#define IOXD_MAX_HEADERS 64
#define IOXD_MAX_PARAMS 32
#define IOXD_MAX_ROUTE_PARAMS 8
#define IOXD_MAX_RESP_HEADERS 16
#define IOXD_RESP_HEAD_CAP 3072
#define IOXD_ROUTE_ARENA 256
typedef struct { ... } ioxd_request;
typedef struct { ... } ioxd_response;
typedef struct { ... } ioxd_ctx;
typedef void (*ioxd_handler)(ioxd_ctx *ctx);
typedef struct ioxd_next ioxd_next;
typedef void (*ioxd_mw)(ioxd_ctx *ctx, ioxd_next *next);
void ioxd_next_run(ioxd_ctx *ctx, ioxd_next *next);
ioxd_slice ioxd_body_all(ioxd_ctx *ctx);
int ioxd_body_read_until(ioxd_ctx *ctx, void *dst, size_t n);
int ioxd_body_read_next_chunk(ioxd_ctx *ctx, void *dst, size_t cap);
int ioxd_write (ioxd_ctx *ctx, const void *data, size_t len);
void *ioxd_reserve(ioxd_ctx *ctx, size_t n);
void ioxd_advance(ioxd_ctx *ctx, size_t n);
int ioxd_text (ioxd_ctx *ctx, const char *s);
int ioxd_printf(ioxd_ctx *ctx, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
bool ioxd_header (ioxd_ctx *ctx, const char *name, const char *value);
bool ioxd_content_type (ioxd_ctx *ctx, const char *type);
bool ioxd_content_length(ioxd_ctx *ctx, size_t n);
int ioxd_flush (ioxd_ctx *ctx);
typedef struct ioxd_certs ioxd_certs;
int ioxd_bind(int port, ioxd_certs *certs);
static inline int ioxd_run(int workers);
const char *ioxd_reason(int status);
DESCRIPTION
The limits that size a request and a reply. They are build-time constants of the LIBRARY: an application may not redefine them, since they lay out the context the library allocates - ioxd_run checks that the two sides agree and refuses to start otherwise. A request past a limit is answered 400 (more headers than fit, more query parameters than fit) or 414 (more decoded query than fits its arena); a reply past one is refused by the call that adds to it.
- IOXD_MAX_HEADERS
- request headers; more is a 400
- IOXD_MAX_PARAMS
- query parameters; more is a 400
- IOXD_MAX_ROUTE_PARAMS
- :name captures a route pattern may have
- IOXD_MAX_RESP_HEADERS
- headers a reply may add
- IOXD_RESP_HEAD_CAP
- bytes the added headers may serialize to
- IOXD_ROUTE_ARENA
- per-request bytes the router decodes into
the request
typedef struct ioxd_request {
ioxd_slice method; /* "GET", "POST", ... */
ioxd_slice target; /* raw request target: path plus any query */
ioxd_slice path; /* the path, query stripped */
ioxd_slice query; /* raw text after '?', undecoded */
int minor_version; /* 0 or 1 for HTTP/1.0 or 1.1 */
ioxd_kv headers[IOXD_MAX_HEADERS]; /* names lower-cased, values as received */
size_t n_headers;
ioxd_kv params[IOXD_MAX_PARAMS]; /* query parameters, percent-decoded */
size_t n_params;
ioxd_kv route_params[IOXD_MAX_ROUTE_PARAMS]; /* the :name captures, percent-decoded */
size_t n_route_params;
size_t content_length; /* what the head declared; 0 if nothing */
bool chunked; /* the body is chunked: length unknown */
ioxd_slice body; /* the whole body, once ioxd_body_all read it */
bool keep_alive; /* computed from version + Connection */
bool expect_continue; /* "Expect: 100-continue": the body waits for the interim reply the first body read sends */
char route_arena[IOXD_ROUTE_ARENA]; /* private: the router's per-request scratch */
} ioxd_request;
All the data of a request, as slices into the connection's read buffer (decoded parameters into a per-request arena), valid only until the handler returns. Read the arrays directly; header names are lower-cased, so compare them with lowercase literals. The body is not here until you ask for it: ioxd_body_all reads it whole, ioxd_body_read_until streams it. The engine has already checked the framing (RFC 9112): a Content-Length that is not a plain number, conflicting duplicates, a Transfer-Encoding with anything but a final "chunked", both fields together, a folded header line, or an HTTP/1.1 request without exactly one Host never reach a handler - they are answered 400 (501 for a transfer coding we do not implement) and the connection closes.
the response
typedef struct ioxd_response {
int status; /* 200 by default */
ioxd_slice content_type; /* "text/plain" by default; assign a slice that outlives the handler (a literal), or ioxd_content_type copies one */
ioxd_kv headers[IOXD_MAX_RESP_HEADERS]; /* added with ioxd_header: copies, names lower-cased */
size_t n_headers;
bool close; /* close the connection after this reply */
bool head_sent;
size_t content_length; /* declared with ioxd_content_length */
bool has_length;
bool chunked, failed; /* private: how a stream is framed; peer gone */
size_t body_sent; /* private: body bytes sent so far */
size_t head_len; /* private: the added headers, serialized */
char head[IOXD_RESP_HEAD_CAP];
} ioxd_response;
The reply being shaped. Body bytes wait in the connection's write slab (ioxd_write, ioxd_printf, ioxd_reserve); a full slab is sent and emptied, and the head (status, content type, headers) goes out in front of the first send - after the chain when everything fit, earlier when the body streams - and is frozen from then on (head_sent). What goes on the wire follows the request and the status, not only the handler: a reply to HEAD, a 1xx, 204 or 304 carries no body whatever was written (HEAD keeps the Content-Length the GET would have had; 1xx and 204 carry no framing header at all), a status outside 100-999 goes out as 500, and a declared Content-Length that the writes do not match is corrected when the reply was buffered whole and closes the connection when it streamed.
the context
typedef struct ioxd_ctx {
ioxd_request req;
ioxd_response res;
void *user; /* free slot: middleware hands data to the handler */
void *priv; /* the engine's own state */
} ioxd_ctx;
typedef void (*ioxd_handler)(ioxd_ctx *ctx);
typedef struct ioxd_next ioxd_next;
typedef void (*ioxd_mw)(ioxd_ctx *ctx, ioxd_next *next);
void ioxd_next_run(ioxd_ctx *ctx, ioxd_next *next);
Middleware runs around the handler (the onion model): shape the context, call ioxd_next_run to run the rest of the chain and then the endpoint, then act on the result - or write a reply and return WITHOUT calling ioxd_next_run to short-circuit (auth failure, cache hit).
the body
ioxd_slice ioxd_body_all(ioxd_ctx *ctx);
The whole body, read into the request buffer once and returned as a slice (also req.body). It must fit the buffer (16 KB by default): otherwise the slice is empty and res.status is 413, which becomes the reply - a handler that streams its reply should check and stop. Not after one of the reads below. A malformed chunked body is a 400 the same way. With "Expect: 100-continue" the first of these reads answers "100 Continue" before waiting; a handler that never reads such a body gets its reply sent and the connection closed.
int ioxd_body_read_until(ioxd_ctx *ctx, void *dst, size_t n);
The next bytes of the body into dst, reading until n are there or the body ends. Returns the count (less than n only at the end), 0 once it is all consumed (or for n == 0), -1 on error (the connection then closes after the reply). Any size of body, nothing kept in the engine.
int ioxd_body_read_next_chunk(ioxd_ctx *ctx, void *dst, size_t cap);
The next chunk of a chunked body, exactly as the sender framed it, into dst: the rest of the current chunk when a read stopped inside one, else the next whole one. Returns its length, 0 at the last chunk, -1 on error - a chunk larger than cap is a 413 - or when the body is not chunked.
the reply
int ioxd_write (ioxd_ctx *ctx, const void *data, size_t len);
Body writes into the slab. When it fills, it is sent - head first - and the body streams from then on: chunked on HTTP/1.1, until close on HTTP/1.0, or with the length declared below. Return 0, or -1 once the reply failed: the peer is gone, or a format could not be written (further writes are ignored either way).
void *ioxd_reserve(ioxd_ctx *ctx, size_t n);
void ioxd_advance(ioxd_ctx *ctx, size_t n);
int ioxd_text (ioxd_ctx *ctx, const char *s);
int ioxd_printf(ioxd_ctx *ctx, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
Or write into the slab directly: reserve n bytes (flushing first when they do not fit; nullptr once the peer is gone or n exceeds the slab) and advance by what was written - never more than reserved; advance clamps to the room that was there.
- ioxd_text
- a C string
- ioxd_printf
- formatted, into the slab
Shape the head, only before it is sent: each returns false afterwards. ioxd_header copies the name (sent lower-cased) and the value, so temporaries are fine; it also returns false for a name that is not an HTTP token, a value with a control byte (CR, LF, NUL: no response splitting), a header the engine owns (content-length, transfer-encoding, connection), when the table or its IOXD_RESP_HEAD_CAP bytes are full. "content-type" through it sets the content type.
- ioxd_content_type
- copied
- ioxd_content_length
- stream a large body with a known length
- ioxd_flush
- send what is in the slab now (starts streaming)
run
typedef struct ioxd_certs ioxd_certs;
int ioxd_bind(int port, ioxd_certs *certs);
Bind a port: plain HTTP when tls is NULL, TLS 1.3 terminated in the kernel otherwise, with the certificate store from ioxd_certs_load (ioxd_tls(3) in the manual). Every bound port serves the same routes; bind as many as you need (at most 8), then run. -1 if refused: a bad port, or the table is full.
static inline int ioxd_run(int workers);
Start `workers` proactor threads (<= 0: one per core) serving HTTP on every bound port, and block until SIGINT/SIGTERM. Returns 0 on clean shutdown, non-zero when nothing was bound, when a port could not be opened, when a worker failed, or when the limits above differ between this header and the library (the context would not match). May be called again after it returns; the ports stay bound.
const char *ioxd_reason(int status);
The reason phrase for a status code ("OK", "Not Found", ...); "Unknown" if unlisted.
EXAMPLES
A route parameter, a query parameter converted, and a formatted reply:
static void user(ioxd_ctx *ctx)
{
int64_t id;
if (!ioxd_to_i64(ctx->req.route_params[0].value, &id)) {
ctx->res.status = 400;
ioxd_text(ctx, "the id must be an integer\n");
return;
}
for (size_t i = 0; i < ctx->req.n_params; i++)
if (ioxd_slice_eq(ctx->req.params[i].key, "fields"))
ioxd_printf(ctx, "fields=%.*s\n", (int)ctx->req.params[i].value.len, ctx->req.params[i].value.p);
ioxd_header(ctx, "x-user", "42"); /* copied: a temporary is fine */
ioxd_printf(ctx, "user %lld\n", (long long)id);
}
A body read whole, then a reply streamed with a flush every ten lines:
static void repeat(ioxd_ctx *ctx)
{
ioxd_slice body = ioxd_body_all(ctx); /* over 16 KB: empty, and res.status is 413 */
if (ctx->res.status != 200)
return;
for (int i = 1; i <= 25; i++) {
if (ioxd_printf(ctx, "%d: %.*s\n", i, (int)body.len, body.p) < 0)
return; /* the peer is gone */
if (i % 10 == 0 && ioxd_flush(ctx) < 0)
return;
}
}
A large upload streamed through a fixed buffer:
static void upload(ioxd_ctx *ctx)
{
char buf[4096];
size_t total = 0;
for (;;) {
int n = ioxd_body_read_until(ctx, buf, sizeof buf);
if (n < 0) return; /* malformed or gone: the engine answers */
if (n == 0) break;
total += (size_t)n;
}
ioxd_printf(ctx, "%zu bytes\n", total);
}
Middleware around the handler, the onion way:
static void timing(ioxd_ctx *ctx, ioxd_next *next)
{
struct timespec t0, t1;
clock_gettime(CLOCK_MONOTONIC, &t0);
ioxd_next_run(ctx, next); /* the rest of the chain, then the endpoint */
clock_gettime(CLOCK_MONOTONIC, &t1);
/* the head may already be out (a streamed reply): ioxd_header then returns false */
}
SEE ALSO
ioxd_config(3), ioxd_router(3), ioxd_slice(3), ioxd_json(3), ioxd_pipe(3), ioxd_tls(3), ioxd_examples(7), ioxd(7)
libioxd 0.1.02026-09-09IOXD_HTTP(3)