I have started to work on a new small Zig project.
The application has 2 tasks it has to perform.
It has to listen for incoming HTTP requests and handle them, and it also has to periodically check if it has to message me a reminder I scheduled for myself through a Telegram bot.
I just wanted to get something working quickly, so for handling incoming HTTP requests I used Zig’s std.http.Server
struct.
This worked fine enough, I had it in a loop like this to wait for incoming requests:
var server = try server_address.listen(.{});
defer server.deinit();
while (true) {
const connection = try server.accept();
defer connection.stream.close();
var http_server = std.http.Server.init(connection, http_read_buffer);
var request = http_server.receiveHead() catch continue;
...
_ = request.respond("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nRequest succesful", .{}) catch unreachable;
However, to handle the second task of the server (periodically checking if a reminder needs to be sent) this was an issue.
http_server.receiveHead()
would block indefinetely, and so the only opportunity the application had to check if it should send me a message was right before or right after listening for a request.
This won’t work, the application would be stuck doing nothing all day if it didn’t get any request in.
Unfortunately the std.http.Server
struct doesn’t have a way check for incoming requests with a timeout.
My next idea was to use some async library, but turns out that the async library from Zig has been removed until a new implementation has been created.
Then I started looking at using the POSIX poll
function, but that was also to no avail. The std.http.Server
struct doesn’t expose its internal file descriptor to the socket.
It only exposes a std.net.Stream
object, but it is not possible to use that together with poll()
.
In the end the solution was to just get rid of the std.http.Server
struct entirely, and make use of sockets directly.
Luckily my use case was very simple, and this application will only requests from my own frontend, so I didn’t have too much about implementing edge cases or handling complicated error scenarios.
This made writing the solution very simple, in fact I think I spent more time looking into my previous 2 attempts than finishing this one.
I ended up with this solution
var server_address = try std.net.Address.resolveIp("127.0.0.1", 8081);
const tpe: u32 = posix.SOCK.STREAM | posix.SOCK.NONBLOCK;
const protocol = posix.IPPROTO.TCP;
const listener = try posix.socket(server_address.any.family, tpe, protocol);
defer posix.close(listener);
try posix.setsockopt(listener, posix.SOL.SOCKET, posix.SO.REUSEADDR, &std.mem.toBytes(@as(c_int, 1)));
try posix.bind(listener, &server_address.any, server_address.getOsSockLen());
try posix.listen(listener, 128);
// Create a timeout using poll
var pollfd = [1]std.posix.pollfd{
.{
.fd = listener,
.events = std.posix.POLL.IN,
.revents = 0,
},
};
while (true) {
// Wait for data with timeout
const poll_result = std.posix.poll(&pollfd, TIMEOUT_MS) catch |err| {
std.debug.print("Poll error: {}\n", .{err});
continue;
};
if (poll_result == 0) {
// No data available.
// Here we can now do the check to see if we need to
// send any new reminders.
// This check will happen at the interval at which we set the timeout for our poll function.
continue;
}
const socket = posix.accept(listener, null, null, 0) catch continue;
defer posix.close(socket);
const read_bytes = posix.read(socket, http_read_buffer) catch continue;
...// further handling of the request
It takes a bit more lines of code to set everything up, but luckily they aren’t that compilcated.
Important details to note are that the socket has been given the flag posix.SOCK.NONBLOCK
to make sure we can use it together with our poll
function.
And later on as you can see, we can create a poll struct and simply pass the socket into it in the var pollfd = [1]std.posix.pollfd{
line.
Now we can call std.posix.poll
and check if any data is available.
If not, we can use this time to check if we have to send any of our reminders.
In the end, there also wasn’t a lot of pain in moving away from the built-in std.http.Server
struct.
I didn’t really care about the headers anyway, and I was able to just simply extract the body from the request and use that to fulfill the request.
So I think this is the easiest to create a simple non-blocking HTTP request handler.
Here are some other links with additional useful information: