IOXD_EXAMPLES(7)libioxd Programmer's ManualIOXD_EXAMPLES(7)

NAME

ioxd_examples - whole programs, one per way of using the library; each builds as ioxd-example-<name> with make examples

DESCRIPTION

groups.croutes in groups: a prefix plus middleware, nesting, an endpoint's own middleware, a fallback of your own - as plain calls for one part of the tree and as the script for the rest, since they register the same thing.
json.cJSON replies: a struct described once and serialized with one call, a list of thousands streamed as one array, an object written call by call, and an error object with its status.
middleware.cmiddleware written by hand: one that stamps a header on every reply, one that times the request around the rest of the chain, one that gates a route and short-circuits, and how a middleware hands what it found to the handler.
pipes.ca protocol of your own on the pipe the HTTP engine itself reads and writes through: raw TCP, one coroutine per connection, the same suspend-and-resume. A frame here is a 4-byte big-endian length and a payload; the reply is the payload reversed, framed the same way.
static_files.cfiles from a directory, served by hand: the path's last segment names the file, its extension the content type, fstat the length, an ETag the version, and the bytes go through the reply slab piece by piece with a declared length. HEAD and 304 cost no read.
stream_request.ca request body streamed, so a body of any size never sits in memory whole: read through a fixed buffer until it ends, whatever its framing, or taken chunk by chunk exactly as the sender framed it.
stream_response.creplies that stream: a body bigger than the slab goes out chunked as the slab fills, a flush sends what is there on purpose so a client sees lines as they are made, a declared length lets a large body of known size go out with Content-Length instead, and reserve/advance write straight into the slab.

groups.c

make examples && ./ioxd-example-groups
curl -i http://127.0.0.1:8080/legacy/ping
curl -i http://127.0.0.1:8080/api/v1/users/42
curl -i http://127.0.0.1:8080/api/v1/admin/stats            # 403 from the group's gate
curl -i -H 'X-Admin: 1' http://127.0.0.1:8080/api/v1/admin/stats
curl -i http://127.0.0.1:8080/nowhere                       # the fallback, as JSON
curl -i -X DELETE http://127.0.0.1:8080/api/v1/users/42     # 405 with an allow header

ioxd_run resolves it all once: every full path into a segment tree, every endpoint's middleware - the root's, then each group's from the outside in, then its own - into one flat chain. A request costs one walk and no scan.

#include <ioxd.h>

/* --- middleware --- */

static void api_version(ioxd_ctx *ctx, ioxd_next *next)
{
    ioxd_header(ctx, "x-api-version", "1");
    ioxd_next_run(ctx, next);
}

static void require_admin(ioxd_ctx *ctx, ioxd_next *next)
{
    for (size_t i = 0; i < ctx->req.n_headers; i++)
        if (ioxd_slice_eq(ctx->req.headers[i].key, "x-admin")) {
            ioxd_next_run(ctx, next);
            return;
        }
    ctx->res.status = 403;
    ioxd_text(ctx, "admins only\n");
}

static void no_cache(ioxd_ctx *ctx, ioxd_next *next)
{
    ioxd_header(ctx, "cache-control", "no-store");
    ioxd_next_run(ctx, next);
}

/* --- handlers --- */

static void ping(ioxd_ctx *ctx)
{
    ioxd_text(ctx, "pong\n");
}

static void user(ioxd_ctx *ctx)
{
    ioxd_slice id = ctx->req.route_params[0].value;
    ioxd_printf(ctx, "user %.*s\n", (int)id.len, id.p);
}

static void update_user(ioxd_ctx *ctx)
{
    ioxd_slice id = ctx->req.route_params[0].value;
    ioxd_printf(ctx, "user %.*s updated\n", (int)id.len, id.p);
}

static void stats(ioxd_ctx *ctx)
{
    ioxd_text(ctx, "all good\n");
}

/* The fallback for what no route matches; a path that matches without the method is a built-in
 * 405 with an allow header, whatever this does. */
static void not_found(ioxd_ctx *ctx)
{
    ctx->res.status = 404;
    ioxd_content_type(ctx, "application/json");
    ioxd_printf(ctx, "{\"error\":\"no %.*s here\"}", (int)ctx->req.path.len, ctx->req.path.p);
}

int main(void)
{
    /* as calls: a group is a handle, an endpoint too */
    ioxd_group *legacy = ioxd_group_new(NULL, "/legacy");
    ioxd_group_use(legacy, no_cache);
    ioxd_get(legacy, "/ping", ping);                 /* GET /legacy/ping, behind no_cache */

    /* as a script: the block after IOXD_GROUP is the group, and it nests */
    IOXD_GROUP("/api", api_version) {
        IOXD_GROUP("/v1") {
            IOXD_GET ("/users/:id", user);
            IOXD_POST("/users/:id", update_user, no_cache);     /* this endpoint's own middleware */
            IOXD_GROUP("/admin", require_admin) {
                IOXD_GET("/stats", stats);           /* /api/v1/admin/stats: api_version, require_admin, then stats */
            }
        }
    }
    IOXD_DEFAULT(not_found);

    ioxd_bind(8080, NULL);
    return ioxd_run(0);
}

json.c

make examples && ./ioxd-example-json
curl http://127.0.0.1:8080/users/42
curl http://127.0.0.1:8080/users?n=5000 | head -c 200
curl http://127.0.0.1:8080/users/abc                   # {"error":"the id must be an integer"}
curl -i http://127.0.0.1:8080/health

The writer is forward-only and allocates nothing: the bytes go into the reply slab, escaped as they are written, and stream out chunked when a document outgrows it. Nesting and commas are its business; a handler says what it means. Every call returns false once the reply failed or the call had no place in the document, and the rest is then dropped, so checking the last call - or ioxd_json_done at the end - is enough.

#include <ioxd.h>

/* The shape of a user, described once: the struct and user_to_json come out of it. */
#define ADDRESS_FIELDS(X)                       \
    X(VALUE,    const char *, city)             \
    X(VALUE,    const char *, zip)              /* NULL comes out as null */
IOXD_JSON_STRUCT(address, ADDRESS_FIELDS)

#define USER_FIELDS(X)                          \
    X(VALUE,    int64_t,      id)               \
    X(VALUE,    const char *, name)             \
    X(VALUE,    bool,         active)           \
    X(OBJECT,   address,      address)          /* nested, by value                   */ \
    X(OPTIONAL, address,      billing)          /* a pointer: null when there is none */ \
    X(ARRAY,    const char *, tags,   n_tags)   /* scalars, and the field with the count */
IOXD_JSON_STRUCT(user, USER_FIELDS)

static struct user make_user(int64_t id)
{
    static const char *tags[] = { "new", "c23" };
    return (struct user){
        .id = id, .name = "Zo\xc3\xab \"Z\" O'Neil", .active = id % 2 == 0,     /* escaped on the way out */
        .address = { .city = "Porto", .zip = NULL },
        .billing = NULL,
        .tags = tags, .n_tags = 2,
    };
}

/* An error as a document, with its status: {"error":"..."}. */
static void error_reply(ioxd_ctx *ctx, int status, const char *message)
{
    ctx->res.status = status;
    ioxd_json j = ioxd_json_reply(ctx);              /* content-type: application/json */
    ioxd_json_object(&j);
    IOXD_JSON_FIELD(&j, "error", message);
    ioxd_json_end(&j);
}

/* GET /users/:id - one user, one call. */
static void one_user(ioxd_ctx *ctx)
{
    int64_t id;
    if (!ioxd_to_i64(ctx->req.route_params[0].value, &id)) {
        error_reply(ctx, 400, "the id must be an integer");
        return;
    }
    struct user u = make_user(id);
    ioxd_json j = ioxd_json_reply(ctx);
    user_to_json(&j, &u);
}

/* GET /users?n= - an array of n users: the slab fills and streams, chunked, as it goes. */
static void many_users(ioxd_ctx *ctx)
{
    int64_t n = 100;
    for (size_t i = 0; i < ctx->req.n_params; i++)
        if (ioxd_slice_eq(ctx->req.params[i].key, "n"))
            ioxd_to_i64(ctx->req.params[i].value, &n);
    ioxd_json j = ioxd_json_reply(ctx);
    ioxd_json_array(&j);
    for (int64_t id = 1; id <= n; id++) {
        struct user u = make_user(id);
        if (!user_to_json(&j, &u))
            return;                                  /* the peer is gone: nothing more to write */
    }
    ioxd_json_end(&j);
}

/* GET /health - the bare writer, no macros: a key, then the value's call by type; object,
 * array and end for the nesting. IOXD_JSON_FIELD(&j, "workers", 4) would be the key and the
 * value in one line, the call picked from the C type. */
static void health(ioxd_ctx *ctx)
{
    ioxd_json j = ioxd_json_reply(ctx);
    ioxd_json_object(&j);
    ioxd_json_key(&j, "status");  ioxd_json_cstr(&j, "ok");
    ioxd_json_key(&j, "workers"); ioxd_json_int(&j, 4);
    ioxd_json_key(&j, "load");    ioxd_json_double(&j, 0.25);
    ioxd_json_key(&j, "ports");   ioxd_json_array(&j);
                                  ioxd_json_int(&j, 8080);
                                  ioxd_json_int(&j, 8443);
                                  ioxd_json_end(&j);
    ioxd_json_end(&j);
    if (!ioxd_json_done(&j))                         /* nothing failed, nothing left open */
        ctx->res.status = 500;
}

int main(void)
{
    IOXD_GET("/users/:id", one_user);
    IOXD_GET("/users",     many_users);
    IOXD_GET("/health",    health);
    ioxd_bind(8080, NULL);
    return ioxd_run(0);
}

middleware.c

make examples && ./ioxd-example-middleware
curl -i http://127.0.0.1:8080/public
curl -i http://127.0.0.1:8080/whoami                              # 401
curl -i -H 'Authorization: Bearer secret' http://127.0.0.1:8080/whoami

A middleware is a function of the context and the rest of the chain: whatever it does before ioxd_next_run runs before the handler, whatever it does after runs after, and not calling ioxd_next_run at all is the short-circuit. Headers meant for a streamed reply must be added before ioxd_next_run - the head may already be on the wire afterwards.

#include <ioxd.h>

#include <stdatomic.h>
#include <stdio.h>
#include <time.h>

/* Every reply gets an x-request-id, counted across workers. Before ioxd_next_run, so a reply that
 * streams has it too; ioxd_header copies the value, so the local buffer is fine. */
static void request_id(ioxd_ctx *ctx, ioxd_next *next)
{
    static _Atomic unsigned long counter;
    char id[32];
    snprintf(id, sizeof id, "%lu", atomic_fetch_add(&counter, 1) + 1);
    ioxd_header(ctx, "x-request-id", id);
    ioxd_next_run(ctx, next);
}

/* The request, its status and how long the chain took, on stderr, once the chain has run. */
static void timing(ioxd_ctx *ctx, ioxd_next *next)
{
    struct timespec t0, t1;
    clock_gettime(CLOCK_MONOTONIC, &t0);
    ioxd_next_run(ctx, next);
    clock_gettime(CLOCK_MONOTONIC, &t1);
    long us = (t1.tv_sec - t0.tv_sec) * 1000000L + (t1.tv_nsec - t0.tv_nsec) / 1000;
    fprintf(stderr, "%.*s %.*s -> %d in %ld us\n", (int)ctx->req.method.len, ctx->req.method.p,
            (int)ctx->req.path.len, ctx->req.path.p, ctx->res.status, us);
}

/* What the gate found, for the handler: on the middleware's own frame, which outlives the chain. */
struct principal {
    const char *name;
    bool        admin;
};

/* The gate: a bearer token, or a 401 and no handler at all. */
static void auth(ioxd_ctx *ctx, ioxd_next *next)
{
    ioxd_slice token = { NULL, 0 };
    for (size_t i = 0; i < ctx->req.n_headers; i++)      /* names arrive lower-cased */
        if (ioxd_slice_eq(ctx->req.headers[i].key, "authorization"))
            token = ctx->req.headers[i].value;
    if (!ioxd_slice_eq(token, "Bearer secret")) {
        ctx->res.status = 401;
        ioxd_header(ctx, "www-authenticate", "Bearer");
        ioxd_text(ctx, "a bearer token, please\n");
        return;                                      /* no ioxd_next_run: the handler never runs */
    }
    struct principal who = { .name = "diogo", .admin = true };
    ctx->user = &who;                                /* handed down; valid until the chain returns */
    ioxd_next_run(ctx, next);
}

static void public_route(ioxd_ctx *ctx)
{
    ioxd_text(ctx, "for everyone\n");
}

static void whoami(ioxd_ctx *ctx)
{
    const struct principal *who = ctx->user;
    ioxd_printf(ctx, "%s%s\n", who->name, who->admin ? " (admin)" : "");
}

int main(void)
{
    IOXD_USE(request_id);                            /* root middleware: every request, the 404s too */
    IOXD_USE(timing);
    IOXD_GET("/public", public_route);
    IOXD_GET("/whoami", whoami, auth);               /* this endpoint's own middleware, after the root's */
    ioxd_bind(8080, NULL);
    return ioxd_run(0);
}

pipes.c

make examples && ./ioxd-example-pipes
printf '\0\0\0\5hello' | nc -q1 127.0.0.1 8100 | xxd     # 00000005 6f6c6c6568

Reading never copies unless it has to: read hands over the bytes the kernel delivered as one span, in place; copy takes a fixed-size piece into your own buffer; keep consumes bytes but holds them, contiguous with what was kept before, until release - so a message that arrives in several deliveries is assembled by the reader (in the kernel's buffer while it fits one, in the pipe's own once it spans two) and kept() is the whole of it. Writing goes through a slab: reserve room, write into it, advance, flush.

#include <ioxd.h>

#include <stdint.h>

#define FRAME_MAX 16000                              /* what fits the pipe's 16 KB with room to spare */

static void frames(ioxd_pipe *pipe)
{
    for (;;) {
        /* the length: four bytes, whatever deliveries they came in */
        unsigned char head[4];
        for (int got = 0; got < 4;) {
            int n = ioxd_pipe_copy(pipe, head + got, (size_t)(4 - got));
            if (n <= 0)
                return;                              /* the peer is done, or gone */
            got += n;
        }
        size_t len = (size_t)head[0] << 24 | (size_t)head[1] << 16 | (size_t)head[2] << 8 | head[3];
        if (len == 0 || len > FRAME_MAX)
            return;                                  /* not a frame we take: close */

        /* the payload: kept in place as it arrives, one span at the end */
        for (size_t have = 0; have < len;) {
            ioxd_slice live = { NULL, 0 };
            if (ioxd_pipe_read(pipe, &live) <= 0)
                return;
            size_t take = live.len < len - have ? live.len : len - have;
            if (!ioxd_pipe_keep(pipe, take))
                return;                              /* FULL: kept plus live outgrew the pipe */
            have += take;
        }
        ioxd_slice payload = ioxd_pipe_kept(pipe);   /* len bytes, contiguous */

        /* the reply, framed the same way, written straight into the slab: the length, then the
         * payload reversed in pieces no larger than the slab (8 KB) - reserve flushes what is
         * there when a piece would not fit, so a reply of any size streams through it */
        unsigned char *out = ioxd_pipe_reserve(pipe, 4);
        if (!out)
            return;
        out[0] = (unsigned char)(len >> 24); out[1] = (unsigned char)(len >> 16);
        out[2] = (unsigned char)(len >> 8);  out[3] = (unsigned char)len;
        ioxd_pipe_advance(pipe, 4);
        for (size_t done = 0; done < len;) {
            size_t piece = len - done < 4096 ? len - done : 4096;
            out = ioxd_pipe_reserve(pipe, piece);
            if (!out)
                return;
            for (size_t i = 0; i < piece; i++)
                out[i] = (unsigned char)payload.p[len - 1 - done - i];
            ioxd_pipe_advance(pipe, piece);
            done += piece;
        }
        if (ioxd_pipe_flush(pipe) < 0)
            return;
        ioxd_pipe_release(pipe);                     /* the kept bytes go; the next frame's stay */
    }
}

int main(void)
{
    ioxd_bind(8100, NULL);
    return ioxd_run_pipes(0, frames);
}

static_files.c

mkdir -p www && echo '<h1>hello</h1>' > www/index.html && echo 'h1 { color: teal }' > www/site.css
make examples && ./ioxd-example-static_files www
curl -i http://127.0.0.1:8080/
curl -i http://127.0.0.1:8080/static/site.css
curl -i -H 'If-None-Match: "<the etag>"' http://127.0.0.1:8080/static/site.css   # 304

The reads are read(2), synchronous on the worker: what the page cache holds comes back at once, a cold file stalls that worker's other connections for the read. Serving files through io_uring, with a snapshot in memory and change detection, is designed but not built yet.

#include <ioxd.h>

#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

static const char *g_dir = "www";

/* The content type from the extension; what is not known is bytes. */
static const char *type_of(const char *name)
{
    static const struct { const char *ext, *type; } types[] = {
        { ".html", "text/html; charset=utf-8" }, { ".css", "text/css" }, { ".js", "text/javascript" },
        { ".json", "application/json" },          { ".svg", "image/svg+xml" }, { ".png", "image/png" },
        { ".jpg", "image/jpeg" },                 { ".txt", "text/plain; charset=utf-8" },
    };
    const char *dot = strrchr(name, '.');
    if (dot)
        for (size_t i = 0; i < sizeof types / sizeof *types; i++)
            if (strcmp(dot, types[i].ext) == 0)
                return types[i].type;
    return "application/octet-stream";
}

/* The file, or a 404. One path segment names it, so it cannot leave the directory; ".." and a
 * hidden file are refused all the same. */
static void send_file(ioxd_ctx *ctx, const char *name)
{
    if (name[0] == '\0' || name[0] == '.' || strstr(name, "..")) {
        ctx->res.status = 404;
        return;
    }
    char path[PATH_MAX];
    snprintf(path, sizeof path, "%s/%s", g_dir, name);
    int fd = open(path, O_RDONLY | O_CLOEXEC);
    struct stat st;
    if (fd < 0 || fstat(fd, &st) < 0 || !S_ISREG(st.st_mode)) {
        if (fd >= 0)
            close(fd);
        ctx->res.status = 404;
        return;
    }

    /* the version: size and modification time; a client that has it gets a 304 and no body */
    char etag[64];
    snprintf(etag, sizeof etag, "\"%llx-%llx\"", (unsigned long long)st.st_size, (unsigned long long)st.st_mtime);
    for (size_t i = 0; i < ctx->req.n_headers; i++)
        if (ioxd_slice_eq(ctx->req.headers[i].key, "if-none-match") && ioxd_slice_eq(ctx->req.headers[i].value, etag)) {
            ctx->res.status = 304;
            ioxd_header(ctx, "etag", etag);
            close(fd);
            return;
        }

    ioxd_header(ctx, "etag", etag);
    ioxd_header(ctx, "cache-control", "max-age=60");
    ioxd_content_type(ctx, type_of(name));
    ioxd_content_length(ctx, (size_t)st.st_size);   /* Content-Length framing, whatever the size */
    if (ioxd_slice_eq(ctx->req.method, "HEAD")) {   /* the head is all that goes out: no read */
        close(fd);
        return;
    }
    for (off_t left = st.st_size; left > 0;) {
        size_t piece = left < 8192 ? (size_t)left : 8192;
        char  *at = ioxd_reserve(ctx, piece);        /* room in the slab, flushed first when full */
        if (!at)
            break;                                   /* the peer is gone */
        ssize_t n = read(fd, at, piece);
        if (n <= 0)
            break;                                   /* short of the declared length: the engine closes */
        ioxd_advance(ctx, (size_t)n);
        left -= n;
    }
    close(fd);
}

/* GET /static/:name */
static void file_route(ioxd_ctx *ctx)
{
    char name[NAME_MAX + 1];
    if (!ioxd_cstr(ctx->req.route_params[0].value, name, sizeof name)) {   /* too long, or a NUL inside */
        ctx->res.status = 404;
        return;
    }
    send_file(ctx, name);
}

/* GET / */
static void index_route(ioxd_ctx *ctx)
{
    send_file(ctx, "index.html");
}

int main(int argc, char **argv)
{
    if (argc > 1)
        g_dir = argv[1];
    IOXD_GET("/",             index_route);
    IOXD_GET("/static/:name", file_route);
    ioxd_bind(8080, NULL);
    return ioxd_run(0);
}

stream_request.c

make examples && ./ioxd-example-stream_request
head -c 50000000 /dev/urandom > big; curl -T big http://127.0.0.1:8080/upload
curl -H 'Transfer-Encoding: chunked' -T big http://127.0.0.1:8080/chunks

ioxd_body_read_until fills the buffer or reaches the end; ioxd_body_read_next_chunk hands over one chunk of a chunked body, whole. Both return 0 once the body is consumed and -1 on a malformed body or a peer that is gone - the engine then answers the 400 or closes, so the handler just returns. Nothing is drained twice: what a handler leaves unread, the engine reads past after it, up to a limit, so the connection stays in sync for the next request.

#include <ioxd.h>

#include <stdint.h>
#include <stdlib.h>

/* PUT or POST /upload: any framing, 4 KB at a time, hashed as it goes (FNV-1a). */
static void upload(ioxd_ctx *ctx)
{
    char     buf[4096];
    uint64_t hash = 1469598103934665603ULL, 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;                                   /* the whole body has been read */
        for (int i = 0; i < n; i++)
            hash = (hash ^ (unsigned char)buf[i]) * 1099511628211ULL;
        total += (uint64_t)n;
    }
    ioxd_printf(ctx, "%llu bytes, fnv1a %016llx\n", (unsigned long long)total, (unsigned long long)hash);
}

/* POST /chunks: a chunked body, each chunk as the sender framed it. A chunk larger than the
 * buffer is a 413, and curl frames 64 KB at a time, so the buffer is that big - and off the
 * coroutine's stack, which is 128 KB with the engine's own frames on it. */
static void chunks(ioxd_ctx *ctx)
{
    if (!ctx->req.chunked) {
        ctx->res.status = 400;
        ioxd_text(ctx, "send it chunked\n");
        return;
    }
    enum { CHUNK_MAX = 65536 };
    char *buf = malloc(CHUNK_MAX);
    if (!buf) {
        ctx->res.status = 500;
        return;
    }
    int count = 0;
    for (;;) {
        int n = ioxd_body_read_next_chunk(ctx, buf, CHUNK_MAX);
        if (n < 0)
            break;                                   /* malformed, gone, or a chunk too large: 413 */
        if (n == 0) {                                /* the last chunk: the body is done */
            ioxd_printf(ctx, "%d chunks in all\n", count);
            break;
        }
        count++;
        if (count <= 5)                              /* the reply streams too, chunked, as the slab fills */
            ioxd_printf(ctx, "chunk %d: %d bytes\n", count, n);
    }
    free(buf);
}

int main(void)
{
    IOXD_PUT ("/upload", upload);
    IOXD_POST("/upload", upload);
    IOXD_PUT ("/chunks", chunks);
    IOXD_POST("/chunks", chunks);
    ioxd_bind(8080, NULL);
    return ioxd_run(0);
}

stream_response.c

make examples && ./ioxd-example-stream_response
curl -N http://127.0.0.1:8080/rows?n=100000            # chunked; -N shows it arriving
curl -o /dev/null -w '%{size_download}\n' http://127.0.0.1:8080/blob?n=50000000
curl -I http://127.0.0.1:8080/blob?n=50000000          # HEAD: the head, no body

The head goes out with the first send and is frozen from then on: a status or a header set after a flush is too late (ioxd_header returns false). A write or a flush returns -1 once the peer is gone, and a handler should stop then; there is nobody to send to.

#include <ioxd.h>

#include <string.h>

/* The n of ?n=..., or a fallback. */
static long count_param(const ioxd_ctx *ctx, long fallback)
{
    for (size_t i = 0; i < ctx->req.n_params; i++) {
        int64_t v;
        if (ioxd_slice_eq(ctx->req.params[i].key, "n") && ioxd_to_i64(ctx->req.params[i].value, &v) && v >= 0)
            return (long)v;
    }
    return fallback;
}

/* GET /rows?n=: n lines. The slab streams by itself when it fills; the flush every 100 rows
 * sends earlier, so a client sees the feed move instead of 8 KB at a time. */
static void rows(ioxd_ctx *ctx)
{
    long n = count_param(ctx, 1000);
    for (long i = 1; i <= n; i++) {
        if (ioxd_printf(ctx, "row %ld\n", i) < 0)
            return;                                  /* the peer is gone */
        if (i % 100 == 0 && ioxd_flush(ctx) < 0)
            return;
    }
}

/* GET /blob?n=: n bytes with a declared length, written in place. Content-Length rather than
 * chunked framing, so a client knows the size up front; the engine holds the reply to it - a
 * handler that writes more is cut at n and the connection closed, one that writes less closes. */
static void blob(ioxd_ctx *ctx)
{
    long n = count_param(ctx, 1000000);
    ioxd_content_length(ctx, (size_t)n);
    ioxd_content_type(ctx, "application/octet-stream");
    for (long left = n; left > 0;) {
        size_t piece = left < 4096 ? (size_t)left : 4096;
        char  *at = ioxd_reserve(ctx, piece);        /* room in the slab; a flush first if it is full */
        if (!at)
            return;
        memset(at, 'x', piece);
        ioxd_advance(ctx, piece);
        left -= (long)piece;
    }
}

int main(void)
{
    IOXD_GET("/rows", rows);
    IOXD_GET("/blob", blob);
    ioxd_bind(8080, NULL);
    return ioxd_run(0);
}

SEE ALSO

ioxd_config(3), ioxd_http(3), ioxd_router(3), ioxd_slice(3), ioxd_json(3), ioxd_pipe(3), ioxd_tls(3), ioxd(7)

libioxd 0.1.02026-09-09IOXD_EXAMPLES(7)