From 483b63d301315d6ca2c1c8660f5ec636c7358dc6 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 22 Feb 2024 19:19:12 -0700 Subject: [PATCH] std.http: migrate remaining test/standalone/http.zig to std lib These tests were not being actually run. Now they are run along with testing the standard library. --- lib/std/http/Client.zig | 4 + lib/std/http/test.zig | 495 ++++++++++++++++++++++++++++++++++++- test/standalone.zig | 4 - test/standalone/http.zig | 510 --------------------------------------- 4 files changed, 486 insertions(+), 527 deletions(-) delete mode 100644 test/standalone/http.zig diff --git a/lib/std/http/Client.zig b/lib/std/http/Client.zig index f9cf6c1d0e..1affbf9b5c 100644 --- a/lib/std/http/Client.zig +++ b/lib/std/http/Client.zig @@ -1676,3 +1676,7 @@ pub fn fetch(client: *Client, options: FetchOptions) !FetchResult { .status = req.response.status, }; } + +test { + _ = &initDefaultProxies; +} diff --git a/lib/std/http/test.zig b/lib/std/http/test.zig index a1f82dc892..5a4c58e4ad 100644 --- a/lib/std/http/test.zig +++ b/lib/std/http/test.zig @@ -1,5 +1,7 @@ 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; @@ -15,7 +17,7 @@ test "trailers" { const conn = try net_server.accept(); defer conn.stream.close(); - var server = std.http.Server.init(conn, &header_buffer); + var server = http.Server.init(conn, &header_buffer); try expectEqual(.ready, server.state); var request = try server.receiveHead(); @@ -24,7 +26,7 @@ test "trailers" { } } - fn serve(request: *std.http.Server.Request) !void { + fn serve(request: *http.Server.Request) !void { try expectEqualStrings(request.head.target, "/trailer"); var send_buffer: [1024]u8 = undefined; @@ -46,7 +48,7 @@ test "trailers" { const gpa = std.testing.allocator; - var client: std.http.Client = .{ .allocator = gpa }; + var client: http.Client = .{ .allocator = gpa }; defer client.deinit(); const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/trailer", .{ @@ -103,14 +105,14 @@ test "HTTP server handles a chunked transfer coding request" { const conn = try net_server.accept(); defer conn.stream.close(); - var server = std.http.Server.init(conn, &header_buffer); + 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(std.mem.eql(u8, buf[0..n], "ABCD")); + try expect(mem.eql(u8, buf[0..n], "ABCD")); try request.respond("message from server!\n", .{ .extra_headers = &.{ @@ -151,18 +153,18 @@ test "echo content server" { const conn = try net_server.accept(); defer conn.stream.close(); - var http_server = std.http.Server.init(conn, &read_buffer); + 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 (std.mem.eql(u8, request.head.target, "/end")) { + if (mem.eql(u8, request.head.target, "/end")) { return request.respond("", .{ .keep_alive = false }); } if (request.head.expect) |expect_header_value| { - if (std.mem.eql(u8, expect_header_value, "garbage")) { + if (mem.eql(u8, expect_header_value, "garbage")) { try expectError(error.HttpExpectationFailed, request.reader()); try request.respond("", .{ .keep_alive = false }); continue; @@ -178,7 +180,7 @@ test "echo content server" { } } - fn handleRequest(request: *std.http.Server.Request) !void { + fn handleRequest(request: *http.Server.Request) !void { //std.debug.print("server received {s} {s} {s}\n", .{ // @tagName(request.head.method), // @tagName(request.head.version), @@ -188,7 +190,7 @@ test "echo content server" { const body = try (try request.reader()).readAllAlloc(std.testing.allocator, 8192); defer std.testing.allocator.free(body); - try expect(std.mem.startsWith(u8, request.head.target, "/echo-content")); + try expect(mem.startsWith(u8, request.head.target, "/echo-content")); try expectEqualStrings("Hello, World!\n", body); try expectEqualStrings("text/plain", request.head.content_type.?); @@ -215,7 +217,7 @@ test "echo content server" { defer test_server.destroy(); { - var client: std.http.Client = .{ .allocator = std.testing.allocator }; + var client: http.Client = .{ .allocator = std.testing.allocator }; defer client.deinit(); try echoTests(&client, test_server.port()); @@ -233,7 +235,7 @@ test "Server.Request.respondStreaming non-chunked, unknown content-length" { const conn = try net_server.accept(); defer conn.stream.close(); - var server = std.http.Server.init(conn, &header_buffer); + var server = http.Server.init(conn, &header_buffer); try expectEqual(.ready, server.state); var request = try server.receiveHead(); @@ -288,7 +290,474 @@ test "Server.Request.respondStreaming non-chunked, unknown content-length" { try expectEqualStrings(expected_response.items, response); } -fn echoTests(client: *std.http.Client, port: u16) !void { +test "general client/server API coverage" { + 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; diff --git a/test/standalone.zig b/test/standalone.zig index 3740ddb80a..e72deb38ae 100644 --- a/test/standalone.zig +++ b/test/standalone.zig @@ -55,10 +55,6 @@ pub const simple_cases = [_]SimpleCase{ .os_filter = .windows, .link_libc = true, }, - .{ - .src_path = "test/standalone/http.zig", - .all_modes = true, - }, // Ensure the development tools are buildable. Alphabetically sorted. // No need to build `tools/spirv/grammar.zig`. diff --git a/test/standalone/http.zig b/test/standalone/http.zig deleted file mode 100644 index ff6467fc6c..0000000000 --- a/test/standalone/http.zig +++ /dev/null @@ -1,510 +0,0 @@ -const std = @import("std"); - -const http = std.http; - -const mem = std.mem; -const testing = std.testing; - -pub const std_options = .{ - .http_disable_tls = true, -}; - -const max_header_size = 8192; - -var gpa_server = std.heap.GeneralPurposeAllocator(.{ .stack_trace_frames = 12 }){}; -var gpa_client = std.heap.GeneralPurposeAllocator(.{ .stack_trace_frames = 12 }){}; - -const salloc = gpa_server.allocator(); -const calloc = gpa_client.allocator(); - -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 body = try (try request.reader()).readAllAlloc(salloc, 8192); - defer salloc.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 (std.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(salloc, "http://127.0.0.1:{d}/redirect/2", .{ - listen_port, - }); - defer salloc.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(salloc, "http://127.0.0.1:{d}", .{invalid_port}); - defer salloc.free(location); - - try request.respond("", .{ - .status = .found, - .extra_headers = &.{ - .{ .name = "location", .value = location }, - }, - }); - } else { - try request.respond("", .{ .status = .not_found }); - } -} - -var handle_new_requests = true; - -fn runServer(server: *std.net.Server) !void { - var client_header_buffer: [1024]u8 = undefined; - outer: while (handle_new_requests) { - var connection = try 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, server.listen_address.getPort()); - } - } -} - -fn serverThread(server: *std.net.Server) void { - defer _ = gpa_server.deinit(); - - runServer(server) catch |err| { - std.debug.print("server error: {}\n", .{err}); - - if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace.*); - } - - _ = gpa_server.deinit(); - std.os.exit(1); - }; -} - -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(); -} - -pub fn main() !void { - const log = std.log.scoped(.client); - - defer _ = gpa_client.deinit(); - - const addr = std.net.Address.parseIp("127.0.0.1", 0) catch unreachable; - var server = try addr.listen(.{ .reuse_address = true }); - defer server.deinit(); - - const port = server.listen_address.getPort(); - - const server_thread = try std.Thread.spawn(.{}, serverThread, .{&server}); - - var client: http.Client = .{ .allocator = calloc }; - errdefer client.deinit(); - // defer client.deinit(); handled below - - var arena_instance = std.heap.ArenaAllocator.init(calloc); - defer arena_instance.deinit(); - const arena = arena_instance.allocator(); - - try client.initDefaultProxies(arena); - - { // read content-length response - const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get", .{port}); - defer calloc.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(calloc, 8192); - defer calloc.free(body); - - try testing.expectEqualStrings("Hello, World!\n", body); - try testing.expectEqualStrings("text/plain", req.response.content_type.?); - } - - // connection has been kept alive - try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1); - - { // read large content-length response - const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/large", .{port}); - defer calloc.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(calloc, 8192 * 1024); - defer calloc.free(body); - - try testing.expectEqual(@as(usize, 14 * 1024 + 14 * 10), body.len); - } - - // connection has been kept alive - try testing.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(calloc, "http://127.0.0.1:{d}/get", .{port}); - defer calloc.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(calloc, 8192); - defer calloc.free(body); - - try testing.expectEqualStrings("", body); - try testing.expectEqualStrings("text/plain", req.response.content_type.?); - try testing.expectEqual(14, req.response.content_length.?); - } - - // connection has been kept alive - try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1); - - { // read chunked response - const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get?chunked", .{port}); - defer calloc.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(calloc, 8192); - defer calloc.free(body); - - try testing.expectEqualStrings("Hello, World!\n", body); - try testing.expectEqualStrings("text/plain", req.response.content_type.?); - } - - // connection has been kept alive - try testing.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(calloc, "http://127.0.0.1:{d}/get?chunked", .{port}); - defer calloc.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(calloc, 8192); - defer calloc.free(body); - - try testing.expectEqualStrings("", body); - try testing.expectEqualStrings("text/plain", req.response.content_type.?); - try testing.expect(req.response.transfer_encoding == .chunked); - } - - // connection has been kept alive - try testing.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(calloc, "http://127.0.0.1:{d}/get", .{port}); - defer calloc.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(calloc, 8192); - defer calloc.free(body); - - try testing.expectEqualStrings("Hello, World!\n", body); - try testing.expectEqualStrings("text/plain", req.response.content_type.?); - } - - // connection has been closed - try testing.expect(client.connection_pool.free_len == 0); - - { // relative redirect - const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/1", .{port}); - defer calloc.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(calloc, 8192); - defer calloc.free(body); - - try testing.expectEqualStrings("Hello, World!\n", body); - } - - // connection has been kept alive - try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1); - - { // redirect from root - const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/2", .{port}); - defer calloc.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(calloc, 8192); - defer calloc.free(body); - - try testing.expectEqualStrings("Hello, World!\n", body); - } - - // connection has been kept alive - try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1); - - { // absolute redirect - const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/3", .{port}); - defer calloc.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(calloc, 8192); - defer calloc.free(body); - - try testing.expectEqualStrings("Hello, World!\n", body); - } - - // connection has been kept alive - try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1); - - { // too many redirects - const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/4", .{port}); - defer calloc.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 testing.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(calloc, "http://127.0.0.1:{d}/redirect/invalid", .{port}); - defer calloc.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 testing.expectError(error.ConnectionRefused, result); // expects not segfault but the regular error - } - } - - // connection has been kept alive - try testing.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(calloc, "http://127.0.0.1:{d}/get", .{port}); - defer calloc.free(location); - const uri = try std.Uri.parse(location); - - const total_connections = client.connection_pool.free_size + 64; - var requests = try calloc.alloc(http.Client.Request, total_connections); - defer calloc.free(requests); - - var header_bufs = std.ArrayList([]u8).init(calloc); - defer header_bufs.deinit(); - defer for (header_bufs.items) |item| calloc.free(item); - - for (0..total_connections) |i| { - const headers_buf = try calloc.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 testing.expect(client.connection_pool.free_len == client.connection_pool.free_size); - } - - client.deinit(); - - { - handle_new_requests = false; - - const conn = std.net.tcpConnectToAddress(server.listen_address) catch return; - conn.close(); - } - - server_thread.join(); -}