Files
Luna Schwalbe 0bbf0461d9 std.http: reliably update reader state
Content length based reading would only set the reader state to `ready`
once it returned EOF, but wrapping readers (such as decompressors)
may stop reading from the underlying source without receiving EOF.
In such cases the http reader state would stay set to
`body_remaining_content_length`, even though the entire body had been
read.

Fixes #30060

Co-authored-by: Andrew Kelley <andre@ziglang.org>
2026-04-11 11:04:24 -07:00

1341 lines
49 KiB
Zig

const builtin = @import("builtin");
const native_endian = builtin.cpu.arch.endian();
const std = @import("std");
const http = std.http;
const mem = std.mem;
const net = std.Io.net;
const Io = std.Io;
const expect = std.testing.expect;
const expectEqual = std.testing.expectEqual;
const expectEqualStrings = std.testing.expectEqualStrings;
const expectError = std.testing.expectError;
test "content length reader state update" {
var in = Io.Reader.fixed("HTTP/1.1 200 OK\r\nContent-Length: 6\r\n\r\nHello!\r\nHTTP/1.1 200 OK\r\n\r\n");
var reader: http.Reader = .{
.in = &in,
.interface = undefined,
.state = .ready,
.max_head_len = 1024,
};
_ = try reader.receiveHead();
var body: [6]u8 = undefined;
_ = try reader.bodyReader(&.{}, .none, body.len).readSliceAll(&body);
try expectEqual(.ready, reader.state);
_ = try reader.receiveHead();
in.seek = 0;
_ = try reader.receiveHead();
try reader.bodyReader(&.{}, .none, body.len).discardAll(body.len);
try expectEqual(.ready, reader.state);
_ = try reader.receiveHead();
}
test "trailers" {
if (builtin.cpu.arch.isPowerPC64() and builtin.mode != .Debug) return error.SkipZigTest; // https://github.com/llvm/llvm-project/issues/171879
if (builtin.os.tag == .openbsd) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/30806
const io = std.testing.io;
const test_server = try createTestServer(io, struct {
fn run(test_server: *TestServer) anyerror!void {
const net_server = &test_server.net_server;
var recv_buffer: [1024]u8 = undefined;
var send_buffer: [1024]u8 = undefined;
var remaining: usize = 1;
while (remaining != 0) : (remaining -= 1) {
var stream = try net_server.accept(io);
defer stream.close(io);
var connection_br = stream.reader(io, &recv_buffer);
var connection_bw = stream.writer(io, &send_buffer);
var server = http.Server.init(&connection_br.interface, &connection_bw.interface);
try expectEqual(.ready, server.reader.state);
var request = try server.receiveHead();
try serve(&request);
try expectEqual(.ready, server.reader.state);
}
}
fn serve(request: *http.Server.Request) !void {
try expectEqualStrings(request.head.target, "/trailer");
var response = try request.respondStreaming(&.{}, .{});
try response.writer.writeAll("Hello, ");
try response.flush();
try response.writer.writeAll("World!\n");
try response.flush();
try response.endChunked(.{
.trailers = &.{
.{ .name = "X-Checksum", .value = "aaaa" },
},
});
}
});
defer test_server.destroy();
const gpa = std.testing.allocator;
var client: http.Client = .{ .allocator = gpa, .io = io };
defer client.deinit();
const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/trailer", .{
test_server.port(),
});
defer gpa.free(location);
const uri = try std.Uri.parse(location);
{
var req = try client.request(.GET, uri, .{});
defer req.deinit();
try req.sendBodiless();
var response = try req.receiveHead(&.{});
{
var it = response.head.iterateHeaders();
const header = it.next().?;
try expectEqualStrings("transfer-encoding", header.name);
try expectEqualStrings("chunked", header.value);
try expectEqual(null, it.next());
}
const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
defer gpa.free(body);
try expectEqualStrings("Hello, World!\n", body);
{
var it = response.iterateTrailers();
const header = it.next().?;
try expectEqualStrings("X-Checksum", header.name);
try expectEqualStrings("aaaa", header.value);
try expectEqual(null, it.next());
}
}
// connection has been kept alive
try expect(client.connection_pool.free_len == 1);
}
test "HTTP server handles a chunked transfer coding request" {
if (builtin.cpu.arch.isPowerPC64() and builtin.mode != .Debug) return error.SkipZigTest; // https://github.com/llvm/llvm-project/issues/171879
if (builtin.os.tag == .openbsd) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/30806
const io = std.testing.io;
const test_server = try createTestServer(io, struct {
fn run(test_server: *TestServer) anyerror!void {
const net_server = &test_server.net_server;
var recv_buffer: [8192]u8 = undefined;
var send_buffer: [500]u8 = undefined;
var stream = try net_server.accept(io);
defer stream.close(io);
var connection_br = stream.reader(io, &recv_buffer);
var connection_bw = stream.writer(io, &send_buffer);
var server = http.Server.init(&connection_br.interface, &connection_bw.interface);
var request = try server.receiveHead();
try expect(request.head.transfer_encoding == .chunked);
var buf: [128]u8 = undefined;
var br = try request.readerExpectContinue(&.{});
const n = try br.readSliceShort(&buf);
try expectEqualStrings("ABCD", buf[0..n]);
try request.respond("message from server!\n", .{
.extra_headers = &.{
.{ .name = "content-type", .value = "text/plain" },
},
.keep_alive = false,
});
}
});
defer test_server.destroy();
const request_bytes =
"POST / HTTP/1.1\r\n" ++
"Content-Type: text/plain\r\n" ++
"Transfer-Encoding: chunked\r\n" ++
"\r\n" ++
"1\r\n" ++
"A\r\n" ++
"1\r\n" ++
"B\r\n" ++
"2\r\n" ++
"CD\r\n" ++
"0\r\n" ++
"\r\n";
const host_name: net.HostName = try .init("127.0.0.1");
var stream = try host_name.connect(io, test_server.port(), .{ .mode = .stream });
defer stream.close(io);
var stream_writer = stream.writer(io, &.{});
try stream_writer.interface.writeAll(request_bytes);
const gpa = std.testing.allocator;
const expected_response =
"HTTP/1.1 200 OK\r\n" ++
"connection: close\r\n" ++
"content-length: 21\r\n" ++
"content-type: text/plain\r\n" ++
"\r\n" ++
"message from server!\n";
var stream_reader = stream.reader(io, &.{});
const response = try stream_reader.interface.allocRemaining(gpa, .limited(expected_response.len + 1));
defer gpa.free(response);
try expectEqualStrings(expected_response, response);
}
test "echo content server" {
if (builtin.cpu.arch.isPowerPC64() and builtin.mode != .Debug) return error.SkipZigTest; // https://github.com/llvm/llvm-project/issues/171879
if (builtin.os.tag == .openbsd) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/30806
const io = std.testing.io;
const test_server = try createTestServer(io, struct {
fn run(test_server: *TestServer) anyerror!void {
const net_server = &test_server.net_server;
var recv_buffer: [1024]u8 = undefined;
var send_buffer: [100]u8 = undefined;
accept: while (!test_server.shutting_down) {
var stream = try net_server.accept(io);
defer stream.close(io);
var connection_br = stream.reader(io, &recv_buffer);
var connection_bw = stream.writer(io, &send_buffer);
var http_server = http.Server.init(&connection_br.interface, &connection_bw.interface);
while (http_server.reader.state == .ready) {
var request = http_server.receiveHead() catch |err| switch (err) {
error.HttpConnectionClosing => continue :accept,
else => |e| return e,
};
if (mem.eql(u8, request.head.target, "/end")) {
return request.respond("", .{ .keep_alive = false });
}
if (request.head.expect) |expect_header_value| {
if (mem.eql(u8, expect_header_value, "garbage")) {
try expectError(error.HttpExpectationFailed, request.readerExpectContinue(&.{}));
request.head.expect = null;
try request.respond("", .{
.keep_alive = false,
.status = .expectation_failed,
});
continue;
}
}
handleRequest(&request) catch |err| {
// This message helps the person troubleshooting determine whether
// output comes from the server thread or the client thread.
std.debug.print("handleRequest failed with '{s}'\n", .{@errorName(err)});
return err;
};
}
}
}
fn handleRequest(request: *http.Server.Request) !void {
//std.debug.print("server received {s} {s} {s}\n", .{
// @tagName(request.head.method),
// @tagName(request.head.version),
// request.head.target,
//});
try expect(mem.startsWith(u8, request.head.target, "/echo-content"));
try expectEqualStrings("text/plain", request.head.content_type.?);
// head strings expire here
const body = try (try request.readerExpectContinue(&.{})).allocRemaining(std.testing.allocator, .unlimited);
defer std.testing.allocator.free(body);
try expectEqualStrings("Hello, World!\n", body);
var response = try request.respondStreaming(&.{}, .{
.content_length = switch (request.head.transfer_encoding) {
.chunked => null,
.none => len: {
try expectEqual(14, request.head.content_length.?);
break :len 14;
},
},
});
try response.flush(); // Test an early flush to send the HTTP headers before the body.
const w = &response.writer;
try w.writeAll("Hello, ");
try w.writeAll("World!\n");
try response.end();
//std.debug.print(" server finished responding\n", .{});
}
});
defer test_server.destroy();
{
var client: http.Client = .{ .allocator = std.testing.allocator, .io = io };
defer client.deinit();
try echoTests(&client, test_server.port());
}
}
test "Server.Request.respondStreaming non-chunked, unknown content-length" {
if (builtin.cpu.arch.isPowerPC64() and builtin.mode != .Debug) return error.SkipZigTest; // https://github.com/llvm/llvm-project/issues/171879
if (builtin.os.tag == .openbsd) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/30806
const io = std.testing.io;
if (builtin.os.tag == .windows) {
// https://github.com/ziglang/zig/issues/21457
return error.SkipZigTest;
}
// In this case, the response is expected to stream until the connection is
// closed, indicating the end of the body.
const test_server = try createTestServer(io, struct {
fn run(test_server: *TestServer) anyerror!void {
const net_server = &test_server.net_server;
var recv_buffer: [1000]u8 = undefined;
var send_buffer: [500]u8 = undefined;
var remaining: usize = 1;
while (remaining != 0) : (remaining -= 1) {
var stream = try net_server.accept(io);
defer stream.close(io);
var connection_br = stream.reader(io, &recv_buffer);
var connection_bw = stream.writer(io, &send_buffer);
var server = http.Server.init(&connection_br.interface, &connection_bw.interface);
try expectEqual(.ready, server.reader.state);
var request = try server.receiveHead();
try expectEqualStrings(request.head.target, "/foo");
var buf: [30]u8 = undefined;
var response = try request.respondStreaming(&buf, .{
.respond_options = .{
.transfer_encoding = .none,
},
});
const w = &response.writer;
for (0..500) |i| {
try w.print("{d}, ah ha ha!\n", .{i});
}
try w.flush();
try response.end();
try expectEqual(.closing, server.reader.state);
}
}
});
defer test_server.destroy();
const request_bytes = "GET /foo HTTP/1.1\r\n\r\n";
const host_name: net.HostName = try .init("127.0.0.1");
var stream = try host_name.connect(io, test_server.port(), .{ .mode = .stream });
defer stream.close(io);
var stream_writer = stream.writer(io, &.{});
try stream_writer.interface.writeAll(request_bytes);
var stream_reader = stream.reader(io, &.{});
const gpa = std.testing.allocator;
const response = try stream_reader.interface.allocRemaining(gpa, .unlimited);
defer gpa.free(response);
var expected_response = std.array_list.Managed(u8).init(gpa);
defer expected_response.deinit();
try expected_response.appendSlice("HTTP/1.1 200 OK\r\nconnection: close\r\n\r\n");
{
var total: usize = 0;
for (0..500) |i| {
var buf: [30]u8 = undefined;
const line = try std.fmt.bufPrint(&buf, "{d}, ah ha ha!\n", .{i});
try expected_response.appendSlice(line);
total += line.len;
}
try expectEqual(7390, total);
}
try expectEqualStrings(expected_response.items, response);
}
test "receiving arbitrary http headers from the client" {
if (builtin.cpu.arch.isPowerPC64() and builtin.mode != .Debug) return error.SkipZigTest; // https://github.com/llvm/llvm-project/issues/171879
if (builtin.os.tag == .openbsd) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/30806
const io = std.testing.io;
const test_server = try createTestServer(io, struct {
fn run(test_server: *TestServer) anyerror!void {
const net_server = &test_server.net_server;
var recv_buffer: [666]u8 = undefined;
var send_buffer: [777]u8 = undefined;
var remaining: usize = 1;
while (remaining != 0) : (remaining -= 1) {
var stream = try net_server.accept(io);
defer stream.close(io);
var connection_br = stream.reader(io, &recv_buffer);
var connection_bw = stream.writer(io, &send_buffer);
var server = http.Server.init(&connection_br.interface, &connection_bw.interface);
try expectEqual(.ready, server.reader.state);
var request = try server.receiveHead();
try expectEqualStrings("/bar", request.head.target);
var it = request.iterateHeaders();
{
const header = it.next().?;
try expectEqualStrings("CoNneCtIoN", header.name);
try expectEqualStrings("close", header.value);
try expect(!it.is_trailer);
}
{
const header = it.next().?;
try expectEqualStrings("aoeu", header.name);
try expectEqualStrings("asdf", header.value);
try expect(!it.is_trailer);
}
try request.respond("", .{});
}
}
});
defer test_server.destroy();
const request_bytes = "GET /bar HTTP/1.1\r\n" ++
"CoNneCtIoN:close\r\n" ++
"aoeu: asdf \r\n" ++
"\r\n";
const host_name: net.HostName = try .init("127.0.0.1");
var stream = try host_name.connect(io, test_server.port(), .{ .mode = .stream });
defer stream.close(io);
var stream_writer = stream.writer(io, &.{});
try stream_writer.interface.writeAll(request_bytes);
var stream_reader = stream.reader(io, &.{});
const gpa = std.testing.allocator;
const response = try stream_reader.interface.allocRemaining(gpa, .unlimited);
defer gpa.free(response);
var expected_response = std.array_list.Managed(u8).init(gpa);
defer expected_response.deinit();
try expected_response.appendSlice("HTTP/1.1 200 OK\r\n");
try expected_response.appendSlice("connection: close\r\n");
try expected_response.appendSlice("content-length: 0\r\n\r\n");
try expectEqualStrings(expected_response.items, response);
}
test "general client/server API coverage" {
if (builtin.cpu.arch.isPowerPC64() and builtin.mode != .Debug) return error.SkipZigTest; // https://github.com/llvm/llvm-project/issues/171879
if (builtin.os.tag == .openbsd) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/30806
const io = std.testing.io;
if (builtin.os.tag == .windows) {
// This test was never passing on Windows.
return error.SkipZigTest;
}
const test_server = try createTestServer(io, struct {
fn run(test_server: *TestServer) anyerror!void {
const net_server = &test_server.net_server;
var recv_buffer: [1024]u8 = undefined;
var send_buffer: [100]u8 = undefined;
outer: while (!test_server.shutting_down) {
var stream = try net_server.accept(io);
defer stream.close(io);
var connection_br = stream.reader(io, &recv_buffer);
var connection_bw = stream.writer(io, &send_buffer);
var http_server = http.Server.init(&connection_br.interface, &connection_bw.interface);
while (http_server.reader.state == .ready) {
var request = http_server.receiveHead() catch |err| switch (err) {
error.HttpConnectionClosing => continue :outer,
else => |e| return e,
};
try handleRequest(&request, net_server.socket.address.getPort());
}
}
}
fn handleRequest(request: *http.Server.Request, listen_port: u16) !void {
const log = std.log.scoped(.server);
const gpa = std.testing.allocator;
log.info("{t} {t} {s}", .{ request.head.method, request.head.version, request.head.target });
const target = try gpa.dupe(u8, request.head.target);
defer gpa.free(target);
const reader = (try request.readerExpectContinue(&.{}));
const body = try reader.allocRemaining(gpa, .unlimited);
defer gpa.free(body);
if (mem.startsWith(u8, target, "/get")) {
var response = try request.respondStreaming(&.{}, .{
.content_length = if (mem.find(u8, target, "?chunked") == null)
14
else
null,
.respond_options = .{
.extra_headers = &.{
.{ .name = "content-type", .value = "text/plain" },
},
},
});
const w = &response.writer;
try w.writeAll("Hello, ");
try w.writeAll("World!\n");
try response.end();
// Writing again would cause an assertion failure.
} else if (mem.startsWith(u8, target, "/large")) {
var response = try request.respondStreaming(&.{}, .{
.content_length = 14 * 1024 + 14 * 10,
});
try response.flush(); // Test an early flush to send the HTTP headers before the body.
const w = &response.writer;
var i: u32 = 0;
while (i < 5) : (i += 1) {
try w.writeAll("Hello, World!\n");
}
var vec: [1][]const u8 = .{"Hello, World!\n"};
try w.writeSplatAll(&vec, 1024);
i = 0;
while (i < 5) : (i += 1) {
try w.writeAll("Hello, World!\n");
}
try response.end();
} else if (mem.eql(u8, target, "/redirect/1")) {
var response = try request.respondStreaming(&.{}, .{
.respond_options = .{
.status = .found,
.extra_headers = &.{
.{ .name = "location", .value = "../../get" },
},
},
});
const w = &response.writer;
try w.writeAll("Hello, ");
try w.writeAll("Redirected!\n");
try response.end();
} else if (mem.eql(u8, target, "/redirect/2")) {
try request.respond("Hello, Redirected!\n", .{
.status = .found,
.extra_headers = &.{
.{ .name = "location", .value = "/redirect/1" },
},
});
} else if (mem.eql(u8, target, "/redirect/3")) {
const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/redirect/2", .{
listen_port,
});
defer gpa.free(location);
try request.respond("Hello, Redirected!\n", .{
.status = .found,
.extra_headers = &.{
.{ .name = "location", .value = location },
},
});
} else if (mem.eql(u8, target, "/redirect/4")) {
try request.respond("Hello, Redirected!\n", .{
.status = .found,
.extra_headers = &.{
.{ .name = "location", .value = "/redirect/3" },
},
});
} else if (mem.eql(u8, target, "/redirect/5")) {
try request.respond("Hello, Redirected!\n", .{
.status = .found,
.extra_headers = &.{
.{ .name = "location", .value = "/%2525" },
},
});
} else if (mem.eql(u8, target, "/%2525")) {
try request.respond("Encoded redirect successful!\n", .{});
} else if (mem.eql(u8, target, "/redirect/invalid")) {
const invalid_port = try getUnusedTcpPort();
const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}", .{invalid_port});
defer gpa.free(location);
try request.respond("", .{
.status = .found,
.extra_headers = &.{
.{ .name = "location", .value = location },
},
});
} else if (mem.eql(u8, target, "/empty")) {
try request.respond("", .{
.extra_headers = &.{
.{ .name = "empty", .value = "" },
},
});
} else {
try request.respond("", .{ .status = .not_found });
}
}
fn getUnusedTcpPort() !u16 {
const addr = try net.IpAddress.parse("127.0.0.1", 0);
var s = try addr.listen(io, .{});
defer s.deinit(io);
return s.socket.address.getPort();
}
});
defer test_server.destroy();
const log = std.log.scoped(.client);
const gpa = std.testing.allocator;
var client: http.Client = .{ .allocator = gpa, .io = io };
defer client.deinit();
const port = test_server.port();
{ // read content-length response
const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/get", .{port});
defer gpa.free(location);
const uri = try std.Uri.parse(location);
log.info("{s}", .{location});
var redirect_buffer: [1024]u8 = undefined;
var req = try client.request(.GET, uri, .{});
defer req.deinit();
try req.sendBodiless();
var response = try req.receiveHead(&redirect_buffer);
try expectEqualStrings("text/plain", response.head.content_type.?);
const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
defer gpa.free(body);
try expectEqualStrings("Hello, World!\n", body);
}
// connection has been kept alive
try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
{ // read large content-length response
const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/large", .{port});
defer gpa.free(location);
const uri = try std.Uri.parse(location);
log.info("{s}", .{location});
var redirect_buffer: [1024]u8 = undefined;
var req = try client.request(.GET, uri, .{});
defer req.deinit();
try req.sendBodiless();
var response = try req.receiveHead(&redirect_buffer);
const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
defer gpa.free(body);
try expectEqual(@as(usize, 14 * 1024 + 14 * 10), body.len);
}
// connection has been kept alive
try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
{ // send head request and not read chunked
const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/get", .{port});
defer gpa.free(location);
const uri = try std.Uri.parse(location);
log.info("{s}", .{location});
var redirect_buffer: [1024]u8 = undefined;
var req = try client.request(.HEAD, uri, .{});
defer req.deinit();
try req.sendBodiless();
var response = try req.receiveHead(&redirect_buffer);
try expectEqualStrings("text/plain", response.head.content_type.?);
try expectEqual(14, response.head.content_length.?);
const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
defer gpa.free(body);
try expectEqualStrings("", body);
}
// connection has been kept alive
try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
{ // read chunked response
const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/get?chunked", .{port});
defer gpa.free(location);
const uri = try std.Uri.parse(location);
log.info("{s}", .{location});
var redirect_buffer: [1024]u8 = undefined;
var req = try client.request(.GET, uri, .{});
defer req.deinit();
try req.sendBodiless();
var response = try req.receiveHead(&redirect_buffer);
try expectEqualStrings("text/plain", response.head.content_type.?);
const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
defer gpa.free(body);
try expectEqualStrings("Hello, World!\n", body);
}
// connection has been kept alive
try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
{ // send head request and not read chunked
const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/get?chunked", .{port});
defer gpa.free(location);
const uri = try std.Uri.parse(location);
log.info("{s}", .{location});
var redirect_buffer: [1024]u8 = undefined;
var req = try client.request(.HEAD, uri, .{});
defer req.deinit();
try req.sendBodiless();
var response = try req.receiveHead(&redirect_buffer);
try expectEqualStrings("text/plain", response.head.content_type.?);
try expect(response.head.transfer_encoding == .chunked);
const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
defer gpa.free(body);
try expectEqualStrings("", body);
}
// connection has been kept alive
try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
{ // read content-length response with connection close
const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/get", .{port});
defer gpa.free(location);
const uri = try std.Uri.parse(location);
log.info("{s}", .{location});
var redirect_buffer: [1024]u8 = undefined;
var req = try client.request(.GET, uri, .{
.keep_alive = false,
});
defer req.deinit();
try req.sendBodiless();
var response = try req.receiveHead(&redirect_buffer);
try expectEqualStrings("text/plain", response.head.content_type.?);
const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
defer gpa.free(body);
try expectEqualStrings("Hello, World!\n", body);
}
// connection has been closed
try expect(client.connection_pool.free_len == 0);
{ // handle empty header field value
const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/empty", .{port});
defer gpa.free(location);
const uri = try std.Uri.parse(location);
log.info("{s}", .{location});
var redirect_buffer: [1024]u8 = undefined;
var req = try client.request(.GET, uri, .{
.extra_headers = &.{
.{ .name = "empty", .value = "" },
},
});
defer req.deinit();
try req.sendBodiless();
var response = try req.receiveHead(&redirect_buffer);
try std.testing.expectEqual(.ok, response.head.status);
var it = response.head.iterateHeaders();
{
const header = it.next().?;
try expect(!it.is_trailer);
try expectEqualStrings("content-length", header.name);
try expectEqualStrings("0", header.value);
}
const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
defer gpa.free(body);
try expectEqualStrings("", body);
{
const header = it.next().?;
try expect(!it.is_trailer);
try expectEqualStrings("empty", header.name);
try expectEqualStrings("", header.value);
}
try expectEqual(null, it.next());
}
// connection has been kept alive
try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
{ // relative redirect
const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/redirect/1", .{port});
defer gpa.free(location);
const uri = try std.Uri.parse(location);
log.info("{s}", .{location});
var redirect_buffer: [1024]u8 = undefined;
var req = try client.request(.GET, uri, .{});
defer req.deinit();
try req.sendBodiless();
var response = try req.receiveHead(&redirect_buffer);
const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
defer gpa.free(body);
try expectEqualStrings("Hello, World!\n", body);
}
// connection has been kept alive
try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
{ // redirect from root
const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/redirect/2", .{port});
defer gpa.free(location);
const uri = try std.Uri.parse(location);
log.info("{s}", .{location});
var redirect_buffer: [1024]u8 = undefined;
var req = try client.request(.GET, uri, .{});
defer req.deinit();
try req.sendBodiless();
var response = try req.receiveHead(&redirect_buffer);
const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
defer gpa.free(body);
try expectEqualStrings("Hello, World!\n", body);
}
// connection has been kept alive
try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
{ // absolute redirect
const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/redirect/3", .{port});
defer gpa.free(location);
const uri = try std.Uri.parse(location);
log.info("{s}", .{location});
var redirect_buffer: [1024]u8 = undefined;
var req = try client.request(.GET, uri, .{});
defer req.deinit();
try req.sendBodiless();
var response = try req.receiveHead(&redirect_buffer);
const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
defer gpa.free(body);
try expectEqualStrings("Hello, World!\n", body);
}
// connection has been kept alive
try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
{ // too many redirects
const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/redirect/4", .{port});
defer gpa.free(location);
const uri = try std.Uri.parse(location);
log.info("{s}", .{location});
var redirect_buffer: [1024]u8 = undefined;
var req = try client.request(.GET, uri, .{});
defer req.deinit();
try req.sendBodiless();
if (req.receiveHead(&redirect_buffer)) |_| {
return error.TestFailed;
} else |err| switch (err) {
error.TooManyHttpRedirects => {},
else => return err,
}
}
{ // redirect to encoded url
const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/redirect/5", .{port});
defer gpa.free(location);
const uri = try std.Uri.parse(location);
log.info("{s}", .{location});
var redirect_buffer: [1024]u8 = undefined;
var req = try client.request(.GET, uri, .{});
defer req.deinit();
try req.sendBodiless();
var response = try req.receiveHead(&redirect_buffer);
const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
defer gpa.free(body);
try expectEqualStrings("Encoded redirect successful!\n", body);
}
// connection has been kept alive
try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
{ // check client without segfault by connection error after redirection
const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/redirect/invalid", .{port});
defer gpa.free(location);
const uri = try std.Uri.parse(location);
log.info("{s}", .{location});
var redirect_buffer: [1024]u8 = undefined;
var req = try client.request(.GET, uri, .{});
defer req.deinit();
try req.sendBodiless();
const result = req.receiveHead(&redirect_buffer);
// a proxy without an upstream is likely to return a 5xx status.
if (client.http_proxy == null) {
try expectError(error.ConnectionRefused, result); // expects not segfault but the regular error
}
}
// connection has been kept alive
try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
}
test "Server streams both reading and writing" {
if (builtin.cpu.arch.isPowerPC64() and builtin.mode != .Debug) return error.SkipZigTest; // https://github.com/llvm/llvm-project/issues/171879
if (builtin.os.tag == .openbsd) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/30806
const io = std.testing.io;
const test_server = try createTestServer(io, struct {
fn run(test_server: *TestServer) anyerror!void {
const net_server = &test_server.net_server;
var recv_buffer: [1024]u8 = undefined;
var send_buffer: [777]u8 = undefined;
var stream = try net_server.accept(io);
defer stream.close(io);
var connection_br = stream.reader(io, &recv_buffer);
var connection_bw = stream.writer(io, &send_buffer);
var server = http.Server.init(&connection_br.interface, &connection_bw.interface);
var request = try server.receiveHead();
var read_buffer: [100]u8 = undefined;
var br = try request.readerExpectContinue(&read_buffer);
var response = try request.respondStreaming(&.{}, .{
.respond_options = .{
.transfer_encoding = .none, // Causes keep_alive=false
},
});
const w = &response.writer;
while (true) {
try response.flush();
const buf = br.peekGreedy(1) catch |err| switch (err) {
error.EndOfStream => break,
error.ReadFailed => return error.ReadFailed,
};
br.toss(buf.len);
for (buf) |*b| b.* = std.ascii.toUpper(b.*);
try w.writeAll(buf);
}
try response.end();
}
});
defer test_server.destroy();
var client: http.Client = .{
.allocator = std.testing.allocator,
.io = io,
};
defer client.deinit();
var redirect_buffer: [555]u8 = undefined;
var req = try client.request(.POST, .{
.scheme = "http",
.host = .{ .raw = "127.0.0.1" },
.port = test_server.port(),
.path = .{ .percent_encoded = "/" },
}, .{});
defer req.deinit();
req.transfer_encoding = .chunked;
var body_writer = try req.sendBody(&.{});
var response = try req.receiveHead(&redirect_buffer);
try body_writer.writer.writeAll("one ");
try body_writer.writer.writeAll("fish");
try body_writer.end();
const body = try response.reader(&.{}).allocRemaining(std.testing.allocator, .unlimited);
defer std.testing.allocator.free(body);
try expectEqualStrings("ONE FISH", body);
}
fn echoTests(client: *http.Client, port: u16) !void {
const gpa = std.testing.allocator;
var location_buffer: [100]u8 = undefined;
{ // send content-length request
const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/echo-content", .{port});
defer gpa.free(location);
const uri = try std.Uri.parse(location);
var redirect_buffer: [1024]u8 = undefined;
var req = try client.request(.POST, uri, .{
.extra_headers = &.{
.{ .name = "content-type", .value = "text/plain" },
},
});
defer req.deinit();
req.transfer_encoding = .{ .content_length = 14 };
var body_writer = try req.sendBody(&.{});
try body_writer.writer.writeAll("Hello, ");
try body_writer.writer.writeAll("World!\n");
try body_writer.end();
var response = try req.receiveHead(&redirect_buffer);
const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
defer gpa.free(body);
try expectEqualStrings("Hello, World!\n", body);
}
// connection has been kept alive
try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
{ // send chunked request
const uri = try std.Uri.parse(try std.fmt.bufPrint(
&location_buffer,
"http://127.0.0.1:{d}/echo-content",
.{port},
));
var redirect_buffer: [1024]u8 = undefined;
var req = try client.request(.POST, uri, .{
.extra_headers = &.{
.{ .name = "content-type", .value = "text/plain" },
},
});
defer req.deinit();
req.transfer_encoding = .chunked;
var body_writer = try req.sendBody(&.{});
try body_writer.writer.writeAll("Hello, ");
try body_writer.writer.writeAll("World!\n");
try body_writer.end();
var response = try req.receiveHead(&redirect_buffer);
const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
defer gpa.free(body);
try expectEqualStrings("Hello, World!\n", body);
}
// connection has been kept alive
try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
{ // Client.fetch()
const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/echo-content#fetch", .{port});
defer gpa.free(location);
var body: std.Io.Writer.Allocating = .init(gpa);
defer body.deinit();
try body.ensureUnusedCapacity(64);
const res = try client.fetch(.{
.location = .{ .url = location },
.method = .POST,
.payload = "Hello, World!\n",
.extra_headers = &.{
.{ .name = "content-type", .value = "text/plain" },
},
.response_writer = &body.writer,
});
try expectEqual(.ok, res.status);
try expectEqualStrings("Hello, World!\n", body.written());
}
{ // expect: 100-continue
const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/echo-content#expect-100", .{port});
defer gpa.free(location);
const uri = try std.Uri.parse(location);
var redirect_buffer: [1024]u8 = undefined;
var req = try client.request(.POST, uri, .{
.extra_headers = &.{
.{ .name = "expect", .value = "100-continue" },
.{ .name = "content-type", .value = "text/plain" },
},
});
defer req.deinit();
req.transfer_encoding = .chunked;
var body_writer = try req.sendBody(&.{});
try body_writer.writer.writeAll("Hello, ");
try body_writer.writer.writeAll("World!\n");
try body_writer.end();
var response = try req.receiveHead(&redirect_buffer);
try expectEqual(.ok, response.head.status);
const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
defer gpa.free(body);
try expectEqualStrings("Hello, World!\n", body);
}
{ // expect: garbage
const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/echo-content#expect-garbage", .{port});
defer gpa.free(location);
const uri = try std.Uri.parse(location);
var redirect_buffer: [1024]u8 = undefined;
var req = try client.request(.POST, uri, .{
.extra_headers = &.{
.{ .name = "content-type", .value = "text/plain" },
.{ .name = "expect", .value = "garbage" },
},
});
defer req.deinit();
req.transfer_encoding = .chunked;
var body_writer = try req.sendBody(&.{});
try body_writer.flush();
var response = try req.receiveHead(&redirect_buffer);
try expectEqual(.expectation_failed, response.head.status);
_ = try response.reader(&.{}).discardRemaining();
}
}
const TestServer = struct {
io: Io,
shutting_down: bool,
server_thread: std.Thread,
net_server: net.Server,
fn destroy(self: *@This()) void {
const io = self.io;
self.shutting_down = true;
var stream = self.net_server.socket.address.connect(io, .{ .mode = .stream }) catch
@panic("shutdown failure");
stream.close(io);
self.server_thread.join();
self.net_server.deinit(io);
std.testing.allocator.destroy(self);
}
fn port(self: @This()) u16 {
return self.net_server.socket.address.getPort();
}
};
fn createTestServer(io: Io, S: type) !*TestServer {
if (builtin.single_threaded) return error.SkipZigTest;
if (builtin.zig_backend == .stage2_llvm and native_endian == .big) {
// https://github.com/ziglang/zig/issues/13782
return error.SkipZigTest;
}
const address = try net.IpAddress.parse("127.0.0.1", 0);
const gpa = std.testing.allocator;
const test_server = try gpa.create(TestServer);
errdefer gpa.destroy(test_server);
var net_server = try address.listen(io, .{ .reuse_address = true });
errdefer net_server.deinit(io);
// populate `test_server` first so `S.run` can use it
test_server.* = .{
.io = io,
.net_server = net_server,
.shutting_down = false,
.server_thread = undefined, // set below
};
test_server.server_thread = try .spawn(.{}, S.run, .{test_server});
errdefer comptime unreachable;
return test_server;
}
test "redirect to different connection" {
if (builtin.cpu.arch.isPowerPC64() and builtin.mode != .Debug) return error.SkipZigTest; // https://github.com/llvm/llvm-project/issues/171879
if (builtin.os.tag == .openbsd) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/30806
const io = std.testing.io;
const test_server_new = try createTestServer(io, struct {
fn run(test_server: *TestServer) anyerror!void {
const net_server = &test_server.net_server;
var recv_buffer: [888]u8 = undefined;
var send_buffer: [777]u8 = undefined;
var stream = try net_server.accept(io);
defer stream.close(io);
var connection_br = stream.reader(io, &recv_buffer);
var connection_bw = stream.writer(io, &send_buffer);
var server = http.Server.init(&connection_br.interface, &connection_bw.interface);
var request = try server.receiveHead();
try expectEqualStrings(request.head.target, "/ok");
try request.respond("good job, you pass", .{});
}
});
defer test_server_new.destroy();
const global = struct {
var other_port: ?u16 = null;
};
global.other_port = test_server_new.port();
const test_server_orig = try createTestServer(io, struct {
fn run(test_server: *TestServer) anyerror!void {
const net_server = &test_server.net_server;
var recv_buffer: [999]u8 = undefined;
var send_buffer: [100]u8 = undefined;
var stream = try net_server.accept(io);
defer stream.close(io);
var loc_buf: [50]u8 = undefined;
const new_loc = try std.fmt.bufPrint(&loc_buf, "http://127.0.0.1:{d}/ok", .{
global.other_port.?,
});
var connection_br = stream.reader(io, &recv_buffer);
var connection_bw = stream.writer(io, &send_buffer);
var server = http.Server.init(&connection_br.interface, &connection_bw.interface);
var request = try server.receiveHead();
try expectEqualStrings(request.head.target, "/help");
try request.respond("", .{
.status = .found,
.extra_headers = &.{
.{ .name = "location", .value = new_loc },
},
});
}
});
defer test_server_orig.destroy();
const gpa = std.testing.allocator;
var client: http.Client = .{
.allocator = gpa,
.io = io,
};
defer client.deinit();
var loc_buf: [100]u8 = undefined;
const location = try std.fmt.bufPrint(&loc_buf, "http://127.0.0.1:{d}/help", .{
test_server_orig.port(),
});
const uri = try std.Uri.parse(location);
{
var redirect_buffer: [666]u8 = undefined;
var req = try client.request(.GET, uri, .{});
defer req.deinit();
try req.sendBodiless();
var response = try req.receiveHead(&redirect_buffer);
var reader = response.reader(&.{});
const body = try reader.allocRemaining(gpa, .unlimited);
defer gpa.free(body);
try expectEqualStrings("good job, you pass", body);
}
}
test "boot failed connections from the pool" {
if (builtin.cpu.arch.isPowerPC64() and builtin.mode != .Debug) return error.SkipZigTest; // https://github.com/llvm/llvm-project/issues/171879
if (builtin.os.tag == .openbsd) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/30806
const io = std.testing.io;
const gpa = std.testing.allocator;
const test_server_orig = try createTestServer(io, struct {
fn run(test_server: *TestServer) anyerror!void {
const net_server = &test_server.net_server;
var recv_buffer: [500]u8 = undefined;
var send_buffer: [500]u8 = undefined;
accept: while (!test_server.shutting_down) {
var stream = try net_server.accept(io);
defer stream.close(io);
for (0..2) |i| {
var connection_br = stream.reader(io, &recv_buffer);
var connection_bw = stream.writer(io, &send_buffer);
var server = http.Server.init(&connection_br.interface, &connection_bw.interface);
var request = server.receiveHead() catch |err| switch (err) {
error.HttpConnectionClosing => continue :accept,
else => |e| return e,
};
if (i == 0) try request.respond("hello", .{});
}
}
}
});
defer test_server_orig.destroy();
var client: http.Client = .{
.allocator = gpa,
.io = io,
};
defer client.deinit();
var loc_buf: [100]u8 = undefined;
const location = try std.fmt.bufPrint(&loc_buf, "http://127.0.0.1:{d}/", .{
test_server_orig.port(),
});
const uri = try std.Uri.parse(location);
{
const response = try client.fetch(.{ .location = .{ .uri = uri } });
try expectEqual(.ok, response.status);
}
{
try expectError(error.HttpConnectionClosing, client.fetch(.{ .location = .{ .uri = uri } }));
}
{
const response = try client.fetch(.{ .location = .{ .uri = uri } });
try expectEqual(.ok, response.status);
}
{
try expectError(error.HttpConnectionClosing, client.fetch(.{ .location = .{ .uri = uri } }));
}
}