diff options
| author | Andrew Kelley <andrew@ziglang.org> | 2023-05-11 08:37:42 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-05-11 08:37:42 -0700 |
| commit | 3bb3d39fb4158ba4b811bcae7e7a897febf07e17 (patch) | |
| tree | 51d408085e1e9c60003c02960e509e3498807ab5 /test | |
| parent | 5569e6b49d9b421d35e3175df36eb9fe7e4e8084 (diff) | |
| parent | 9017d758b96c1f296249670dffb774a598bc8598 (diff) | |
| download | zig-3bb3d39fb4158ba4b811bcae7e7a897febf07e17.tar.gz zig-3bb3d39fb4158ba4b811bcae7e7a897febf07e17.zip | |
Merge pull request #15487 from truemedian/http-tests
std.http: more http fixes, add standalone http server test
Diffstat (limited to 'test')
| -rw-r--r-- | test/standalone.zig | 4 | ||||
| -rw-r--r-- | test/standalone/http.zig | 541 |
2 files changed, 545 insertions, 0 deletions
diff --git a/test/standalone.zig b/test/standalone.zig index 1f4d7cfded..a055da9761 100644 --- a/test/standalone.zig +++ b/test/standalone.zig @@ -55,6 +55,10 @@ 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 new file mode 100644 index 0000000000..13dc278b6d --- /dev/null +++ b/test/standalone/http.zig @@ -0,0 +1,541 @@ +const std = @import("std"); + +const http = std.http; +const Server = http.Server; +const Client = http.Client; + +const mem = std.mem; +const testing = std.testing; + +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(); + +var server: Server = undefined; + +fn handleRequest(res: *Server.Response) !void { + const log = std.log.scoped(.server); + + log.info("{s} {s} {s}", .{ @tagName(res.request.method), @tagName(res.request.version), res.request.target }); + + const body = try res.reader().readAllAlloc(salloc, 8192); + defer salloc.free(body); + + if (res.request.headers.contains("connection")) { + try res.headers.append("connection", "keep-alive"); + } + + if (mem.startsWith(u8, res.request.target, "/get")) { + if (std.mem.indexOf(u8, res.request.target, "?chunked") != null) { + res.transfer_encoding = .chunked; + } else { + res.transfer_encoding = .{ .content_length = 14 }; + } + + try res.headers.append("content-type", "text/plain"); + + try res.do(); + if (res.request.method != .HEAD) { + try res.writeAll("Hello, "); + try res.writeAll("World!\n"); + try res.finish(); + } + } else if (mem.startsWith(u8, res.request.target, "/large")) { + res.transfer_encoding = .{ .content_length = 14 * 1024 + 14 * 10 }; + + try res.do(); + + var i: u32 = 0; + while (i < 5) : (i += 1) { + try res.writeAll("Hello, World!\n"); + } + + try res.writeAll("Hello, World!\n" ** 1024); + + i = 0; + while (i < 5) : (i += 1) { + try res.writeAll("Hello, World!\n"); + } + + try res.finish(); + } else if (mem.eql(u8, res.request.target, "/echo-content")) { + try testing.expectEqualStrings("Hello, World!\n", body); + try testing.expectEqualStrings("text/plain", res.request.headers.getFirstValue("content-type").?); + + if (res.request.headers.contains("transfer-encoding")) { + try testing.expectEqualStrings("chunked", res.request.headers.getFirstValue("transfer-encoding").?); + res.transfer_encoding = .chunked; + } else { + res.transfer_encoding = .{ .content_length = 14 }; + try testing.expectEqualStrings("14", res.request.headers.getFirstValue("content-length").?); + } + + try res.do(); + try res.writeAll("Hello, "); + try res.writeAll("World!\n"); + try res.finish(); + } else if (mem.eql(u8, res.request.target, "/trailer")) { + res.transfer_encoding = .chunked; + + try res.do(); + try res.writeAll("Hello, "); + try res.writeAll("World!\n"); + // try res.finish(); + try res.connection.writeAll("0\r\nX-Checksum: aaaa\r\n\r\n"); + try res.connection.flush(); + } else if (mem.eql(u8, res.request.target, "/redirect/1")) { + res.transfer_encoding = .chunked; + + res.status = .found; + try res.headers.append("location", "../../get"); + + try res.do(); + try res.writeAll("Hello, "); + try res.writeAll("Redirected!\n"); + try res.finish(); + } else if (mem.eql(u8, res.request.target, "/redirect/2")) { + res.transfer_encoding = .chunked; + + res.status = .found; + try res.headers.append("location", "/redirect/1"); + + try res.do(); + try res.writeAll("Hello, "); + try res.writeAll("Redirected!\n"); + try res.finish(); + } else if (mem.eql(u8, res.request.target, "/redirect/3")) { + res.transfer_encoding = .chunked; + + const location = try std.fmt.allocPrint(salloc, "http://127.0.0.1:{d}/redirect/2", .{server.socket.listen_address.getPort()}); + defer salloc.free(location); + + res.status = .found; + try res.headers.append("location", location); + + try res.do(); + try res.writeAll("Hello, "); + try res.writeAll("Redirected!\n"); + try res.finish(); + } else if (mem.eql(u8, res.request.target, "/redirect/4")) { + res.transfer_encoding = .chunked; + + res.status = .found; + try res.headers.append("location", "/redirect/3"); + + try res.do(); + try res.writeAll("Hello, "); + try res.writeAll("Redirected!\n"); + try res.finish(); + } else { + res.status = .not_found; + try res.do(); + } +} + +var handle_new_requests = true; + +fn runServer(srv: *Server) !void { + outer: while (handle_new_requests) { + var res = try srv.accept(.{ + .allocator = salloc, + .header_strategy = .{ .dynamic = max_header_size }, + }); + defer res.deinit(); + + while (res.reset() != .closing) { + res.wait() catch |err| switch (err) { + error.HttpHeadersInvalid => continue :outer, + error.EndOfStream => continue, + else => return err, + }; + + try handleRequest(&res); + } + } +} + +fn serverThread(srv: *Server) void { + defer srv.deinit(); + defer _ = gpa_server.deinit(); + + runServer(srv) catch |err| { + std.debug.print("server error: {}\n", .{err}); + + if (@errorReturnTrace()) |trace| { + std.debug.dumpStackTrace(trace.*); + } + + _ = gpa_server.deinit(); + std.os.exit(1); + }; +} + +fn killServer(addr: std.net.Address) void { + handle_new_requests = false; + + const conn = std.net.tcpConnectToAddress(addr) catch return; + conn.close(); +} + +pub fn main() !void { + const log = std.log.scoped(.client); + + defer _ = gpa_client.deinit(); + + server = Server.init(salloc, .{ .reuse_address = true }); + + const addr = std.net.Address.parseIp("127.0.0.1", 0) catch unreachable; + try server.listen(addr); + + const port = server.socket.listen_address.getPort(); + + const server_thread = try std.Thread.spawn(.{}, serverThread, .{&server}); + + var client = Client{ .allocator = calloc }; + // defer client.deinit(); handled below + + { // read content-length response + var h = http.Headers{ .allocator = calloc }; + defer h.deinit(); + + 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 req = try client.request(.GET, uri, h, .{}); + defer req.deinit(); + + try req.start(); + 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.headers.getFirstValue("content-type").?); + } + + // connection has been kept alive + try testing.expect(client.connection_pool.free_len == 1); + + { // read large content-length response + var h = http.Headers{ .allocator = calloc }; + defer h.deinit(); + + 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 req = try client.request(.GET, uri, h, .{}); + defer req.deinit(); + + try req.start(); + 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.connection_pool.free_len == 1); + + { // send head request and not read chunked + var h = http.Headers{ .allocator = calloc }; + defer h.deinit(); + + 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 req = try client.request(.HEAD, uri, h, .{}); + defer req.deinit(); + + try req.start(); + 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.headers.getFirstValue("content-type").?); + try testing.expectEqualStrings("14", req.response.headers.getFirstValue("content-length").?); + } + + // connection has been kept alive + try testing.expect(client.connection_pool.free_len == 1); + + { // read chunked response + var h = http.Headers{ .allocator = calloc }; + defer h.deinit(); + + 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 req = try client.request(.GET, uri, h, .{}); + defer req.deinit(); + + try req.start(); + 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.headers.getFirstValue("content-type").?); + } + + // connection has been kept alive + try testing.expect(client.connection_pool.free_len == 1); + + { // send head request and not read chunked + var h = http.Headers{ .allocator = calloc }; + defer h.deinit(); + + 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 req = try client.request(.HEAD, uri, h, .{}); + defer req.deinit(); + + try req.start(); + 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.headers.getFirstValue("content-type").?); + try testing.expectEqualStrings("chunked", req.response.headers.getFirstValue("transfer-encoding").?); + } + + // connection has been kept alive + try testing.expect(client.connection_pool.free_len == 1); + + { // check trailing headers + var h = http.Headers{ .allocator = calloc }; + defer h.deinit(); + + const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/trailer", .{port}); + defer calloc.free(location); + const uri = try std.Uri.parse(location); + + log.info("{s}", .{location}); + var req = try client.request(.GET, uri, h, .{}); + defer req.deinit(); + + try req.start(); + 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("aaaa", req.response.headers.getFirstValue("x-checksum").?); + } + + // connection has been kept alive + try testing.expect(client.connection_pool.free_len == 1); + + { // send content-length request + var h = http.Headers{ .allocator = calloc }; + defer h.deinit(); + + try h.append("content-type", "text/plain"); + + const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/echo-content", .{port}); + defer calloc.free(location); + const uri = try std.Uri.parse(location); + + log.info("{s}", .{location}); + var req = try client.request(.POST, uri, h, .{}); + defer req.deinit(); + + req.transfer_encoding = .{ .content_length = 14 }; + + try req.start(); + try req.writeAll("Hello, "); + try req.writeAll("World!\n"); + try req.finish(); + + 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.connection_pool.free_len == 1); + + { // read content-length response with connection close + var h = http.Headers{ .allocator = calloc }; + defer h.deinit(); + + try h.append("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 req = try client.request(.GET, uri, h, .{}); + defer req.deinit(); + + try req.start(); + 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.headers.getFirstValue("content-type").?); + } + + // connection has been closed + try testing.expect(client.connection_pool.free_len == 0); + + { // send chunked request + var h = http.Headers{ .allocator = calloc }; + defer h.deinit(); + + try h.append("content-type", "text/plain"); + + const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/echo-content", .{port}); + defer calloc.free(location); + const uri = try std.Uri.parse(location); + + log.info("{s}", .{location}); + var req = try client.request(.POST, uri, h, .{}); + defer req.deinit(); + + req.transfer_encoding = .chunked; + + try req.start(); + try req.writeAll("Hello, "); + try req.writeAll("World!\n"); + try req.finish(); + + 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.connection_pool.free_len == 1); + + { // relative redirect + var h = http.Headers{ .allocator = calloc }; + defer h.deinit(); + + 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 req = try client.request(.GET, uri, h, .{}); + defer req.deinit(); + + try req.start(); + 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.connection_pool.free_len == 1); + + { // redirect from root + var h = http.Headers{ .allocator = calloc }; + defer h.deinit(); + + 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 req = try client.request(.GET, uri, h, .{}); + defer req.deinit(); + + try req.start(); + 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.connection_pool.free_len == 1); + + { // absolute redirect + var h = http.Headers{ .allocator = calloc }; + defer h.deinit(); + + 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 req = try client.request(.GET, uri, h, .{}); + defer req.deinit(); + + try req.start(); + 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.connection_pool.free_len == 1); + + { // too many redirects + var h = http.Headers{ .allocator = calloc }; + defer h.deinit(); + + 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 req = try client.request(.GET, uri, h, .{}); + defer req.deinit(); + + try req.start(); + req.wait() catch |err| switch (err) { + error.TooManyHttpRedirects => {}, + else => return err, + }; + } + + // connection has been kept alive + try testing.expect(client.connection_pool.free_len == 1); + + client.deinit(); + + killServer(server.socket.listen_address); + server_thread.join(); +} |
