diff options
| author | Andrew Kelley <andrew@ziglang.org> | 2024-02-23 17:41:38 -0800 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-02-23 17:41:38 -0800 |
| commit | cfce81f7d5f11ab93b2d5fd26df41edf967f333b (patch) | |
| tree | 11e52ad0a44620f4a4519683abd945146c11b312 /lib/std/http/test.zig | |
| parent | 7230b68b350b16c637e84f3ff224be24d23214ce (diff) | |
| parent | 653d4158cdcb20be82ff525e122277064e6acb92 (diff) | |
| download | zig-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.zig | 995 |
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; +} |
