Skip to content

Instantly share code, notes, and snippets.

@Zorgatone
Last active October 18, 2025 23:31
Show Gist options
  • Save Zorgatone/968ce86711aecea984a2c4a9771eed5f to your computer and use it in GitHub Desktop.
Save Zorgatone/968ce86711aecea984a2c4a9771eed5f to your computer and use it in GitHub Desktop.
Zig 0.15.1 http-client GET example
const builtin = @import("builtin");
const std = @import("std");
// This version won't read/print headers, just the response
pub fn main() !void {
var writer_buffer: [8 * 1024]u8 = undefined;
var redirect_buffer: [8 * 1024]u8 = undefined;
var writer = std.fs.File.stdout().writer(&writer_buffer);
var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
defer switch (builtin.mode) {
.Debug => std.debug.assert(debug_allocator.deinit() == .ok),
.ReleaseFast, .ReleaseSmall, .ReleaseSafe => {
// Nothing
},
};
const allocator = switch (builtin.mode) {
.Debug => debug_allocator.allocator(),
.ReleaseFast, .ReleaseSmall, .ReleaseSafe => std.heap.smp_allocator,
};
const uri = try std.Uri.parse("https://postman-echo.com/get");
var client: std.http.Client = .{ .allocator = allocator };
defer client.deinit();
const result = try client.fetch(.{
.location = .{ .uri = uri },
.method = .GET,
.redirect_buffer = &redirect_buffer,
.response_writer = &writer.interface,
});
if (builtin.mode == .Debug) {
std.debug.assert(result.status == .ok);
}
try writer.interface.flush();
}
const builtin = @import("builtin");
const std = @import("std");
// Manual http request printing also the response headers (won't work if chunked or compressed)
pub fn main() !void {
var writer_buffer: [8 * 1024]u8 = undefined;
var redirect_buffer: [8 * 1024]u8 = undefined;
var transfer_buffer: [8 * 1024]u8 = undefined;
var reader_buffer: [8 * 1024]u8 = undefined;
var writer = std.fs.File.stdout().writer(&writer_buffer);
var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
defer switch (builtin.mode) {
.Debug => std.debug.assert(debug_allocator.deinit() == .ok),
.ReleaseFast, .ReleaseSmall, .ReleaseSafe => {
// Nothing
},
};
const allocator = switch (builtin.mode) {
.Debug => debug_allocator.allocator(),
.ReleaseFast, .ReleaseSmall, .ReleaseSafe => std.heap.smp_allocator,
};
const uri = try std.Uri.parse("https://postman-echo.com/get");
var client: std.http.Client = .{ .allocator = allocator };
defer client.deinit();
var request = try client.request(.GET, uri, .{});
defer request.deinit();
try request.sendBodiless();
const response = try request.receiveHead(&redirect_buffer);
_ = try writer.interface.write(response.head.bytes);
const content_length = response.head.content_length;
const reader = request.reader.bodyReader(&transfer_buffer, .none, content_length);
var done = false;
var bytes_read: usize = 0;
while (!done) {
const size = try reader.readSliceShort(&reader_buffer);
if (size > 0) {
bytes_read += size;
_ = try writer.interface.write(reader_buffer[0..size]);
}
if (content_length) |c_len| {
if (bytes_read >= c_len) {
done = true;
}
}
if (size < reader_buffer.len) {
done = true;
}
}
try writer.interface.flush();
}
@Numeez
Copy link

Numeez commented Sep 11, 2025

Hey @Zorgatone
Thank you for this code
It helped me
Can you answer one thing for me and that is:
What will we do if the content length header is not present

@Zorgatone
Copy link
Author

Hey @Zorgatone Thank you for this code It helped me Can you answer one thing for me and that is: What will we do if the content length header is not present

@Numeez AFAIK HTTP/1.0 requires the content-length header as mandatory: https://www.w3.org/Protocols/HTTP/1.0/draft-ietf-http-spec.html#Content-Length

A valid Content-Length field value is required on all HTTP/1.0 request messages containing an entity body

I'll improve this snippet, I found out I can access headers of the response in a better way without parsing it RAW

@Zorgatone
Copy link
Author

@Numeez done, updated the example with proper usage of the headers, response.head.content_length is optional and request.reader.bodyReader accepts an optional so this will work also when the Content-Length header is missing.

I'm not manually parsing the headers to look for the content-length, and also now I'm reading the response in a loop until the connection is closed or content-length bytes have been read (if present)

@Zorgatone
Copy link
Author

I believe there should be an even better way using just http_client.fetch() and passing all that's needed as options in a struct, and takes care of most of the code I've manually written in this example

@Zorgatone
Copy link
Author

@Numeez added an example with client.fetch() in file http-fetch.zig while keeping the full manual example in file http-get.zig

@Numeez
Copy link

Numeez commented Sep 15, 2025

@Zorgatone

Thank you for the update
The new snippet is a better way of dealing with response irrespective of underlying handling of headers

@definitepotato
Copy link

definitepotato commented Sep 17, 2025

@Zorgatone I have a couple questions about your approach out of genuine curiosity, trying to learn more about zig.

1>>
http-fetch.zig:
25: const uri = try std.Uri.parse("https://postman-echo.com/get");

Is there a reason to parse the url here as a Uri instead of letting the call to fetch() do it?

In Client.zig it appears the call to fetch() already parses the Uri and does the work:

pub fn fetch(client: *Client, options: FetchOptions) FetchError!FetchResult {
    const uri = switch (options.location) {
        .url => |u| try Uri.parse(u),
        .uri => |u| u,
    };
...

2>>
http-fetch.zig:
11: var writer = std.fs.File.stdout().writer(&writer_buffer);

Is there a reason why you chose a file descriptor here? With 0.15.1 we get std.Io which gives a lot of functions for "free" that are quite useful and you can always use a std.heap.FixedBufferAllocator with std.Io.Writer.Allocating.init() to keep it on the stack if you don't want to allocate to the heap.

@Zorgatone
Copy link
Author

Zorgatone commented Sep 22, 2025

@definitepotato

  1. well, this was an easy example so you could go with both approaches. Since it's going to use Uri.parse anyway, I did it earlier, in case I want to reuse the same uri for multiple fetch/http requests.

  2. Not sure I understand your question, what do you mean why am I using a file descriptor here? I am using the new std.Io in this example (all readers, writers also implement the std.Io interface now, ie. writer.interface and reader.interface). Turns out standard output (stdout) is also implemented as a file descriptor (macOs, linux). Your example writer.Allocating + heap.FixedBufferAllocator would make sense if you wanted to write to some array (buffer) in memory you could then read, or chunked.buffered stream/pipe to an actual file on the file system (instead of stdout). Here I'm only printing (streaming/piping) to the console (stdout) to debug the result of the HTTP request (body).
    Did I answer your question, or did you want to know something else?

@definitepotato
Copy link

@Zorgatone

Thanks for this. #1 makes sense. #2 your perspective is helping me understand zig better. Your explanation makes sense to me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment