Modules

Http

moduleSource
macOSLinuxWindowswasm

#import "Extensions/Http";

HTTP/1.0 and HTTP/1.1 message codecs, plus a server, a client and test tools on top of them for fibers. A jaic extension, not official Jai: other Jai compilers have no such module, and it may change between jaic releases. See docs/extensions/http-codec.md, http-server.md and http-client.md.

The codec has no IO of its own. A Parser reads requests or responses from byte buffers the caller fills, in pieces of any size, and reports the head, the body in pieces and the end of each message with strict RFC 9112 framing; the writers build heads and chunked bodies. Nothing in it touches a socket, so it runs on every target, wasm included. The server (App, serve) and the client (Client, get, send) need fibers and Extensions/Io, and are not built for wasm. Failures are values: the parser's .ERROR event and Parser.error, an Http_Error returned by a handler, a Client_Error returned by a client call. Memory the parser or a response owns is released with parser_free and close; everything else is borrowed or in the temporary allocator, as each procedure says.

ExampleOpen in playground ↗
parser: Parser;
parser_init(*parser, .REQUEST);
defer parser_free(*parser);

request := "GET /users?id=7 HTTP/1.1\r\nHost: example.com\r\n\r\n";
bytes: [] u8 = .{ request.count, request.data };

used, event := parse(*parser, bytes); // any chunking gives the same result
assert(event.kind == .HEAD);
assert(parser.head.method == .GET);
assert(parser.head.target == "/users?id=7");

Procedures#

Types#

(method: Method) -> string

The spelling of method on the wire, such as "GET". .OTHER has no spelling of its own and gives ""; use Head.method_text for the token a request actually carried. The string is a constant. The client refuses to send .OTHER (.INVALID_METHOD).

assert(method_name(.GET) == "GET");
assert(method_name(.OTHER) == "");
↗
(text: string) -> Method

The Method for a token (case-sensitive: method names are). Unknown tokens are .OTHER.

(version: Version) -> string

The text of version as it appears in a start line, "HTTP/1.0" or "HTTP/1.1". The string is a constant.

assert(version_name(.HTTP_1_0) == "HTTP/1.0");
assert(version_name(.HTTP_1_1) == "HTTP/1.1");
↗
(id: Header_Name) -> string

The canonical spelling of a well-known header, as the writers send it. .UNKNOWN has no spelling and gives "". The string is a constant.

assert(header_name_string(.CONTENT_TYPE) == "Content-Type");
assert(header_name_string(.WWW_AUTHENTICATE) == "WWW-Authenticate");
assert(HEADER_NAME_COUNT == 71);
↗
find_header2 overloads
(headers: [] Header_Field, name: string) -> (value: string, found: bool)

The first header called name (any case); found says whether there was one.

(value: string, token: string) -> bool

True when a comma-separated header value (Connection, Upgrade, Accept-Encoding...) lists token, compared without regard to case. Parameters after ; are ignored.

Parser#

(p: *Parser, kind: Parser_Kind, limits := Limits.{})

Prepares p to read requests or responses. Any state the parser had is released first, so it is safe to call on a parser that was used. limits caps line, head and body sizes (the defaults are in Limits); a message over a limit is refused with the error kind that maps to the matching status. For a response parser, set p.response_to to the request's method before the response head arrives.

limits := Limits.{ max_body_size = 4 };

parser: Parser;
parser_init(*parser, .REQUEST, limits);
defer parser_free(*parser);

text := "POST / HTTP/1.1\r\nHost: a\r\nContent-Length: 10\r\n\r\n0123456789";
data: [] u8 = .{ text.count, text.data };

failed := false;
while true {
    used, event := parse(*parser, data);
    data = advance(data, used);

    if event.kind == .ERROR failed = true;
    if event.kind == .ERROR || event.kind == .END || event.kind == .NEED_MORE break;
}

assert(failed);
assert(parser.error.kind == .BODY_TOO_LARGE);
↗
(p: *Parser)

Starts over for a new connection, keeping the allocated buffers.

(p: *Parser)

Releases the parser's buffers. The parser must not be used afterwards, except to start again with parser_init. Strings taken from its head, trailers or events are invalid after this.

(p: *Parser, limit: int)

Between messages: gives back the head and trailer storage that grew past limit bytes (each of the six arrays on its own), so a connection that once saw a huge head does not keep it. The previous message's head and trailers are gone afterwards, like when the next message begins. Does nothing while a message is under way.

(p: *Parser) -> bool

True when no message has begun: the connection may close without it being an error.

(data: [] u8, count: int) -> [] u8

Drops the first count bytes of data.

(p: *Parser, data: [] u8) -> (consumed: int, event: Event)

Reads from data and returns the number of bytes used and one event. Call again with the rest until NEED_MORE (the input is then used up); END and HEAD can come with no bytes used, so loop on the event, not on the byte count. BODY data is a slice of data.

(p: *Parser) -> Event

The connection has closed. Ends a body that runs until close; reports CLOSED when it fell between messages and an UNEXPECTED_EOF error when it fell inside one.

Writer#

(status: int) -> string

The standard reason phrase of a status, or "" for one this table does not know.

(out: *[..] u8, method: string, target: string, headers: [] Header_Field, body_length: s64) -> (Body_Kind, bool)

Appends a request head, framing included, and returns how the body must be sent: with a non-negative body_length, Content-Length is added (except for a GET, HEAD, DELETE, CONNECT or TRACE without a body) and the body is sent as is (.LENGTH, or .NONE for length 0); with -1 the length is not known, Transfer-Encoding: chunked is added and the body goes through write_chunk and write_last_chunk (.CHUNKED). ok is false, and out is left as it was, when a header is not valid or sets the framing itself.

(out: *[..] u8, status: int, headers: [] Header_Field, body_length: s64, options := Response_Options.{}) -> (Body_Kind, bool)

Appends a response head and returns how the body must be sent (see write_request_head). body_length is the length of the body the response is for; for a HEAD response (set request_method) it only fills in Content-Length, and nothing may be sent after the head. 1xx, 204 and CONNECT 2xx responses get no framing headers. A length of -1 means unknown: chunked on HTTP/1.1, and for HTTP/1.0 the body ends when the connection closes (Connection: close is added and the caller must close).

(out: *[..] u8)

HTTP/1.1 100 Continue, the answer to a request with Expect: 100-continue before its body is read.

write_chunk2 overloads
(out: *[..] u8, data: [] u8)

Appends data as one chunk. An empty data writes nothing: a chunk of size 0 would end the body (use write_last_chunk). Appends data to out as one chunk of a chunked body: the size in hex, CRLF, the bytes, CRLF. Nothing is written for empty data. out is a growing array the caller owns (it uses the context allocator); end the body with write_last_chunk.

out: [..] u8;
defer array_free(out);

write_chunk(*out, "hello");
write_chunk(*out, ""); // empty: nothing is written
assert(write_last_chunk(*out));

assert(string.{ out.count, out.data } == "5\r\nhello\r\n0\r\n\r\n");
↗
write_last_chunk2 overloads
(out: *[..] u8) -> bool

The zero-size chunk with optional trailers, which ends a chunked body. False, with nothing written, when a trailer is not valid or is a field a trailer may not be (framing, Host). Appends the zero-size chunk that ends a chunked body, with no trailers. Returns true.

Date#

(seconds: s64, out: *[HTTP_DATE_SIZE] u8)

Writes seconds since 1970-01-01 UTC as an IMF-fixdate (always 29 bytes).

(cache: *Date_Cache, seconds: s64) -> string

The date for seconds; the string points into cache and stays valid until the next call.

(text: string) -> (s64, bool)

Reads an IMF-fixdate into seconds since 1970-01-01 UTC. The obsolete RFC 850 and asctime forms are not accepted.

Date native#

URL#

(text: string, plus_is_space := false) -> string

Decodes %XX escapes (and + as a space when plus_is_space, which forms and queries use but paths do not). The text itself is returned, with no allocation, when there is nothing to decode; otherwise the result comes from context.allocator. A % not followed by two hex digits stays as it is.

(query: string) -> [] Pair

Splits a=1&b=two&flag into decoded pairs (flag has an empty value). The pairs and the strings that needed decoding come from context.allocator; the server calls this with the temporary allocator.

(text: string) -> string

Percent-encodes everything but unreserved characters (RFC 3986), for building queries and URLs.

(text: string) -> (Url, Url_Problem)

Reads an absolute http:// or https:// URL (the port defaults to 80 and 443).

Message#

(request: *Request, name: string) -> (string, bool)

The first query parameter called name, percent-decoded, and whether it was given.

app_statesince 0.30.0
(request: *Request, $T: Type) -> *T

The state of the app that serves request (app.state), or null when it was never set. For middleware; handlers take a *State(T) instead.

with_header2 overloads
(response: Response, name: string, value: string) -> Response

response with one more header: with_header(json(user), "ETag", tag). The name and value must stay valid until the response is written.

(status: int) -> Response

An empty response with only a status (status_only(204)).

(value: $T, status := 200) -> Response

value as JSON with Content-Type: application/json. The body is built in the temporary allocator.

createdsince 0.30.0
(value: $T, location := "") -> Response

value as JSON with status 201 and, when location is given, a Location header: the answer to a request that made something.

response := created(42, tprint("/users/%", 42));

assert(response.status == 201);
location, found := find_header(response.headers, "Location");
assert(found && location == "/users/42");

Doesn't run in the browser (skipped on wasm).

(body: string, status := 200) -> Response

Plain text with Content-Type: text/plain; charset=utf-8.

(body: string, status := 200) -> Response

HTML with Content-Type: text/html; charset=utf-8.

(body: $T, content_type: string, status := 200) -> Response

Raw bytes (a [] u8 or a string) with a content type of your choice. The bytes must stay valid until the response is written.

(location: string, status := 302) -> Response

A redirect to location: 302 unless you pass 301, 303, 307 or 308.

(status: int, detail := "") -> Http_Error

An error with a status and a detail text (used as it is). The named helpers below take a tprint format instead: not_found("No user %", id).

(format := "", args: ..Any) -> Http_Error

An Http_Error with status 400, for when the request is malformed or its content is invalid. format and args make the detail text as tprint does, in the temporary allocator.

(format := "", args: ..Any) -> Http_Error

An Http_Error with status 401, for when the caller has not authenticated. Add WWW-Authenticate with with_header. format and args make the detail text as tprint does, in the temporary allocator.

error := with_header(unauthorized("a token is required"), "WWW-Authenticate", "Bearer");

assert(error.status == 401);
assert(error.headers.count == 1);

Doesn't run in the browser (skipped on wasm).

(format := "", args: ..Any) -> Http_Error

An Http_Error with status 403, for when the caller is known but not allowed. format and args make the detail text as tprint does, in the temporary allocator.

(format := "", args: ..Any) -> Http_Error

An Http_Error with status 404, for when there is nothing at the requested resource. format and args make the detail text as tprint does, in the temporary allocator.

error := not_found("No user %", 7);

assert(error.status == 404);
assert(error.title == "Not Found");
assert(error.detail == "No user 7");

Doesn't run in the browser (skipped on wasm).

(format := "", args: ..Any) -> Http_Error

An Http_Error with status 409, for when the request clashes with the current state, such as a duplicate. format and args make the detail text as tprint does, in the temporary allocator.

(format := "", args: ..Any) -> Http_Error

An Http_Error with status 410, for when the resource existed and has been removed for good. format and args make the detail text as tprint does, in the temporary allocator.

(format := "", args: ..Any) -> Http_Error

An Http_Error with status 422, for when the request is well-formed but its content cannot be processed. format and args make the detail text as tprint does, in the temporary allocator.

(format := "", args: ..Any) -> Http_Error

An Http_Error with status 429, for when the caller is over a rate limit; add Retry-After with with_header. format and args make the detail text as tprint does, in the temporary allocator.

(format := "", args: ..Any) -> Http_Error

An Http_Error with status 500, for when something failed that is not the caller's fault. format and args make the detail text as tprint does, in the temporary allocator.

(format := "", args: ..Any) -> Http_Error

An Http_Error with status 503, for when the service cannot answer now; add Retry-After with with_header. format and args make the detail text as tprint does, in the temporary allocator.

error_text3 overloads
(error: Http_Error) -> string

A one-line description of any error, for logs and messages.

(request: *Request, error: Http_Error) -> Response

The response for an error: what app.on_error returns, or the problem document. Middleware use it to answer without calling next.

(request: *Request, error: Http_Error) -> Response

The default format, RFC 9457: application/problem+json with type, title, status, detail, instance (the request path) and, for bad input, location and field.

(status: int, location: string, field: string, detail: string) -> Http_Error

The error for bad input found by an extractor: names where the input came from and which part.

Body#

readsince 0.30.0
(body: *Body, buffer: [] u8) -> (int, Http_Error)

Copies the next bytes of the body, at most buffer.count, into buffer and returns how many. Zero means the body is over (an empty buffer also reads zero). Waits for bytes when none have arrived, within config.body_timeout. A body over the route's limit (max_body, else config.max_body_size) is a 413, and a client that stops sending a 408; the error stays the same on later calls.

read_allsince 0.30.0
(body: *Body) -> ([] u8, Http_Error)

The rest of the body. The bytes live in the connection's own buffer and stay valid until the response has been written; copy them to keep them longer. Read as read: over the limit is a 413, and a declared length over the limit is refused without reading anything.

read_json2 overloads
(body: *Body, $T: Type) -> (T, Http_Error)

The rest of the body decoded as JSON into a T, with app.json_options. Content-Type: application/json (or +json) is required (415); a body that is not JSON is a 400 and one of the wrong shape a 422, both naming the JSON path.

Stream#

stream_responsesince 0.30.0
(produce: Stream_Producer, data: *void, content_type: string, status := 200) -> Response

A response whose body produce writes with send_chunk. data is handed to it as is.

file_responsesince 0.30.0
(path: string, content_type := "", status := 200) -> Response

A response with the contents of the file at path, read in pieces as the client takes them (Content-Length is the file's size when the response is sent). An empty content_type is chosen from the file's extension (application/octet-stream when unknown). A file that cannot be opened is a 404, or a 500 for any other failure; a file that shrinks while it is sent cuts the connection.

send_chunk2 overloadssince 0.30.0
(stream: *Stream, data: [] u8) -> bool

Sends data to the client now, as one chunk of the body. False when the client has gone or the write timed out: the producer should return.

App#

(app: *App)

Releases what the app owns after servers and requests have stopped using it.

use2 overloads
(app: *App, middleware: Middleware)

Adds middleware that wraps every request (in the order added), including ones with no route.

(app: *App)

Asks the servers started by serve to stop: they stop accepting and finish the requests in progress. Safe to call from any thread; serve returns once they are done.

group2 overloads
(app: *App, $prefix: string, middleware: ..Middleware) -> *Group(prefix)

Routes under prefix that share middleware, which runs after the app's own. The prefix is part of the group's type, so a handler's Path(...) is checked against the whole route. The group belongs to the app and is released with it: do not free it. Groups nest with group(parent, "/more"), and the middleware add up.

guard :: (request: *Request, next: Next) -> Response {
    _, found := header(request, "Authorization");

    if !found return error_response(request, unauthorized("a token is required"));

    return next(request);
}

stats :: () -> string {
    return "stats";
}

main :: () {
    app: App;
    defer free_app(*app);

    admin := group(*app, "/admin", guard);
    get(admin, "/stats", stats); // GET /admin/stats

    api := test_client(*app);

    refused, _ := get(api, "/admin/stats");
    assert(refused.status == 401);
    close(*refused);

    token := Header_Field.[.{ name = "Authorization", value = "secret" }];
    allowed, _ := get(api, "/admin/stats", token);
    assert(allowed.status == 200);
    close(*allowed);
}

Doesn't run in the browser (skipped on wasm).

get3 overloads
(app: *App, $template: string, $handler: $H, max_body := 0, $loc := #caller_location)

get(*app, "/users/{id}", handler). max_body (all the verbs take it) is the most request body bytes the route reads: 0 uses config.max_body_size, -1 allows any size (for a handler that streams its *Body); a body over it is a 413 from Json, Form, read and read_all. The template and the handler are compile-time arguments: the template is parsed and every extractor of the handler is checked against it while the program compiles, and the code that fills them in is generated for this route. A route that does not fit is an error that points at the call.

find_user :: (id: Path("id", int)) -> string, Http_Error {
    if id.value != 1 return "", not_found("No user %", id.value);

    return "Ada", .{};
}

main :: () {
    app: App;
    defer free_app(*app);

    get(*app, "/users/{id}", find_user);

    api := test_client(*app);

    found, _ := get(api, "/users/1");
    assert(found.status == 200);
    close(*found);

    missing, _ := get(api, "/users/2");
    assert(missing.status == 404);
    close(*missing);
}

Doesn't run in the browser (skipped on wasm).

post4 overloads
(app: *App, $template: string, $handler: $H, max_body := 0, $loc := #caller_location)

Adds a POST route; the template, handler and max_body work as for get. A handler reads the body through Json(T), Form(T) or a *Body parameter.

Item :: struct {
    name: string;
}

add :: (item: Json(Item)) -> string {
    return tprint("added %", item.temporary.name);
}

main :: () {
    app: App;
    defer free_app(*app);

    post(*app, "/items", add);

    api := test_client(*app);
    response, _ := post(api, "/items", Item.{ name = "pen" }); // a struct is sent as JSON
    defer close(*response);

    text, _ := read_text(*response);
    defer free(text);

    assert(response.status == 200 && text == "added pen");
}

Doesn't run in the browser (skipped on wasm).

put3 overloads
(app: *App, $template: string, $handler: $H, max_body := 0, $loc := #caller_location)

Adds a PUT route; the template, handler and max_body work as for get.

patch3 overloads
(app: *App, $template: string, $handler: $H, max_body := 0, $loc := #caller_location)

Adds a PATCH route; the template, handler and max_body work as for get.

delete3 overloads
(app: *App, $template: string, $handler: $H, max_body := 0, $loc := #caller_location)

Adds a DELETE route; the template, handler and max_body work as for get. A handler that answers with nothing to say can return status_only(204).

discard :: (id: Path("id", int)) -> Response {
    return status_only(204);
}

main :: () {
    app: App;
    defer free_app(*app);

    delete(*app, "/items/{id}", discard);

    api := test_client(*app);
    response, _ := delete(api, "/items/7");
    defer close(*response);

    assert(response.status == 204);
}

Doesn't run in the browser (skipped on wasm).

options3 overloads
(app: *App, $template: string, $handler: $H, max_body := 0, $loc := #caller_location)

Adds an OPTIONS route. Register one only to change the default, which answers with an Allow list of the methods the path has. The template, handler and max_body work as for get.

choices :: () -> Response {
    return with_header(status_only(204), "Allow", "GET, POST");
}

main :: () {
    app: App;
    defer free_app(*app);

    options(*app, "/things", choices);

    api := test_client(*app);
    response, _ := options(api, "/things");
    defer close(*response);

    assert(response.status == 204);
    allow, found := find_header(response.headers, "allow");
    assert(found && allow == "GET, POST");
}

Doesn't run in the browser (skipped on wasm).

close2 overloads
(link: Link, now := false) -> Io.Io_Error

Closes the connection: both halves share one socket or pipe end, so closing the reader closes it once. With now it does not wait (see Io.close).

(timeout: s64) -> s64

A with_deadline argument for a timeout setting in milliseconds where 0 or less means no limit.

Server#

(app: *App, reuse_port := false) -> (*Server, Serve_Error)

Starts listening on the running scheduler (call it inside Io.io_run, from a fiber) and returns at once; connections are served by fibers of this scheduler. Fails if the address cannot be bound, the routes conflict or config.tls is not usable. stop and wait end it.

(server: *Server) -> string

The http://host:port (https:// with config.tls) the server is reachable at (a wildcard address shows as 127.0.0.1).

(server: *Server)

Starts the graceful shutdown: no new connections, idle ones close, requests in progress finish (up to config.shutdown_timeout, then the connections are cut). Returns at once; wait returns when it is done.

(server: *Server)

Blocks the calling fiber until the server has stopped and its connections are gone. The Server itself stays valid (its fibers may still be winding down) and costs a few hundred bytes; nothing needs it afterwards.

(app: *App) -> Serve_Error

Runs the app until it is stopped: SIGINT or SIGTERM (when config.handle_signals), or shutdown(*app) from another thread. One scheduler per config.threads (default: the processor count), each with its own listener on the same port. Returns once the servers have stopped; the error says why it could not start (the port is taken, two routes conflict, the TLS configuration is unusable) and is .NONE for a clean stop.

Client#

(client: *Client, name: string, value: string)

A header sent with every request of client that does not set it: set_header(*api, "Authorization", "Bearer abc"). Setting a name again replaces the value.

(text: string) -> [] u8

The bytes of text (the same memory), for a request's body.

Sends request and returns once the status line and headers have arrived. The response is whatever the server answered (a 404 is a response, not an error); the body is read from it with read_text, read_bytes, read_json or read_some. Errors are only about not getting a response: the URL, the connection, a timeout, a reply that is not HTTP, too many redirects.

(response: *Client_Response) -> (string, Client_Error)

The whole body as text, in memory from the context allocator (release it with free). A body longer than the request's max_response_size is a .TOO_LARGE error and is not read.

(data: *void, buffer: [] u8) -> (s64, Io.Io_Error)

The Io.Reader read procedure over a response body: it reads through read_some, stops at the request's max_response_size, and keeps the client error it ran into in the Body_Reader (it can report only an Io_Error).

(response: *Client_Response, buffer: [] u8) -> (int, Client_Error)

Reads the next part of the body into buffer and returns how many bytes it wrote: at least one while there is more, then 0 once the body has ended (and on every call after that). It waits for data when none has arrived. For a body that should not be held in memory:

hello :: () -> string {
    return "hello";
}

main :: () {
    app: App;
    defer free_app(*app);

    get(*app, "/", hello);

    api := test_client(*app);
    response, _ := get(api, "/");
    defer close(*response);

    buffer: [4] u8;
    total := 0;

    while true {
        count, error := read_some(*response, buffer);
        if error.kind != .NONE || count == 0 break;
        total += count;
    }

    assert(total == 5);
}

Doesn't run in the browser (skipped on wasm).

A non-2xx response as an error, for code that treats those as failures: .NONE for a 2xx. The error carries status, title and detail (from an RFC 9457 problem body, the kind the server sends, when there is one) and the body text, so this reads the body. Release the error with free_error.

main :: () {
    app: App;
    defer free_app(*app);

    api := test_client(*app);
    response, _ := get(api, "/missing");
    defer close(*response);

    error := error_for_status(*response);
    defer free_error(*error);

    assert(error.kind == .STATUS && error.status == 404);
}

Doesn't run in the browser (skipped on wasm).

()

Closes every idle connection of the running scheduler (they are also closed when it ends).

The counts of the running scheduler's pools, for tests and diagnostics. Call it on a scheduler, inside Io.io_run or a test client call. A program that closes or reads every response to the end shows no open responses.

Io :: #import "Extensions/Io";

main :: () {
    Io.io_run(() {
        statistics := http_statistics();

        assert(statistics.open_responses == 0);
        assert(statistics.idle_connections == 0);
    });
}

Doesn't run in the browser (skipped on wasm).

Test client#

(app: *App, options := Test_Options.{}) -> *Client

A client for app. The app must stay alive and unchanged in its routes while the client is used; its state pointer is yours to inspect between requests. The client belongs to the test run (run_tests frees it afterwards); free_test_client releases one sooner.

(client: *Client)

Releases a client made by test_client before the test ends. run_tests frees the clients of a test by itself, so call this only for a client that should not live until then. The client and its responses must not be used afterwards.

Expect#

(condition: bool, what := "", loc := #caller_location) -> bool

A condition the test relies on, for anything that is not a response: the state a handler left behind, a count. what says what the condition means.

(actual: $T, expected: T, what := "", loc := #caller_location) -> bool

actual == expected, showing both when they differ.

expect_header2 overloads
(response: Client_Response, name: string, value: string, loc := #caller_location) -> bool

The response has the header, with exactly this value (any case in the name). For Content-Type a value without parameters matches the media type: "application/json" accepts application/json; charset=utf-8.

(response: Client_Response, name: string, loc := #caller_location) -> bool

The response has no header called name (any case). A failure names the header and its value.

hello :: () -> string {
    return "hello";
}

main :: () {
    app: App;
    defer free_app(*app);

    get(*app, "/hello", hello);

    api := test_client(*app);
    response, _ := get(api, "/hello");
    defer close(*response);

    assert(expect_no_header(response, "Set-Cookie"));
    assert(expect_header(response, "Content-Type"));
}

Doesn't run in the browser (skipped on wasm).

(response: Client_Response, location: string, status := 0, loc := #caller_location) -> bool

The response redirects (3xx, or exactly status when given) to location.

(response: Client_Response, $T: Type, status := 0, loc := #caller_location) -> T

The body decoded as JSON into a T. The status must be status, or 2xx when it is left out, and the Content-Type JSON. On failure the value is empty. The strings in it are allocated with the context allocator; a test may leave them.

(response: Client_Response, status: int, detail := "", loc := #caller_location) -> Problem

The response is an RFC 9457 problem document with this status and, when detail is given, a detail that contains it. Returns the decoded document (empty when the check failed).

(cases: [] Test_Case)

Runs the tests one after another on the calling thread, prints a line per test (and the details of what failed), then a summary. Exits with status 1 when any expectation failed. Clients made by test_client are released after each test.

()

Collects the failure text that would be printed instead of printing it, and does not count the failures against the run: for tests of the test tools themselves. end_failure_capture returns the text and how many failures there were.

() -> (string, int)

Ends a begin_failure_capture and returns the failure text collected since, in the temporary allocator, and how many expectations failed. Failures that were captured are not counted against the run.

main :: () {
    begin_failure_capture();
    expect_equal(1, 2, "numbers");
    text, count := end_failure_capture();

    assert(count == 1);
    assert(text.count > 0);
}

Doesn't run in the browser (skipped on wasm).

() -> int

Totals so far, for programs that run their own loop instead of run_tests.

Types#

Methodenum
:: enum

The request methods the codec knows. A token that is not one of them is .OTHER and its spelling stays in Head.method_text. parse_method turns text into a Method and method_name turns it back.

assert(parse_method("PATCH") == .PATCH);
assert(parse_method("get") == .OTHER); // method names are case-sensitive
assert(method_name(.DELETE) == "DELETE");
↗
:: enum

The HTTP versions the codec speaks: HTTP_1_0 and HTTP_1_1. Anything else in a start line is refused as .VERSION_NOT_SUPPORTED (or .BAD_VERSION when it is not shaped like a version).

:: enum

What shape the request target has (RFC 9112 3.2).

:: enum

How a message body is delimited.

:: enum

Well-known header names. Parsed headers carry the id so code can compare an enum instead of strings; HEADER_NAMES holds the canonical spelling the writers use.

:: struct

One header field line. name keeps the spelling on the wire; id says which well-known header it is (.UNKNOWN for others). value has no leading or trailing blanks. Strings of a parsed head point into the parser's buffer.

:: enum

Why the parser refused a message, one value per rule it enforces. The comments give the status a server answers with (error_status returns the same number). The kind is in Parser.error after an .ERROR event, and a failed parser stays failed.

parser: Parser;
parser_init(*parser, .REQUEST);
defer parser_free(*parser);

// An HTTP/1.1 request without a Host header.
text := "GET / HTTP/1.1\r\n\r\n";
_, event := parse(*parser, .{ text.count, text.data });

assert(event.kind == .ERROR);
assert(parser.error.kind == .MISSING_HOST);
assert(error_status(parser.error.kind) == 400);
↗
:: struct

Why parsing stopped. message is a static string.

Limitsstruct
:: struct

Limits on what the parser accepts. A message over a limit is refused with the kind that maps to the status the limit names.

:: enum

Whether a Parser reads requests (as a server does) or responses (as a client does). Set once by parser_init.

:: enum

What parse found. Loop on the event, not on the number of bytes used: HEAD and END can come with none. The comments say where the data of each kind lives.

parser: Parser;
parser_init(*parser, .RESPONSE);
defer parser_free(*parser);

text := "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello";
data: [] u8 = .{ text.count, text.data };

kinds: [..] Event_Kind;
defer array_free(kinds);

body: [..] u8;
defer array_free(body);

while true {
    used, event := parse(*parser, data);
    data = advance(data, used);
    array_add(*kinds, event.kind);

    if event.kind == .BODY array_add(*body, ..event.data);
    if event.kind == .END || event.kind == .NEED_MORE || event.kind == .ERROR break;
}

assert(kinds.count == 3 && kinds[0] == .HEAD && kinds[1] == .BODY && kinds[2] == .END);
assert(parser.head.status == 200);
assert(string.{ body.count, body.data } == "hello");
↗
Eventstruct
:: struct

One result of parse: its kind and, for .BODY, data, a slice of the input that was handed to parse (not a copy), so it is valid only as long as that input is. For the other kinds data is empty.

Spanstruct
:: struct

A header line of the head or the trailers, as offsets into the buffer that holds it (the buffer moves while it grows; strings are made once the head is complete).

:: enum

Where the reader is inside a chunk extension (RFC 9112 7.1.1).

:: enum

Where a Parser is in a message. It is bookkeeping for the parser itself; callers watch the events parse returns instead.

Parserstruct
:: struct

An incremental HTTP/1.x parser for one direction of a connection. Start it with parser_init, feed it with parse in pieces of any size, and release it with parser_free. The result does not depend on how the bytes were split.

parser: Parser;
parser_init(*parser, .REQUEST);
defer parser_free(*parser);

text := "POST /items HTTP/1.1\r\nHost: example.com\r\nContent-Length: 2\r\n\r\nhi";
data: [] u8 = .{ text.count, text.data };

body: [..] u8;
defer array_free(body);

while true {
    used, event := parse(*parser, data);
    data = advance(data, used);

    if event.kind == .HEAD {
        assert(parser.head.method == .POST);
        assert(parser.head.target == "/items");
        assert(parser.head.content_length == 2);
    }

    if event.kind == .BODY array_add(*body, ..event.data);
    if event.kind == .END || event.kind == .NEED_MORE || event.kind == .ERROR break;
}

assert(string.{ body.count, body.data } == "hi");
↗
:: struct

What the response writer needs to know besides the status and the headers.

:: struct

The date text for the current second, formatted again only when the second changes. One cache per thread (it is not synchronized).

Urlstruct
:: struct

The parts of an http://host:port/path?query (or https://) URL. All strings point into the text given to parse_url.

:: enum

Why parse_url refused a text, with .NONE for success.

url, problem := parse_url("https://example.com:8443/a/b?x=1");
assert(problem == .NONE);
assert(url.secure && url.host == "example.com" && url.port == 8443);
assert(url.target == "/a/b?x=1");

_, problem = parse_url("ftp://example.com/");
assert(problem == .UNSUPPORTED_SCHEME);

_, problem = parse_url("/relative/path");
assert(problem == .NOT_A_URL);

Doesn't run in the browser (skipped on wasm).

Requeststruct
:: struct

A parsed request, as handlers and middleware see it. The strings (target, header names and values) stay valid until the response has been written: copy what has to outlive the request. The body is not here: take a *Body parameter to read it (see Body), or Json(T), Form(T).

:: #type (request: *Request) -> Response

A handler after its extractors are built: (request) -> Response. Routes store one per route.

Nexttype
:: #type (request: *Request) -> Response

The rest of the middleware chain and then the handler: next(request) runs it and returns its response, which the middleware may change or replace.

:: #type (request: *Request, next: Next) -> Response

A request filter: log, check a token, add headers, or answer itself without calling next.

require_token :: (request: *Request, next: Next) -> Response {
    token, found := header(request, "Authorization");
    if !found return error_response(request, unauthorized("a token is required"));

    return next(request);
}

Doesn't run in the browser (skipped on wasm).

Responsestruct
:: struct

What a handler returns when it wants full control. The headers and body must stay valid until the response is written: use string literals or the temporary allocator (tprint, ,, temp), which the server resets after each response. The builders below do.

:: enum

Whether an Http_Error is set: .NONE (a handler's .{}) or .STATUS, an HTTP status to answer with.

:: struct

What a handler returns to answer with a status instead of a value. A handler's .{} means no error. The server sends it as an RFC 9457 application/problem+json document:

find_user :: (id: int) -> string, Http_Error {
    return "", not_found("No user with id %", id);
}

_, error := find_user(7);

// Sent as 404 {"type":"about:blank","title":"Not Found","status":404,"detail":"No user with id 7"}
assert(error.kind == .STATUS);
assert(error.status == 404);
assert(error.detail == "No user with id 7");

Doesn't run in the browser (skipped on wasm).

Bodystructsince 0.30.0
:: struct

The body of the request being handled. Take it as a handler parameter (body: *Body); it is valid until the handler returns.

Stream_Producertypesince 0.30.0
:: #type (stream: *Stream, data: *void)

Where a stream's bytes come from. Called once, after the head has been sent, on the connection's fiber; it may wait (Io.sleep, channels) between pieces. Not called for a HEAD request.

Streamstructsince 0.30.0
:: struct

The sending end of a streamed response, valid while its producer runs.

Pathstruct
:: struct

One {name} of the route, parsed as a T: integers, floats, bools, strings and enums (by member name). A value that does not parse is a 400 that names the parameter.

Paramsstruct
:: struct

Several {params} at once: every field of T names a parameter of the route. The T lives in temporary storage for the request.

Post_Key :: struct {
    user: int;
    post: string;
}

get_post :: (key: Params(Post_Key)) -> string {
    return tprint("% / %", key.temporary.user, key.temporary.post);
}

main :: () {
    app: App;
    defer free_app(*app);

    get(*app, "/users/{user}/posts/{post}", get_post);

    api := test_client(*app);
    response, _ := get(api, "/users/7/posts/intro");
    defer close(*response);

    text, _ := read_text(*response);
    defer free(text);

    assert(text == "7 / intro");
}

Doesn't run in the browser (skipped on wasm).

Reststruct
:: struct

The {name...} tail of the route as one string (the rest of the path, percent-decoded).

Querystruct
:: struct

The query string, decoded into a T with the rules and notes of JSON (default, rename, alias...): a field without default is required, repeated keys fill arrays. The T lives in temporary storage for the request.

Jsonstruct
:: struct

The whole request body as JSON, read before the handler runs (read_json does the same from a *Body). Content-Type: application/json is required (415 otherwise), a body that is not valid JSON is a 400 and one of the wrong shape a 422; both name the JSON path. A body over the route's max_body is a 413. The T lives in temporary storage for the request: copy the strings you keep.

New_User :: struct {
    name: string;
}

User :: struct {
    id: int;
    name: string;
}

create :: (body: Json(New_User)) -> User {
    return .{ id = 1, name = copy_string(body.temporary.name) };
}

main :: () {
    app: App;
    defer free_app(*app);

    post(*app, "/users", create);

    api := test_client(*app);
    response, _ := post(api, "/users", New_User.{ name = "ada" });
    defer close(*response);

    text, _ := read_text(*response);
    defer free(text);

    assert(response.status == 200 && text.count > 0);
}

Doesn't run in the browser (skipped on wasm).

Formstruct
:: struct

An application/x-www-form-urlencoded body, read before the handler runs and decoded like a query string. The same limit and the same lifetime as Json.

:: struct

One request header that may be missing, parsed as a T when it is there. present says which; when it is false value is the zero value. A header that is present but does not parse as T is still a 400. Strings point into the request and last as long as it does.

Statestruct
:: struct

The shared state set with app.state = *my_state. Take it as *State(My_State); the fields of My_State are reachable directly. The pointer is the state itself, not a copy: handlers on several threads see the same memory, so guard what they change.

:: struct

What an App does when nothing else is said. Every number is a limit or a timeout in milliseconds unless it says otherwise; 0 turns the ones that allow it off.

:: #type (error: Http_Error, request: *Request) -> Response

Called for every error response (a handler's Http_Error, a failed extractor, 404, 405, 413...) to make the response. Set app.on_error to use your own format instead of problem+json.

Appstruct
:: struct

A web application: its routes, middleware, settings and shared state. A zeroed App is a working one. Add routes with get, post and the other verbs, then serve it with serve or start, or drive it in memory with test_client. Routes are added before serving and not changed while the app serves; the handlers run on the scheduler threads. The app owns the router, groups and setup errors it builds: release them with free_app after the servers have stopped. Strings you store in its fields (config.address, ...) are yours and are not copied.

hello :: () -> string {
    return "hello";
}

main :: () {
    app: App;
    defer free_app(*app);

    get(*app, "/hello", hello);

    api := test_client(*app); // the app, driven in memory: no socket
    response, error := get(api, "/hello");
    defer close(*response);

    assert(error.kind == .NONE && response.status == 200);

    text, read_error := read_text(*response);
    defer free(text);

    assert(read_error.kind == .NONE && text == "hello");
}

Doesn't run in the browser (skipped on wasm).

Groupstruct
:: struct

A prefix and middleware shared by the routes registered through it. The prefix is part of the type, so a handler's Path("org", ...) is checked against the whole route, prefix included.

Serve_Error_Kindenumsince 0.30.0
:: enum

Why a server did not start (Serve_Error.kind), with .NONE for success. The comments say what each kind points at. Routes are checked when the server starts, not when they are added, so a conflict shows up here.

Serve_Errorstructsince 0.30.0
:: struct

Why a server did not start. A kind of .NONE is success.

Serverstruct
:: struct

One scheduler's listener and its connections.

:: struct

A request. Only url is needed; .{ url = "http://..." } is a GET.

:: struct

A response: the status and headers, and the body still waiting on its connection. close releases it.

:: enum

What went wrong when the client did not get, or could not read, an answer (Client_Error.kind), with .NONE for success. A response with any HTTP status, a 404 included, is not an error; error_for_status turns a non-2xx status into .STATUS.

// A URL the client cannot request is refused before anything is sent.
api: Client;
response, error := get(*api, "ftp://example.com/file");

assert(error.kind == .INVALID_URL);
assert(response.status == 0);

Doesn't run in the browser (skipped on wasm).

:: struct

What the client returns when something went wrong. A handler's Http_Error is about answering with a status; this is about not getting, or not reading, an answer. error_text makes one line of it.

Transportstruct
:: struct

How a client reaches a server. Both procedures run on a fiber of the running scheduler.

Clientstruct
:: struct

What requests have in common.

:: struct

Pool settings, for the whole program. Idle connections are closed when they have waited idle_timeout milliseconds or when more than max_idle_per_host pile up for one host.

:: struct

The body of a response as an Io.Reader, for read_json: each read is a read_some that stops at the request's max_response_size and keeps the client error it ran into, which the reader can only report as an Io_Error.

:: struct

What the running scheduler's pools hold right now, for tests and diagnostics (nothing here is needed to use the module). Call it on a scheduler: inside Io.io_run or a test client call.

:: struct

How test_client reaches the app: in memory (the default) or over a loopback socket, whether redirects are followed, and the timeout of a request.

go_home :: () -> Response {
    return redirect("/home");
}

home :: () -> string {
    return "home";
}

main :: () {
    app: App;
    defer free_app(*app);

    get(*app, "/", go_home);
    get(*app, "/home", home);

    plain := test_client(*app);
    first, _ := get(plain, "/");
    assert(first.status == 302); // the default hands the redirect back
    close(*first);

    following := test_client(*app, .{ follow_redirects = true });
    second, _ := get(following, "/");
    assert(second.status == 200);
    close(*second);
}

Doesn't run in the browser (skipped on wasm).

Problemstruct
:: struct

The RFC 9457 document the server sends for errors, as expect_problem decodes it.

Test_Casestruct
:: struct

One test for run_tests: a name to print and a procedure to run. The procedure makes its checks with the expect_ calls; a failed one is printed and counted, and the test goes on.

check_math :: () {
    expect_equal(1 + 1, 2, "addition");
}

main :: () {
    run_tests(.[.{ name = "math", run = check_math }]);
}

Doesn't run in the browser (skipped on wasm).

Constants#

:: string.[ "", "Accept", "Accept-Charset", "Accept-Encoding", "Accept-Language", "Accept-Ranges", "Access-Control-Allow-Credentials", "Access-Control-Allow-Header ...

Indexed by Header_Name.

:: HEADER_NAMES.count

The number of entries in Header_Name, .UNKNOWN included. Use it to size a table indexed by header id.

:: Query_Pair

One decoded name=value of a query string or a form, or one path parameter. The same type Extensions/Json decodes typed values from.