Skip to content

Responses

A handler fills in *http.Response with a status, headers and a body:

fn createUser(res: *http.Response) !void {
    res.status = .created;
    try res.header("Location", "/users/42");
    try res.print(.json, "{{\"id\":{d}}}\n", .{42});
}

The status is .ok unless the handler sets another one. For a response like this, nothing is sent until the handler returns. Middleware that runs after the handler can still change the response.

res.print formats the body. Its first argument is the content type, from http.ContentType: .text, .html, .json and so on. res.header sets any other header.

A redirect is a status and a Location header:

fn oldPage(res: *http.Response) !void {
    res.status = .moved_permanently;
    try res.header("Location", "/new-page");
}

Writing the Body

res.print is enough for a body you can format in one call. For more than that, res.writer() gives you a std.Io.Writer to write the body with:

fn listItems(res: *http.Response) !void {
    var body = res.writer();
    for (0..3) |i| {
        try body.interface.print("Item {d}\n", .{i});
    }
    try body.end();
    res.content_type = .text;
}

Call end when the body is done. The body is kept in memory until the handler returns, and sent with the rest of the response.

res.render writes the body from a template. See Templating. res.encode and res.json write it from a value. See Serialization.

Streaming

res.stream sends the headers right away, and then the body as the handler writes it:

fn countdown(req: *http.Request, res: *http.Response) !void {
    res.content_type = .text;
    var buf: [1024]u8 = undefined;
    var body = try res.stream(&buf);
    var i: usize = 3;
    while (i > 0) : (i -= 1) {
        try body.interface.print("{d}...\n", .{i});
        try body.interface.flush();
        try req.io.sleep(.fromSeconds(1), .awake);
    }
    try body.interface.writeAll("Done\n");
    try body.end();
}

buf collects the body before it's sent. flush sends what is in it. The body is sent with chunked encoding, unless the handler set a Content-Length header. Call end when the body is done.

Once the headers are sent, they can't change. If the handler fails after that, the server closes the connection, since it can't send an error response anymore.

For events sent to a browser as they happen, see Server-Sent Events.

Files

res.sendFile sends a file from a directory:

fn download(app: *App, res: *http.Response) !void {
    _ = res.sendFile(app.files, "report.pdf", .{}) catch |err| switch (err) {
        error.FileNotFound => return error.NotFound,
        else => return err,
    };
    res.content_type = .pdf;
    try res.header("Content-Disposition", "attachment");
}

The file is opened when sendFile is called, so a missing file is an error the handler can still turn into a 404. It's sent after the handler returns. sendFile doesn't set a content type, so set res.content_type yourself.

router.static serves files named by the request path, and checks that the path stays inside the directory. See Static Files and Assets.

Compression

With res.compress = true, the body is sent gzipped to clients that accept it:

fn report(res: *http.Response) !void {
    res.compress = true;
    res.content_type = .text;
    var body = res.writer();
    for (0..1000) |i| try body.interface.print("Line {d} of a long report\n", .{i});
    try body.end();
}

Cookies

res.setCookie adds a Set-Cookie header:

fn setTheme(res: *http.Response) !void {
    try res.setCookie("theme", "dark", .{
        .path = "/",
        .max_age = .fromSeconds(365 * 24 * 60 * 60),
        .http_only = true,
        .same_site = .lax,
    });
    try res.print(.text, "Theme saved\n", .{});
}

The options are in http.CookieOpts. To delete a cookie, set it again with .max_age = .zero, and the same .path. To read cookies, see Requests.

Next Steps