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
- Templating covers HTML pages.
- Serialization covers JSON and MessagePack.