aboutsummaryrefslogtreecommitdiff
path: root/lib/std/http/test.zig
diff options
context:
space:
mode:
authorAndrew Kelley <andrew@ziglang.org>2024-02-23 17:41:38 -0800
committerGitHub <noreply@github.com>2024-02-23 17:41:38 -0800
commitcfce81f7d5f11ab93b2d5fd26df41edf967f333b (patch)
tree11e52ad0a44620f4a4519683abd945146c11b312 /lib/std/http/test.zig
parent7230b68b350b16c637e84f3ff224be24d23214ce (diff)
parent653d4158cdcb20be82ff525e122277064e6acb92 (diff)
downloadzig-cfce81f7d5f11ab93b2d5fd26df41edf967f333b.tar.gz
zig-cfce81f7d5f11ab93b2d5fd26df41edf967f333b.zip
Merge pull request #18955 from ziglang/std.http.Server
take std.http in a different direction
Diffstat (limited to 'lib/std/http/test.zig')
-rw-r--r--lib/std/http/test.zig995
1 files changed, 995 insertions, 0 deletions
diff --git a/lib/std/http/test.zig b/lib/std/http/test.zig
new file mode 100644
index 0000000000..e36b0cdf28
--- /dev/null
+++ b/lib/std/http/test.zig
@@ -0,0 +1,995 @@
+const builtin = @import("builtin");
+const std = @import("std");
+const http = std.http;
+const mem = std.mem;
+const native_endian = builtin.cpu.arch.endian();
+const expect = std.testing.expect;
+const expectEqual = std.testing.expectEqual;
+const expectEqualStrings = std.testing.expectEqualStrings;
+const expectError = std.testing.expectError;
+
+test "trailers" {
+ const test_server = try createTestServer(struct {
+ fn run(net_server: *std.net.Server) anyerror!void {
+ var header_buffer: [1024]u8 = undefined;
+ var remaining: usize = 1;
+ while (remaining != 0) : (remaining -= 1) {
+ const conn = try net_server.accept();
+ defer conn.stream.close();
+
+ var server = http.Server.init(conn, &header_buffer);
+
+ try expectEqual(.ready, server.state);
+ var request = try server.receiveHead();
+ try serve(&request);
+ try expectEqual(.ready, server.state);
+ }
+ }
+
+ fn serve(request: *http.Server.Request) !void {
+ try expectEqualStrings(request.head.target, "/trailer");
+
+ var send_buffer: [1024]u8 = undefined;
+ var response = request.respondStreaming(.{
+ .send_buffer = &send_buffer,
+ });
+ try response.writeAll("Hello, ");
+ try response.flush();
+ try response.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 };
+ 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 server_header_buffer: [1024]u8 = undefined;
+ var req = try client.open(.GET, uri, .{
+ .server_header_buffer = &server_header_buffer,
+ });
+ defer req.deinit();
+
+ try req.send(.{});
+ try req.wait();
+
+ const body = try req.reader().readAllAlloc(gpa, 8192);
+ defer gpa.free(body);
+
+ try expectEqualStrings("Hello, World!\n", body);
+
+ var it = req.response.iterateHeaders();
+ {
+ const header = it.next().?;
+ try expect(!it.is_trailer);
+ try expectEqualStrings("connection", header.name);
+ try expectEqualStrings("keep-alive", header.value);
+ }
+ {
+ const header = it.next().?;
+ try expect(!it.is_trailer);
+ try expectEqualStrings("transfer-encoding", header.name);
+ try expectEqualStrings("chunked", header.value);
+ }
+ {
+ const header = it.next().?;
+ try expect(it.is_trailer);
+ 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" {
+ const test_server = try createTestServer(struct {
+ fn run(net_server: *std.net.Server) !void {
+ var header_buffer: [8192]u8 = undefined;
+ const conn = try net_server.accept();
+ defer conn.stream.close();
+
+ var server = http.Server.init(conn, &header_buffer);
+ var request = try server.receiveHead();
+
+ try expect(request.head.transfer_encoding == .chunked);
+
+ var buf: [128]u8 = undefined;
+ const n = try (try request.reader()).readAll(&buf);
+ try expect(mem.eql(u8, buf[0..n], "ABCD"));
+
+ 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 gpa = std.testing.allocator;
+ const stream = try std.net.tcpConnectToHost(gpa, "127.0.0.1", test_server.port());
+ defer stream.close();
+ try stream.writeAll(request_bytes);
+}
+
+test "echo content server" {
+ const test_server = try createTestServer(struct {
+ fn run(net_server: *std.net.Server) anyerror!void {
+ var read_buffer: [1024]u8 = undefined;
+
+ accept: while (true) {
+ const conn = try net_server.accept();
+ defer conn.stream.close();
+
+ var http_server = http.Server.init(conn, &read_buffer);
+
+ while (http_server.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.reader());
+ try request.respond("", .{ .keep_alive = false });
+ 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,
+ //});
+
+ const body = try (try request.reader()).readAllAlloc(std.testing.allocator, 8192);
+ defer std.testing.allocator.free(body);
+
+ try expect(mem.startsWith(u8, request.head.target, "/echo-content"));
+ try expectEqualStrings("Hello, World!\n", body);
+ try expectEqualStrings("text/plain", request.head.content_type.?);
+
+ var send_buffer: [100]u8 = undefined;
+ var response = request.respondStreaming(.{
+ .send_buffer = &send_buffer,
+ .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 };
+ defer client.deinit();
+
+ try echoTests(&client, test_server.port());
+ }
+}
+
+test "Server.Request.respondStreaming non-chunked, unknown content-length" {
+ // 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(struct {
+ fn run(net_server: *std.net.Server) anyerror!void {
+ var header_buffer: [1000]u8 = undefined;
+ var remaining: usize = 1;
+ while (remaining != 0) : (remaining -= 1) {
+ const conn = try net_server.accept();
+ defer conn.stream.close();
+
+ var server = http.Server.init(conn, &header_buffer);
+
+ try expectEqual(.ready, server.state);
+ var request = try server.receiveHead();
+ try expectEqualStrings(request.head.target, "/foo");
+ var send_buffer: [500]u8 = undefined;
+ var response = request.respondStreaming(.{
+ .send_buffer = &send_buffer,
+ .respond_options = .{
+ .transfer_encoding = .none,
+ },
+ });
+ 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 response.writeAll(line);
+ total += line.len;
+ }
+ try expectEqual(7390, total);
+ try response.end();
+ try expectEqual(.closing, server.state);
+ }
+ }
+ });
+ defer test_server.destroy();
+
+ const request_bytes = "GET /foo HTTP/1.1\r\n\r\n";
+ const gpa = std.testing.allocator;
+ const stream = try std.net.tcpConnectToHost(gpa, "127.0.0.1", test_server.port());
+ defer stream.close();
+ try stream.writeAll(request_bytes);
+
+ const response = try stream.reader().readAllAlloc(gpa, 8192);
+ defer gpa.free(response);
+
+ var expected_response = std.ArrayList(u8).init(gpa);
+ defer expected_response.deinit();
+
+ try expected_response.appendSlice("HTTP/1.1 200 OK\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" {
+ const test_server = try createTestServer(struct {
+ fn run(net_server: *std.net.Server) anyerror!void {
+ var read_buffer: [666]u8 = undefined;
+ var remaining: usize = 1;
+ while (remaining != 0) : (remaining -= 1) {
+ const conn = try net_server.accept();
+ defer conn.stream.close();
+
+ var server = http.Server.init(conn, &read_buffer);
+ try expectEqual(.ready, server.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 gpa = std.testing.allocator;
+ const stream = try std.net.tcpConnectToHost(gpa, "127.0.0.1", test_server.port());
+ defer stream.close();
+ try stream.writeAll(request_bytes);
+
+ const response = try stream.reader().readAllAlloc(gpa, 8192);
+ defer gpa.free(response);
+
+ var expected_response = std.ArrayList(u8).init(gpa);
+ defer expected_response.deinit();
+
+ try expected_response.appendSlice("HTTP/1.1 200 OK\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.os.tag == .windows) {
+ // This test was never passing on Windows.
+ return error.SkipZigTest;
+ }
+
+ const global = struct {
+ var handle_new_requests = true;
+ };
+ const test_server = try createTestServer(struct {
+ fn run(net_server: *std.net.Server) anyerror!void {
+ var client_header_buffer: [1024]u8 = undefined;
+ outer: while (global.handle_new_requests) {
+ var connection = try net_server.accept();
+ defer connection.stream.close();
+
+ var http_server = http.Server.init(connection, &client_header_buffer);
+
+ while (http_server.state == .ready) {
+ var request = http_server.receiveHead() catch |err| switch (err) {
+ error.HttpConnectionClosing => continue :outer,
+ else => |e| return e,
+ };
+
+ try handleRequest(&request, net_server.listen_address.getPort());
+ }
+ }
+ }
+
+ fn handleRequest(request: *http.Server.Request, listen_port: u16) !void {
+ const log = std.log.scoped(.server);
+
+ log.info("{} {s} {s}", .{
+ request.head.method,
+ @tagName(request.head.version),
+ request.head.target,
+ });
+
+ const gpa = std.testing.allocator;
+ const body = try (try request.reader()).readAllAlloc(gpa, 8192);
+ defer gpa.free(body);
+
+ var send_buffer: [100]u8 = undefined;
+
+ if (mem.startsWith(u8, request.head.target, "/get")) {
+ var response = request.respondStreaming(.{
+ .send_buffer = &send_buffer,
+ .content_length = if (mem.indexOf(u8, request.head.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, request.head.target, "/large")) {
+ var response = request.respondStreaming(.{
+ .send_buffer = &send_buffer,
+ .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");
+ }
+
+ try w.writeAll("Hello, World!\n" ** 1024);
+
+ i = 0;
+ while (i < 5) : (i += 1) {
+ try w.writeAll("Hello, World!\n");
+ }
+
+ try response.end();
+ } else if (mem.eql(u8, request.head.target, "/redirect/1")) {
+ var response = request.respondStreaming(.{
+ .send_buffer = &send_buffer,
+ .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, request.head.target, "/redirect/2")) {
+ try request.respond("Hello, Redirected!\n", .{
+ .status = .found,
+ .extra_headers = &.{
+ .{ .name = "location", .value = "/redirect/1" },
+ },
+ });
+ } else if (mem.eql(u8, request.head.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, request.head.target, "/redirect/4")) {
+ try request.respond("Hello, Redirected!\n", .{
+ .status = .found,
+ .extra_headers = &.{
+ .{ .name = "location", .value = "/redirect/3" },
+ },
+ });
+ } else if (mem.eql(u8, request.head.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 {
+ try request.respond("", .{ .status = .not_found });
+ }
+ }
+
+ fn getUnusedTcpPort() !u16 {
+ const addr = try std.net.Address.parseIp("127.0.0.1", 0);
+ var s = try addr.listen(.{});
+ defer s.deinit();
+ return s.listen_address.in.getPort();
+ }
+ });
+ defer test_server.destroy();
+
+ const log = std.log.scoped(.client);
+
+ const gpa = std.testing.allocator;
+ var client: http.Client = .{ .allocator = gpa };
+ errdefer client.deinit();
+ // defer client.deinit(); handled below
+
+ 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 server_header_buffer: [1024]u8 = undefined;
+ var req = try client.open(.GET, uri, .{
+ .server_header_buffer = &server_header_buffer,
+ });
+ defer req.deinit();
+
+ try req.send(.{});
+ try req.wait();
+
+ const body = try req.reader().readAllAlloc(gpa, 8192);
+ defer gpa.free(body);
+
+ try expectEqualStrings("Hello, World!\n", body);
+ try expectEqualStrings("text/plain", req.response.content_type.?);
+ }
+
+ // 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 server_header_buffer: [1024]u8 = undefined;
+ var req = try client.open(.GET, uri, .{
+ .server_header_buffer = &server_header_buffer,
+ });
+ defer req.deinit();
+
+ try req.send(.{});
+ try req.wait();
+
+ const body = try req.reader().readAllAlloc(gpa, 8192 * 1024);
+ 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 server_header_buffer: [1024]u8 = undefined;
+ var req = try client.open(.HEAD, uri, .{
+ .server_header_buffer = &server_header_buffer,
+ });
+ defer req.deinit();
+
+ try req.send(.{});
+ try req.wait();
+
+ const body = try req.reader().readAllAlloc(gpa, 8192);
+ defer gpa.free(body);
+
+ try expectEqualStrings("", body);
+ try expectEqualStrings("text/plain", req.response.content_type.?);
+ try expectEqual(14, req.response.content_length.?);
+ }
+
+ // 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 server_header_buffer: [1024]u8 = undefined;
+ var req = try client.open(.GET, uri, .{
+ .server_header_buffer = &server_header_buffer,
+ });
+ defer req.deinit();
+
+ try req.send(.{});
+ try req.wait();
+
+ const body = try req.reader().readAllAlloc(gpa, 8192);
+ defer gpa.free(body);
+
+ try expectEqualStrings("Hello, World!\n", body);
+ try expectEqualStrings("text/plain", req.response.content_type.?);
+ }
+
+ // 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 server_header_buffer: [1024]u8 = undefined;
+ var req = try client.open(.HEAD, uri, .{
+ .server_header_buffer = &server_header_buffer,
+ });
+ defer req.deinit();
+
+ try req.send(.{});
+ try req.wait();
+
+ const body = try req.reader().readAllAlloc(gpa, 8192);
+ defer gpa.free(body);
+
+ try expectEqualStrings("", body);
+ try expectEqualStrings("text/plain", req.response.content_type.?);
+ try expect(req.response.transfer_encoding == .chunked);
+ }
+
+ // 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 server_header_buffer: [1024]u8 = undefined;
+ var req = try client.open(.GET, uri, .{
+ .server_header_buffer = &server_header_buffer,
+ .keep_alive = false,
+ });
+ defer req.deinit();
+
+ try req.send(.{});
+ try req.wait();
+
+ const body = try req.reader().readAllAlloc(gpa, 8192);
+ defer gpa.free(body);
+
+ try expectEqualStrings("Hello, World!\n", body);
+ try expectEqualStrings("text/plain", req.response.content_type.?);
+ }
+
+ // connection has been closed
+ try expect(client.connection_pool.free_len == 0);
+
+ { // 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 server_header_buffer: [1024]u8 = undefined;
+ var req = try client.open(.GET, uri, .{
+ .server_header_buffer = &server_header_buffer,
+ });
+ defer req.deinit();
+
+ try req.send(.{});
+ try req.wait();
+
+ const body = try req.reader().readAllAlloc(gpa, 8192);
+ 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 server_header_buffer: [1024]u8 = undefined;
+ var req = try client.open(.GET, uri, .{
+ .server_header_buffer = &server_header_buffer,
+ });
+ defer req.deinit();
+
+ try req.send(.{});
+ try req.wait();
+
+ const body = try req.reader().readAllAlloc(gpa, 8192);
+ 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 server_header_buffer: [1024]u8 = undefined;
+ var req = try client.open(.GET, uri, .{
+ .server_header_buffer = &server_header_buffer,
+ });
+ defer req.deinit();
+
+ try req.send(.{});
+ try req.wait();
+
+ const body = try req.reader().readAllAlloc(gpa, 8192);
+ 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 server_header_buffer: [1024]u8 = undefined;
+ var req = try client.open(.GET, uri, .{
+ .server_header_buffer = &server_header_buffer,
+ });
+ defer req.deinit();
+
+ try req.send(.{});
+ req.wait() catch |err| switch (err) {
+ error.TooManyHttpRedirects => {},
+ else => return err,
+ };
+ }
+
+ // 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 server_header_buffer: [1024]u8 = undefined;
+ var req = try client.open(.GET, uri, .{
+ .server_header_buffer = &server_header_buffer,
+ });
+ defer req.deinit();
+
+ try req.send(.{});
+ const result = req.wait();
+
+ // 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);
+
+ { // issue 16282 *** This test leaves the client in an invalid state, it must be last ***
+ 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);
+
+ const total_connections = client.connection_pool.free_size + 64;
+ var requests = try gpa.alloc(http.Client.Request, total_connections);
+ defer gpa.free(requests);
+
+ var header_bufs = std.ArrayList([]u8).init(gpa);
+ defer header_bufs.deinit();
+ defer for (header_bufs.items) |item| gpa.free(item);
+
+ for (0..total_connections) |i| {
+ const headers_buf = try gpa.alloc(u8, 1024);
+ try header_bufs.append(headers_buf);
+ var req = try client.open(.GET, uri, .{
+ .server_header_buffer = headers_buf,
+ });
+ req.response.parser.done = true;
+ req.connection.?.closing = false;
+ requests[i] = req;
+ }
+
+ for (0..total_connections) |i| {
+ requests[i].deinit();
+ }
+
+ // free connections should be full now
+ try expect(client.connection_pool.free_len == client.connection_pool.free_size);
+ }
+
+ client.deinit();
+
+ {
+ global.handle_new_requests = false;
+
+ const conn = try std.net.tcpConnectToAddress(test_server.net_server.listen_address);
+ conn.close();
+ }
+}
+
+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 server_header_buffer: [1024]u8 = undefined;
+ var req = try client.open(.POST, uri, .{
+ .server_header_buffer = &server_header_buffer,
+ .extra_headers = &.{
+ .{ .name = "content-type", .value = "text/plain" },
+ },
+ });
+ defer req.deinit();
+
+ req.transfer_encoding = .{ .content_length = 14 };
+
+ try req.send(.{});
+ try req.writeAll("Hello, ");
+ try req.writeAll("World!\n");
+ try req.finish();
+
+ try req.wait();
+
+ const body = try req.reader().readAllAlloc(gpa, 8192);
+ 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 server_header_buffer: [1024]u8 = undefined;
+ var req = try client.open(.POST, uri, .{
+ .server_header_buffer = &server_header_buffer,
+ .extra_headers = &.{
+ .{ .name = "content-type", .value = "text/plain" },
+ },
+ });
+ defer req.deinit();
+
+ req.transfer_encoding = .chunked;
+
+ try req.send(.{});
+ try req.writeAll("Hello, ");
+ try req.writeAll("World!\n");
+ try req.finish();
+
+ try req.wait();
+
+ const body = try req.reader().readAllAlloc(gpa, 8192);
+ 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.ArrayList(u8).init(gpa);
+ defer body.deinit();
+
+ const res = try client.fetch(.{
+ .location = .{ .url = location },
+ .method = .POST,
+ .payload = "Hello, World!\n",
+ .extra_headers = &.{
+ .{ .name = "content-type", .value = "text/plain" },
+ },
+ .response_storage = .{ .dynamic = &body },
+ });
+ try expectEqual(.ok, res.status);
+ try expectEqualStrings("Hello, World!\n", body.items);
+ }
+
+ { // 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 server_header_buffer: [1024]u8 = undefined;
+ var req = try client.open(.POST, uri, .{
+ .server_header_buffer = &server_header_buffer,
+ .extra_headers = &.{
+ .{ .name = "expect", .value = "100-continue" },
+ .{ .name = "content-type", .value = "text/plain" },
+ },
+ });
+ defer req.deinit();
+
+ req.transfer_encoding = .chunked;
+
+ try req.send(.{});
+ try req.writeAll("Hello, ");
+ try req.writeAll("World!\n");
+ try req.finish();
+
+ try req.wait();
+ try expectEqual(.ok, req.response.status);
+
+ const body = try req.reader().readAllAlloc(gpa, 8192);
+ 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 server_header_buffer: [1024]u8 = undefined;
+ var req = try client.open(.POST, uri, .{
+ .server_header_buffer = &server_header_buffer,
+ .extra_headers = &.{
+ .{ .name = "content-type", .value = "text/plain" },
+ .{ .name = "expect", .value = "garbage" },
+ },
+ });
+ defer req.deinit();
+
+ req.transfer_encoding = .chunked;
+
+ try req.send(.{});
+ try req.wait();
+ try expectEqual(.expectation_failed, req.response.status);
+ }
+
+ _ = try client.fetch(.{
+ .location = .{
+ .url = try std.fmt.bufPrint(&location_buffer, "http://127.0.0.1:{d}/end", .{port}),
+ },
+ });
+}
+
+const TestServer = struct {
+ server_thread: std.Thread,
+ net_server: std.net.Server,
+
+ fn destroy(self: *@This()) void {
+ self.server_thread.join();
+ self.net_server.deinit();
+ std.testing.allocator.destroy(self);
+ }
+
+ fn port(self: @This()) u16 {
+ return self.net_server.listen_address.in.getPort();
+ }
+};
+
+fn createTestServer(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 std.net.Address.parseIp("127.0.0.1", 0);
+ const test_server = try std.testing.allocator.create(TestServer);
+ test_server.net_server = try address.listen(.{ .reuse_address = true });
+ test_server.server_thread = try std.Thread.spawn(.{}, S.run, .{&test_server.net_server});
+ return test_server;
+}