Skip to content

Handlers

A handler is a function that gets the request and fills in the response. It declares only the parameters it needs, in any order, and each one is filled in by its type:

The return type is void or !void.

Values from the Request

http.Param, http.Query and http.Header give the handler a path parameter, a query parameter and a header, parsed as the type you ask for:

fn listPosts(
    user: http.Param("id", u64),
    sort: http.Query("sort", ?enum { new, top }),
    lang: http.Header("Accept-Language", ?[]const u8),
    res: *http.Response,
) !void {
    try res.print(.text, "Posts of user {d}, sorted by {t}, in {s}\n", .{
        user.value,
        sort.value orelse .new,
        lang.value orelse "any language",
    });
}
server.router.get("/users/:id/posts", listPosts);

Each one takes a name and a type. The parsed value is in .value. They parse the same types: integers, enums, bool, []const u8, and types with a parseParam function. See Routing for the details.

They differ in what happens when the value isn't there or doesn't parse:

  • A path parameter that doesn't parse makes the route not match, so the request gets a 404 unless another route matches it.
  • A query parameter or header that is missing is a 400. With an optional type, like ?enum { new, top } above, it's null instead.
  • A query parameter or header that doesn't parse is a 400, optional or not. ?sort=old gets a 400 here.

Your Own Parameter Types

A handler can take any type with a pub fn extract(scope: anytype) !T function. scope has four fields: app, req, res and ctx. This one reads two query parameters, with defaults:

const Page = struct {
    number: u32,
    size: u32,

    pub fn extract(scope: anytype) !Page {
        const number = try http.Query("page", ?u32).extract(scope);
        const size = try http.Query("size", ?u32).extract(scope);
        return .{
            .number = number.value orelse 1,
            .size = @min(size.value orelse 20, 100),
        };
    }
};

fn listComments(page: Page, res: *http.Response) !void {
    try res.print(.text, "Page {d}, {d} per page\n", .{ page.number, page.size });
}

extract can return error.MissingValue or error.InvalidValue, and the request gets a 400. Any other error is handled the same way as an error from the handler. Here, http.Query(...).extract returns error.InvalidValue for ?page=x, and Page.extract passes it on.

App State

All requests get the same *App, and they can run at the same time on different threads. Anything in the app that changes after startup has to be safe to use from several threads, for example with an atomic or a mutex:

fn index(app: *App, res: *http.Response) !void {
    const visit = app.visits.fetchAdd(1, .monotonic) + 1;
    try res.print(.text, "Visit number {d}\n", .{visit});
}

An app with no state uses http.Server(void), and passes {} in place of &app.

Errors

When a handler returns an error, the server answers with a 500 and logs the error. Some errors are the client's fault, and get a different status:

  • Reading a body that is too large gives a 413. A body in an encoding the server can't decode gives a 415, and a broken body gives a 400.
  • A query parameter, header or your own parameter type that is missing or doesn't parse gives a 400, as described above.

A handler that returns error.NotFound gets the same 404 as a request with no route:

fn getPost(id: http.Param("id", u64), res: *http.Response) !void {
    if (id.value > 100) return error.NotFound;
    try res.print(.text, "Post {d}\n", .{id.value});
}

The body the handler wrote before the error is thrown away. If the handler has already started sending the response, for example with res.stream, the server can't replace it and closes the connection.

Custom Error Responses

The app can replace the default 404 and error responses by declaring notFound and uncaughtError:

const App = struct {
    visits: std.atomic.Value(u64) = .init(0),

    pub fn notFound(req: *http.Request, res: *http.Response) !void {
        res.status = .not_found;
        try res.print(.text, "Nothing at {s}\n", .{req.path});
    }

    pub fn uncaughtError(req: *http.Request, res: *http.Response, err: anyerror) void {
        std.log.err("{s} failed: {t}", .{ req.path, err });
        res.status = .internal_server_error;
        res.print(.text, "Something went wrong\n", .{}) catch {};
    }
};

notFound takes parameters the same way a handler does, except path parameters, since there is no route. The 405 for a method with no route on the path goes through methodNotAllowed the same way, which gets the response with the status and the Allow header already set. uncaughtError returns void, so it can't fail. It can take the error as anyerror, but not http.Param, http.Query, http.Header or your own types.

The 400, 413 and 415 for what the client got wrong don't go through uncaughtError. To answer those yourself too, for example as JSON, turn off answer_client_errors in the server's request settings:

.request = .{ .answer_client_errors = false },

uncaughtError then gets them as well, and req.clientErrorStatus() gives the status the server would have used, or null when the client got nothing wrong.

Timeouts

A request has 30 seconds to finish by default, including the handler. A handler that needs longer can set a new deadline with req.setTimeout:

req.setTimeout(.{ .duration = .{ .raw = .fromSeconds(120), .clock = .awake } });

The new deadline counts from the call. req.setTimeout(.none) removes it. See Configuration to change the default.

Next Steps

  • Requests covers the request body, forms and cookies.
  • Responses covers status codes, headers and the ways to write a body.