HTTP client with method PUT fails with EndOfStream

I’m experimenting with HTTP (not yet HTTPS) clients using some options (Accept headers, methods other than GET or POST). I run the stable 0.15.2 version on a Linux machine.

A program with GET works fine (attached at the end, comments welcome but it works).

A program with PUT (attached at the end) cannot read the body of the reply:

% ./broken-http-put                                                                
error: EndOfStream
/usr/local/zig/zig-x86_64-linux-0.15.2/lib/std/Io/Reader.zig:1671:5: 0x10cf7af in endingRebase (std.zig)
    return error.EndOfStream;
    ^
/usr/local/zig/zig-x86_64-linux-0.15.2/lib/std/Io/Reader.zig:1276:5: 0x115b8d9 in rebase (std.zig)
    return r.vtable.rebase(r, capacity);
    ^
/usr/local/zig/zig-x86_64-linux-0.15.2/lib/std/Io/Reader.zig:1061:5: 0x1147cd9 in fillUnbuffered (std.zig)
    try rebase(r, n);
    ^
/usr/local/zig/zig-x86_64-linux-0.15.2/lib/std/Io/Reader.zig:1049:5: 0x112fc0f in fill (std.zig)
    return fillUnbuffered(r, n);
    ^
/usr/local/zig/zig-x86_64-linux-0.15.2/lib/std/Io/Reader.zig:454:5: 0x111defd in peek (std.zig)
    try r.fill(n);
    ^
/home/stephane/tmp/broken-http-put.zig:45:18: 0x1165d6e in main (broken-http-put.zig)
    const data = try body_reader.peek(blength);
```

I suspect a bug in the standard library but it may be simply that I did not understand something.

The program with the GET method:

```
// Zig example of the use of the standard library HTTP client
// <https://ziglang.org/documentation/0.15.2/std/#std.http.Client> We
// retrieve JSON data from a network API. We are not very paranoid,
// more checks should be created.

const std = @import("std");

// The API we use.
const ref_url = "http://httpbin.org/get"; // For testing

// Some values
const headers_max_size = 8192;
const body_max_size = 65536;
const url_max_size = 1024;

pub fn main() !void {
    // We need an allocator to create a std.http.Client
    var gpa_impl = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa_impl.deinit();
    const gpa = gpa_impl.allocator();

    var client = std.http.Client{ .allocator = gpa };
    defer client.deinit();

    const url = try std.Uri.parse(ref_url);

    var body = std.Io.Writer.Allocating.init(gpa);
    defer body.deinit();

    const mimetype: std.http.Header = .{ .name = "Accept", .value = "application/rpp+json" };
    const headers = [_]std.http.Header{mimetype};

    const options = std.http.Client.RequestOptions{ .extra_headers = &headers };

    // Call the API endpoint. We cannot use std.http.Client.fetch
    // because it apparently does not allow to retrieve the HTTP
    // headers.
    var request = try client.request(std.http.Method.GET, url, options);
    defer request.deinit();

    _ = try request.sendBodiless();
    var rbuffer: [url_max_size]u8 = undefined;
    var response = try request.receiveHead(&rbuffer);

    // Check the HTTP return code
    if (response.head.status != std.http.Status.ok) {
        std.debug.print("Status: {any}\n", .{response.head.status});
        return error.WrongStatusResponse;
    }

    // Read the headers
    var hlength: usize = 0;
    var it = response.head.iterateHeaders();
    var header_list = std.ArrayList([]u8).empty;
    var h: []u8 = undefined;
    var h2: []u8 = undefined;
    while (it.next()) |header| {
        hlength += header.name.len;
        hlength += header.value.len;
        hlength += 2;
        h = try std.mem.concat(gpa, u8, &.{ header.name, ": " });
        h2 = try std.mem.concat(gpa, u8, &.{ h, header.value });
        try header_list.append(gpa, h2);
        gpa.free(h);
    }

    // Display the headers
    std.debug.print("{d} header bytes returned:\n", .{hlength});
    for (header_list.items) |item| {
        std.debug.print("  {s}\n", .{item});
    }
    std.debug.print("\n", .{});

    // Clean
    for (header_list.items) |item| {
        gpa.free(item);
    }
    _ = header_list.deinit(gpa);

    // Read the body.  The response is in JSON so we should here add
    // JSON parsing code.
    var bbuffer: [body_max_size]u8 = undefined;
    const blength = response.head.content_length orelse return error.NoBodyLength;
    // We trust the Content-Length returned by the server…
    const body_reader = response.reader(&bbuffer);
    const data = try body_reader.peek(blength);
    std.debug.print("{d} bytes of body:\n{s}\n", .{ blength, data });
}
```

And here is the program failing with method PUT:


```
// TODO: DOES NOT WORK, it's a minimal example, for bug reporting. It
// fails with EndOfStream while a "curl -d '*' -X PUT
// http://httpbin.org/put" works.

const std = @import("std");

const ref_url = "http://httpbin.org/put";

// Some values
const headers_max_size = 8192;
const body_max_size = 65536;
const url_max_size = 1024;

pub fn main() !void {
    var gpa_impl = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa_impl.deinit();
    const gpa = gpa_impl.allocator();

    var client = std.http.Client{ .allocator = gpa };
    defer client.deinit();

    const url = try std.Uri.parse(ref_url);
    
    var dummy: [1]u8 = undefined;
    dummy[0] = 42;

    var body = std.Io.Writer.Allocating.init(gpa);
    defer body.deinit();

    const options = std.http.Client.RequestOptions{};

    var request = try client.request(std.http.Method.PUT, url, options);
    defer request.deinit();

    _ = try request.sendBodyComplete(&dummy);

    var rbuffer: [url_max_size]u8 = undefined;
    var response = try request.receiveHead(&rbuffer);
        
    // Read the body. This is where it fails.
    var bbuffer: [body_max_size]u8 = undefined;
    const blength = response.head.content_length orelse return error.NoBodyLength;
    // We trust the Content-Length returned by the server…
    const body_reader = response.reader(&bbuffer);
    const data = try body_reader.peek(blength);
    std.debug.print("{d} bytes of body:\n{s}\n", .{blength, data});
    
}
```

It is not specific to the test server I use (`http://httpbin.org/`), I can reproduce it with other servers.

I didn’t look at the code closely but I’m pretty sure EndOfStream is right there in the docs and you should catch that error yourself.

The reason why this is happening is because std.http.Client.Response.reader() is returning std.Io.Reader.ending, which returns error.EndOfStream no matter what.
(You can even verify this with std.debug.assert(body_reader == std.Io.Reader.ending);)
It does this, because this reader() function calls std.http.Method.responseHasBody(), which returns false for your HTTP request’s method. Which is strange, as this function is supposed to return true for .PUT.

It seems as though it’s possible to “hack” this program to work by inserting request.method = .GET; after _ = try request.sendBodyComplete(&dummy);.