I’m working with the code below. I’ve been digging into the Client.zig file and looking at how the fetch call is made and haven’t been able to figure out why this is happening. When I look at the server side where the API the call is made, I can see the API call being made without the payload (only \r\n in the body), though I can validate that up to the point fetch() is called, there is a valid payload in the FetchOptions.
const std = @import("std");
const print = std.debug.print;
const assert = std.debug.assert;
pub fn main() !void {
const url: []const u8 = "https://example.com";
const payload =
\\{
\\ "name": "Alice",
\\ "handle": "Alice",
\\ "age": "23",
\\ "id": "12345"
\\}
;
const allocator = std.heap.page_allocator;
var client = std.http.Client{ .allocator = allocator };
defer client.deinit();
const payload_len = try std.fmt.allocPrint(allocator, "{}", .{payload.len});
const header = &[_]std.http.Header{
.{ .name = "Referer", .value = url },
.{ .name = "Content-Type", .value = "application/json" },
.{ .name = "Content-Length", .value = payload_len },
.{ .name = "X-Api-Key", .value = "abc123" },
};
const headers = try allocator.alloc(std.http.Header, header.len);
defer allocator.free(headers);
for (header, 0..) |h, i| {
headers[i] = h;
}
var res_body: std.Io.Writer.Allocating = .init(allocator);
defer res_body.deinit();
const fetch_res = try client.fetch(.{
.location = .{ .url = url },
.method = .POST,
.payload = payload,
.response_writer = &res_body.writer,
.extra_headers = headers,
});
if (fetch_res.status != .ok) {
print("error fetching: {s}\nstatus: {}\n\n", .{ url, fetch_res.status });
}
try res_body.writer.flush();
const res_body_owned = try res_body.toOwnedSlice();
print("{s}\n", .{res_body_owned});
}
vulpesx
September 19, 2025, 11:56pm
2
so it’s hanging in fetch?
Possibly relevant:
opened 04:15PM - 19 Sep 25 UTC
bug
### Zig Version
0.15.1
### Steps to Reproduce and Observed Behavior
The `http… .Client` will hang on a HTTPS POST when the size of the request body exceeds the write buffer size:
``` zig
const std = @import("std");
pub fn main() !void {
var http_client: std.http.Client = .{ .allocator = std.heap.smp_allocator };
std.debug.assert(http_client.write_buffer_size == 1024);
{
var req = try http_client.request(.POST, try std.Uri.parse("https://www.google.com/"), .{});
defer req.deinit();
var buf: [1024]u8 = undefined;
std.debug.assert(buf.len <= http_client.write_buffer_size);
try req.sendBodyComplete(&buf);
}
{
var req = try http_client.request(.POST, try std.Uri.parse("https://www.google.com/"), .{});
defer req.deinit();
var buf: [1025]u8 = undefined;
std.debug.assert(buf.len > http_client.write_buffer_size);
try req.sendBodyComplete(&buf); // this one hangs
}
}
```
I found that it is spinning on [this line](https://github.com/ziglang/zig/blob/master/lib/std/Io/Writer.zig#L317) in `defaultFlush`, calling `tls.Client.drain`. In `drain`, it does not consume any input from `data` if [the first buffer is too small](https://github.com/ziglang/zig/blob/bc921fec122f4fa9e9a3957657c6ee74d6870f5b/lib/std/crypto/tls/Client.zig#L945). Since this `drain` doesn't buffer data, I don't believe it's compatible with `std.Io.defaultFlush`, which is [used implicitly by the `http.BodyWriter`](https://github.com/ziglang/zig/blob/bc921fec122f4fa9e9a3957657c6ee74d6870f5b/lib/std/http/Client.zig#L945-L946).
### Expected Behavior
The example program should not hang.
opened 02:46PM - 07 Sep 25 UTC
bug
standard library
### Zig Version
0.16.0-dev.195+ac42eaaad
### Steps to Reproduce and Observed B… ehavior
The HTTP Client will try to read/discard response body even if no body is expected to appear, which will hang forever/until the server closes the connection. Which can take a while with `Connection: keep-alive`.
```zig
const std = @import("std");
test {
var client: std.http.Client = .{
.allocator = std.testing.allocator,
};
defer client.deinit();
for ([_]bool{ false, true }) |keep_alive| {// The problem does not happen when keep_alive is false
for ([_][]const u8{ "http", "https" }) |protocol| {
const host_url = "httpbun.org";// Arbitrary service to reproduce the problem
const method: std.http.Method = .DELETE;// Zig expects DELETE response to have a body
const path = "status/204";// No Content, causes the server to return no Content-Length header
var response: std.Io.Writer.Allocating = .init(std.testing.allocator);
defer response.deinit();
var url_buffer: [512]u8 = undefined;
const result = try client.fetch(.{
.response_writer = &response.writer,
.location = .{ .url = try std.fmt.bufPrint(&url_buffer, "{s}://{s}/{s}", .{ protocol, host_url, path }) },
.method = method,
.payload = null,
.keep_alive = keep_alive,
});
try std.testing.expectEqual(.no_content, result.status);
try std.testing.expectEqual(0, response.written().len);
std.debug.print("Done {s} {}\n", .{protocol, keep_alive});
}
}
}
```
In this example, when `keep_alive` is `false`, everything will work as expected, because while Client will attempt to errorniously discard the body, the server will close the connection and no hang will occur.
But with `keep_alive` `true`, the connection will stay open, with no traffic. Because the response code is `204 No Content`, the response will not even contain `Content-Length: 0` header. The Client will still assume that the response body is everything left in the stream (despite response headers containing HTTP 1.1 and Connection: keep-alive). It will then try to read the body forever and hang [here](https://github.com/ziglang/zig/blob/34409635ad3a5aa199adde1cabbeadb1ac6e256c/lib/std/http/Client.zig#L1809).
### Expected Behavior
`zig.http.Client` (probably function `bodyReader` in particular) should respect HTTP 1.1 specification on when the response contains the body.
> For response messages, whether or not a message-body is included with a message is dependent on both the request method and the response status code. All responses to the HEAD request method MUST NOT include a message-body, even though the presence of entity-header fields might lead one to believe they do. All 1xx (informational), 204 (no content), and 304 (not modified) responses MUST NOT include a message-body. All other responses do include a message-body, although it MAY be of zero length. ([RFC-2616, section 4.3](https://www.rfc-editor.org/rfc/rfc2616#section-4.3))
Thanks for this. It doesn’t hang. I’m able to get a response for GET and POST requests. The problem is when I look on the API server side the body is empty. In my code I’m sending a body. I’m not sure why it sends bodiless, it should only do that if the payload is null.
vulpesx
September 20, 2025, 1:10am
4
in that case you should be sharing the server code, or a cut down example
I guess I should investigate a bit more. I didn’t write the server side it’s an application that I installed. Here’s an SDK I was working on in Zig for this same application GitHub - definitepotato/espocrmz: An API client for EspoCRM in Zig. .
Using zig 0.14 this SDK works and I can say with certainty the server side works, and to the best of my ability I can determine using the new std.Io interface and zig 0.15 the payload isn’t being sent when calling fetch even though the payload is defined and has a value in my code.
I will try without calling fetch and doing it manually step-by-step to see how that works out. This could all be my error.