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:
*App, the pointer you passed tohttp.Server(App).init*http.Request*http.Response*App.RequestContext, a struct that each request gets its own copy of, see Middlewarehttp.Param,http.Queryandhttp.Header, values from the request- Your own types, filled in by their
extractfunction
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",
});
}
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=oldgets 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:
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:
The new deadline counts from the call. req.setTimeout(.none) removes it. See
Configuration to change the default.