authorgravatar for truemedian@gmail.comNameless <truemedian@gmail.com> 2023-04-25 09:22:20-05:00
committergravatar for truemedian@gmail.comNameless <truemedian@gmail.com> 2023-05-06 21:35:15-05:00
log71c228fe6572b9f3b30e82035bf8fd7e2b1dd29d
treebc260df4dd4ac6befa2f05d54a39dd8b43548907
parentd71a43ec2c28a53a3e1d9bcb538707eca00a6fc0
signaturelock-open Commit is signed but in an unrecognized format.

std.http: add simple standalone http tests, add state check for http server


8 files changed, 546 insertions(+), 134 deletions(-)

lib/std/Uri.zig+6-3
......@@ -216,6 +216,7 @@ pub fn format(
216216
217217 const needs_absolute = comptime std.mem.indexOf(u8, fmt, "+") != null;
218218 const needs_path = comptime std.mem.indexOf(u8, fmt, "/") != null or fmt.len == 0;
219 const needs_fragment = comptime std.mem.indexOf(u8, fmt, "#") != null;
219220
220221 if (needs_absolute) {
221222 try writer.writeAll(uri.scheme);
......@@ -253,9 +254,11 @@ pub fn format(
253254 try Uri.writeEscapedQuery(writer, q);
254255 }
255256
256 if (uri.fragment) |f| {
257 try writer.writeAll("#");
258 try Uri.writeEscapedQuery(writer, f);
257 if (needs_fragment) {
258 if (uri.fragment) |f| {
259 try writer.writeAll("#");
260 try Uri.writeEscapedQuery(writer, f);
261 }
259262 }
260263 }
261264}
lib/std/http.zig-1
......@@ -275,5 +275,4 @@ test {
275275 _ = Client;
276276 _ = Method;
277277 _ = Status;
278 _ = @import("http/test.zig");
279278}
lib/std/http/Client.zig+9-7
......@@ -264,7 +264,7 @@ pub const BufferedConnection = struct {
264264 const nread = try bconn.conn.read(bconn.buf[0..]);
265265 if (nread == 0) return error.EndOfStream;
266266 bconn.start = 0;
267 bconn.end = @truncate(u16, nread);
267 bconn.end = @intCast(u16, nread);
268268 }
269269
270270 pub fn peek(bconn: *BufferedConnection) []const u8 {
......@@ -282,7 +282,7 @@ pub const BufferedConnection = struct {
282282 const left = buffer.len - out_index;
283283
284284 if (available > 0) {
285 const can_read = @truncate(u16, @min(available, left));
285 const can_read = @intCast(u16, @min(available, left));
286286
287287 @memcpy(buffer[out_index..][0..can_read], bconn.buf[bconn.start..][0..can_read]);
288288 out_index += can_read;
......@@ -355,8 +355,6 @@ pub const Compression = union(enum) {
355355/// A HTTP response originating from a server.
356356pub const Response = struct {
357357 pub const ParseError = Allocator.Error || error{
358 ShortHttpStatusLine,
359 BadHttpVersion,
360358 HttpHeadersInvalid,
361359 HttpHeaderContinuationsUnsupported,
362360 HttpTransferEncodingUnsupported,
......@@ -370,12 +368,12 @@ pub const Response = struct {
370368
371369 const first_line = it.next() orelse return error.HttpHeadersInvalid;
372370 if (first_line.len < 12)
373 return error.ShortHttpStatusLine;
371 return error.HttpHeadersInvalid;
374372
375373 const version: http.Version = switch (int64(first_line[0..8])) {
376374 int64("HTTP/1.0") => .@"HTTP/1.0",
377375 int64("HTTP/1.1") => .@"HTTP/1.1",
378 else => return error.BadHttpVersion,
376 else => return error.HttpHeadersInvalid,
379377 };
380378 if (first_line[8] != ' ') return error.HttpHeadersInvalid;
381379 const status = @intToEnum(http.Status, parseInt3(first_line[9..12].*));
......@@ -695,7 +693,6 @@ pub const Request = struct {
695693
696694 if (req.method == .CONNECT and req.response.status == .ok) {
697695 req.connection.data.closing = false;
698 req.connection.data.proxied = true;
699696 req.response.parser.done = true;
700697 }
701698
......@@ -725,6 +722,11 @@ pub const Request = struct {
725722 req.response.parser.done = true;
726723 }
727724
725 // HEAD requests have no body
726 if (req.method == .HEAD) {
727 req.response.parser.done = true;
728 }
729
728730 if (req.transfer_encoding == .none and req.response.status.class() == .redirect and req.handle_redirects) {
729731 req.response.skip = true;
730732
lib/std/http/Headers.zig-11
......@@ -36,17 +36,6 @@ pub const Field = struct {
3636 name: []const u8,
3737 value: []const u8,
3838
39 pub fn modify(entry: *Field, allocator: Allocator, new_value: []const u8) !void {
40 if (entry.value.len <= new_value.len) {
41 // TODO: eliminate this use of `@constCast`.
42 @memcpy(@constCast(entry.value)[0..new_value.len], new_value);
43 } else {
44 allocator.free(entry.value);
45
46 entry.value = try allocator.dupe(u8, new_value);
47 }
48 }
49
5039 fn lessThan(ctx: void, a: Field, b: Field) bool {
5140 _ = ctx;
5241 if (a.name.ptr == b.name.ptr) return false;
lib/std/http/Server.zig+95-40
......@@ -108,7 +108,7 @@ pub const BufferedConnection = struct {
108108 const nread = try bconn.conn.read(bconn.buf[0..]);
109109 if (nread == 0) return error.EndOfStream;
110110 bconn.start = 0;
111 bconn.end = @truncate(u16, nread);
111 bconn.end = @intCast(u16, nread);
112112 }
113113
114114 pub fn peek(bconn: *BufferedConnection) []const u8 {
......@@ -126,7 +126,7 @@ pub const BufferedConnection = struct {
126126 const left = buffer.len - out_index;
127127
128128 if (available > 0) {
129 const can_read = @truncate(u16, @min(available, left));
129 const can_read = @intCast(u16, @min(available, left));
130130
131131 @memcpy(buffer[out_index..][0..can_read], bconn.buf[bconn.start..][0..can_read]);
132132 out_index += can_read;
......@@ -199,8 +199,6 @@ pub const Compression = union(enum) {
199199/// A HTTP request originating from a client.
200200pub const Request = struct {
201201 pub const ParseError = Allocator.Error || error{
202 ShortHttpStatusLine,
203 BadHttpVersion,
204202 UnknownHttpMethod,
205203 HttpHeadersInvalid,
206204 HttpHeaderContinuationsUnsupported,
......@@ -215,7 +213,7 @@ pub const Request = struct {
215213
216214 const first_line = it.next() orelse return error.HttpHeadersInvalid;
217215 if (first_line.len < 10)
218 return error.ShortHttpStatusLine;
216 return error.HttpHeadersInvalid;
219217
220218 const method_end = mem.indexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid;
221219 const method_str = first_line[0..method_end];
......@@ -229,7 +227,7 @@ pub const Request = struct {
229227 const version: http.Version = switch (int64(version_str[0..8])) {
230228 int64("HTTP/1.0") => .@"HTTP/1.0",
231229 int64("HTTP/1.1") => .@"HTTP/1.1",
232 else => return error.BadHttpVersion,
230 else => return error.HttpHeadersInvalid,
233231 };
234232
235233 const target = first_line[method_end + 1 .. version_start];
......@@ -312,7 +310,7 @@ pub const Request = struct {
312310 transfer_encoding: ?http.TransferEncoding = null,
313311 transfer_compression: ?http.ContentEncoding = null,
314312
315 headers: http.Headers = undefined,
313 headers: http.Headers,
316314 parser: proto.HeadersParser,
317315 compression: Compression = .none,
318316};
......@@ -336,42 +334,92 @@ pub const Response = struct {
336334 headers: http.Headers,
337335 request: Request,
338336
337 state: State = .first,
338
339 const State = enum {
340 first,
341 start,
342 waited,
343 responded,
344 finished,
345 };
346
339347 pub fn deinit(res: *Response) void {
340 res.server.allocator.destroy(res);
341 }
348 res.connection.close();
342349
343 /// Reset this response to its initial state. This must be called before handling a second request on the same connection.
344 pub fn reset(res: *Response) void {
345 res.request.headers.deinit();
346350 res.headers.deinit();
351 res.request.headers.deinit();
347352
348 switch (res.request.compression) {
349 .none => {},
350 .deflate => |*deflate| deflate.deinit(),
351 .gzip => |*gzip| gzip.deinit(),
352 .zstd => |*zstd| zstd.deinit(),
353 if (res.request.parser.header_bytes_owned) {
354 res.request.parser.header_bytes.deinit(res.server.allocator);
355 }
356 }
357
358 /// Reset this response to its initial state. This must be called before handling a second request on the same connection.
359 pub fn reset(res: *Response) bool {
360 if (res.state == .first) {
361 res.state = .start;
362 return true;
353363 }
354364
355365 if (!res.request.parser.done) {
356366 // If the response wasn't fully read, then we need to close the connection.
357367 res.connection.conn.closing = true;
368 return false;
358369 }
359370
360 if (res.connection.conn.closing) {
361 res.connection.close();
371 // A connection is only keep-alive if the Connection header is present and it's value is not "close".
372 // The server and client must both agree
373 const res_connection = res.headers.getFirstValue("connection");
374 const res_keepalive = res_connection != null and !std.ascii.eqlIgnoreCase("close", res_connection.?);
362375
363 if (res.request.parser.header_bytes_owned) {
364 res.request.parser.header_bytes.deinit(res.server.allocator);
365 }
376 const req_connection = res.request.headers.getFirstValue("connection");
377 const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?);
378 if (res_keepalive and req_keepalive) {
379 res.connection.conn.closing = false;
366380 } else {
367 res.request.parser.reset();
381 res.connection.conn.closing = true;
382 }
383
384 switch (res.request.compression) {
385 .none => {},
386 .deflate => |*deflate| deflate.deinit(),
387 .gzip => |*gzip| gzip.deinit(),
388 .zstd => |*zstd| zstd.deinit(),
368389 }
390
391 res.state = .start;
392 res.version = .@"HTTP/1.1";
393 res.status = .ok;
394 res.reason = null;
395
396 res.transfer_encoding = .none;
397
398 res.headers.clearRetainingCapacity();
399
400 res.request.headers.clearRetainingCapacity();
401 res.request.parser.reset();
402
403 res.request = Request{
404 .version = undefined,
405 .method = undefined,
406 .target = undefined,
407 .headers = res.request.headers,
408 .parser = res.request.parser,
409 };
410
411 return !res.connection.conn.closing;
369412 }
370413
371414 pub const DoError = BufferedConnection.WriteError || error{ UnsupportedTransferEncoding, InvalidContentLength };
372415
373416 /// Send the response headers.
374417 pub fn do(res: *Response) !void {
418 switch (res.state) {
419 .waited => res.state = .responded,
420 .first, .start, .responded, .finished => unreachable,
421 }
422
375423 var buffered = std.io.bufferedWriter(res.connection.writer());
376424 const w = buffered.writer();
377425
......@@ -452,6 +500,11 @@ pub const Response = struct {
452500
453501 /// Wait for the client to send a complete request head.
454502 pub fn wait(res: *Response) WaitError!void {
503 switch (res.state) {
504 .first, .start => res.state = .waited,
505 .waited, .responded, .finished => unreachable,
506 }
507
455508 while (true) {
456509 try res.connection.fill();
457510
......@@ -464,17 +517,6 @@ pub const Response = struct {
464517 res.request.headers = .{ .allocator = res.server.allocator, .owned = true };
465518 try res.request.parse(res.request.parser.header_bytes.items);
466519
467 const res_connection = res.headers.getFirstValue("connection");
468 const res_keepalive = res_connection != null and !std.ascii.eqlIgnoreCase("close", res_connection.?);
469
470 const req_connection = res.request.headers.getFirstValue("connection");
471 const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?);
472 if (res_keepalive and req_keepalive) {
473 res.connection.conn.closing = false;
474 } else {
475 res.connection.conn.closing = true;
476 }
477
478520 if (res.request.transfer_encoding) |te| {
479521 switch (te) {
480522 .chunked => {
......@@ -515,6 +557,11 @@ pub const Response = struct {
515557 }
516558
517559 pub fn read(res: *Response, buffer: []u8) ReadError!usize {
560 switch (res.state) {
561 .waited, .responded, .finished => {},
562 .first, .start => unreachable,
563 }
564
518565 const out_index = switch (res.request.compression) {
519566 .deflate => |*deflate| deflate.read(buffer) catch return error.DecompressionFailure,
520567 .gzip => |*gzip| gzip.read(buffer) catch return error.DecompressionFailure,
......@@ -564,6 +611,11 @@ pub const Response = struct {
564611
565612 /// Write `bytes` to the server. The `transfer_encoding` request header determines how data will be sent.
566613 pub fn write(res: *Response, bytes: []const u8) WriteError!usize {
614 switch (res.state) {
615 .responded => {},
616 .first, .waited, .start, .finished => unreachable,
617 }
618
567619 switch (res.transfer_encoding) {
568620 .chunked => {
569621 try res.connection.writer().print("{x}\r\n", .{bytes.len});
......@@ -583,7 +635,7 @@ pub const Response = struct {
583635 }
584636 }
585637
586 pub fn writeAll(req: *Request, bytes: []const u8) WriteError!void {
638 pub fn writeAll(req: *Response, bytes: []const u8) WriteError!void {
587639 var index: usize = 0;
588640 while (index < bytes.len) {
589641 index += try write(req, bytes[index..]);
......@@ -594,6 +646,11 @@ pub const Response = struct {
594646
595647 /// Finish the body of a request. This notifies the server that you have no more data to send.
596648 pub fn finish(res: *Response) FinishError!void {
649 switch (res.state) {
650 .responded => res.state = .finished,
651 .first, .waited, .start, .finished => unreachable,
652 }
653
597654 switch (res.transfer_encoding) {
598655 .chunked => try res.connection.writeAll("0\r\n\r\n"),
599656 .content_length => |len| if (len != 0) return error.MessageNotCompleted,
......@@ -636,11 +693,10 @@ pub const HeaderStrategy = union(enum) {
636693};
637694
638695/// Accept a new connection and allocate a Response for it.
639pub fn accept(server: *Server, options: HeaderStrategy) AcceptError!*Response {
696pub fn accept(server: *Server, options: HeaderStrategy) AcceptError!Response {
640697 const in = try server.socket.accept();
641698
642 const res = try server.allocator.create(Response);
643 res.* = .{
699 return Response{
644700 .server = server,
645701 .address = in.address,
646702 .connection = .{ .conn = .{
......@@ -652,14 +708,13 @@ pub fn accept(server: *Server, options: HeaderStrategy) AcceptError!*Response {
652708 .version = undefined,
653709 .method = undefined,
654710 .target = undefined,
711 .headers = .{ .allocator = server.allocator, .owned = false },
655712 .parser = switch (options) {
656713 .dynamic => |max| proto.HeadersParser.initDynamic(max),
657714 .static => |buf| proto.HeadersParser.initStatic(buf),
658715 },
659716 },
660717 };
661
662 return res;
663718}
664719
665720test "HTTP server handles a chunked transfer coding request" {
lib/std/http/test.zig deleted-72
......@@ -1,72 +0,0 @@
1const std = @import("std");
2const expect = std.testing.expect;
3
4test "client requests server" {
5 const builtin = @import("builtin");
6
7 // This test requires spawning threads.
8 if (builtin.single_threaded) {
9 return error.SkipZigTest;
10 }
11
12 const native_endian = comptime builtin.cpu.arch.endian();
13 if (builtin.zig_backend == .stage2_llvm and native_endian == .Big) {
14 // https://github.com/ziglang/zig/issues/13782
15 return error.SkipZigTest;
16 }
17
18 if (builtin.os.tag == .wasi) return error.SkipZigTest;
19
20 const allocator = std.testing.allocator;
21
22 const max_header_size = 8192;
23 var server = std.http.Server.init(allocator, .{ .reuse_address = true });
24 defer server.deinit();
25
26 const address = try std.net.Address.parseIp("127.0.0.1", 0);
27 try server.listen(address);
28 const server_port = server.socket.listen_address.in.getPort();
29
30 const server_thread = try std.Thread.spawn(.{}, (struct {
31 fn apply(s: *std.http.Server) !void {
32 const res = try s.accept(.{ .dynamic = max_header_size });
33 defer res.deinit();
34 defer res.reset();
35 try res.wait();
36
37 const server_body: []const u8 = "message from server!\n";
38 res.transfer_encoding = .{ .content_length = server_body.len };
39 try res.headers.append("content-type", "text/plain");
40 try res.headers.append("connection", "close");
41 try res.do();
42
43 var buf: [128]u8 = undefined;
44 const n = try res.readAll(&buf);
45 try expect(std.mem.eql(u8, buf[0..n], "Hello, World!\n"));
46 _ = try res.writer().writeAll(server_body);
47 try res.finish();
48 }
49 }).apply, .{&server});
50
51 var uri_buf: [22]u8 = undefined;
52 const uri = try std.Uri.parse(try std.fmt.bufPrint(&uri_buf, "http://127.0.0.1:{d}", .{server_port}));
53 var client = std.http.Client{ .allocator = allocator };
54 defer client.deinit();
55 var client_headers = std.http.Headers{ .allocator = allocator };
56 defer client_headers.deinit();
57 var client_req = try client.request(.POST, uri, client_headers, .{});
58 defer client_req.deinit();
59
60 client_req.transfer_encoding = .{ .content_length = 14 }; // this will be checked to ensure you sent exactly 14 bytes
61 try client_req.start(); // this sends the request
62 try client_req.writeAll("Hello, ");
63 try client_req.writeAll("World!\n");
64 try client_req.finish();
65 try client_req.wait(); // this waits for a response
66
67 const body = try client_req.reader().readAllAlloc(allocator, 8192 * 1024);
68 defer allocator.free(body);
69 try expect(std.mem.eql(u8, body, "message from server!\n"));
70
71 server_thread.join();
72}
test/standalone.zig+4
......@@ -55,6 +55,10 @@ pub const simple_cases = [_]SimpleCase{
5555 .os_filter = .windows,
5656 .link_libc = true,
5757 },
58 .{
59 .src_path = "test/standalone/http.zig",
60 .all_modes = true,
61 },
5862
5963 // Ensure the development tools are buildable. Alphabetically sorted.
6064 // No need to build `tools/spirv/grammar.zig`.
test/standalone/http.zig created+432
......@@ -0,0 +1,432 @@
1const std = @import("std");
2
3const http = std.http;
4const Server = http.Server;
5const Client = http.Client;
6
7const mem = std.mem;
8const testing = std.testing;
9
10const max_header_size = 8192;
11
12var gpa_server = std.heap.GeneralPurposeAllocator(.{}){};
13var gpa_client = std.heap.GeneralPurposeAllocator(.{}){};
14
15const salloc = gpa_server.allocator();
16const calloc = gpa_client.allocator();
17
18fn handleRequest(res: *Server.Response) !void {
19 const log = std.log.scoped(.server);
20
21 log.info("{s} {s} {s}", .{ @tagName(res.request.method), @tagName(res.request.version), res.request.target });
22
23 const body = try res.reader().readAllAlloc(salloc, 8192);
24 defer salloc.free(body);
25
26 if (res.request.headers.contains("connection")) {
27 try res.headers.append("connection", "keep-alive");
28 }
29
30 if (mem.startsWith(u8, res.request.target, "/get")) {
31 if (std.mem.indexOf(u8, res.request.target, "?chunked") != null) {
32 res.transfer_encoding = .chunked;
33 } else {
34 res.transfer_encoding = .{ .content_length = 14 };
35 }
36
37 try res.headers.append("content-type", "text/plain");
38
39 try res.do();
40 if (res.request.method != .HEAD) {
41 try res.writeAll("Hello, ");
42 try res.writeAll("World!\n");
43 try res.finish();
44 }
45 } else if (mem.eql(u8, res.request.target, "/echo-content")) {
46 try testing.expectEqualStrings("Hello, World!\n", body);
47 try testing.expectEqualStrings("text/plain", res.request.headers.getFirstValue("content-type").?);
48
49 if (res.request.headers.contains("transfer-encoding")) {
50 try testing.expectEqualStrings("chunked", res.request.headers.getFirstValue("transfer-encoding").?);
51 res.transfer_encoding = .chunked;
52 } else {
53 res.transfer_encoding = .{ .content_length = 14 };
54 try testing.expectEqualStrings("14", res.request.headers.getFirstValue("content-length").?);
55 }
56
57 try res.do();
58 try res.writeAll("Hello, ");
59 try res.writeAll("World!\n");
60 try res.finish();
61 } else if (mem.eql(u8, res.request.target, "/trailer")) {
62 res.transfer_encoding = .chunked;
63
64 try res.do();
65 try res.writeAll("Hello, ");
66 try res.writeAll("World!\n");
67 // try res.finish();
68 try res.connection.writeAll("0\r\nX-Checksum: aaaa\r\n\r\n");
69 } else if (mem.eql(u8, res.request.target, "/redirect/1")) {
70 res.transfer_encoding = .chunked;
71
72 res.status = .found;
73 try res.headers.append("location", "../../get");
74
75 try res.do();
76 try res.writeAll("Hello, ");
77 try res.writeAll("Redirected!\n");
78 try res.finish();
79 } else if (mem.eql(u8, res.request.target, "/redirect/2")) {
80 res.transfer_encoding = .chunked;
81
82 res.status = .found;
83 try res.headers.append("location", "/redirect/1");
84
85 try res.do();
86 try res.writeAll("Hello, ");
87 try res.writeAll("Redirected!\n");
88 try res.finish();
89 } else if (mem.eql(u8, res.request.target, "/redirect/3")) {
90 res.transfer_encoding = .chunked;
91
92 const location = try std.fmt.allocPrint(salloc, "http://127.0.0.1:{d}/redirect/2", .{res.server.socket.listen_address.getPort()});
93 defer salloc.free(location);
94
95 res.status = .found;
96 try res.headers.append("location", location);
97
98 try res.do();
99 try res.writeAll("Hello, ");
100 try res.writeAll("Redirected!\n");
101 try res.finish();
102 } else if (mem.eql(u8, res.request.target, "/redirect/4")) {
103 res.transfer_encoding = .chunked;
104
105 res.status = .found;
106 try res.headers.append("location", "/redirect/3");
107
108 try res.do();
109 try res.writeAll("Hello, ");
110 try res.writeAll("Redirected!\n");
111 try res.finish();
112 } else {
113 res.status = .not_found;
114 try res.do();
115 }
116}
117
118var handle_new_requests = true;
119
120fn runServer(srv: *Server) !void {
121 outer: while (handle_new_requests) {
122 var res = try srv.accept(.{ .dynamic = max_header_size });
123 defer res.deinit();
124
125 while (res.reset()) {
126 res.wait() catch |err| switch (err) {
127 error.HttpHeadersInvalid => continue :outer,
128 error.EndOfStream => continue,
129 else => return err,
130 };
131
132 try handleRequest(&res);
133 }
134 }
135}
136
137fn serverThread(srv: *Server) void {
138 defer srv.deinit();
139 defer _ = gpa_server.deinit();
140
141 runServer(srv) catch |err| {
142 std.debug.print("server error: {}\n", .{err});
143
144 if (@errorReturnTrace()) |trace| {
145 std.debug.dumpStackTrace(trace.*);
146 }
147
148 _ = gpa_server.deinit();
149 std.os.exit(1);
150 };
151}
152
153fn killServer(addr: std.net.Address) void {
154 handle_new_requests = false;
155
156 const conn = std.net.tcpConnectToAddress(addr) catch return;
157 conn.close();
158}
159
160pub fn main() !void {
161 const log = std.log.scoped(.client);
162
163 defer _ = gpa_client.deinit();
164
165 var server = Server.init(salloc, .{ .reuse_address = true });
166
167 const addr = std.net.Address.parseIp("127.0.0.1", 0) catch unreachable;
168 try server.listen(addr);
169
170 const port = server.socket.listen_address.getPort();
171
172 const server_thread = try std.Thread.spawn(.{}, serverThread, .{&server});
173
174 var client = Client{ .allocator = calloc };
175
176 defer client.deinit();
177
178 { // read content-length response
179 var h = http.Headers{ .allocator = calloc };
180 defer h.deinit();
181
182 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get", .{port});
183 defer calloc.free(location);
184 const uri = try std.Uri.parse(location);
185
186 log.info("{s}", .{location});
187 var req = try client.request(.GET, uri, h, .{});
188 defer req.deinit();
189
190 try req.start();
191 try req.wait();
192
193 const body = try req.reader().readAllAlloc(calloc, 8192);
194 defer calloc.free(body);
195
196 try testing.expectEqualStrings("Hello, World!\n", body);
197 try testing.expectEqualStrings("text/plain", req.response.headers.getFirstValue("content-type").?);
198 }
199
200 { // send head request and not read chunked
201 var h = http.Headers{ .allocator = calloc };
202 defer h.deinit();
203
204 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get", .{port});
205 defer calloc.free(location);
206 const uri = try std.Uri.parse(location);
207
208 log.info("{s}", .{location});
209 var req = try client.request(.HEAD, uri, h, .{});
210 defer req.deinit();
211
212 try req.start();
213 try req.wait();
214
215 const body = try req.reader().readAllAlloc(calloc, 8192);
216 defer calloc.free(body);
217
218 try testing.expectEqualStrings("", body);
219 try testing.expectEqualStrings("text/plain", req.response.headers.getFirstValue("content-type").?);
220 try testing.expectEqualStrings("14", req.response.headers.getFirstValue("content-length").?);
221 }
222
223 { // read chunked response
224 var h = http.Headers{ .allocator = calloc };
225 defer h.deinit();
226
227 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get?chunked", .{port});
228 defer calloc.free(location);
229 const uri = try std.Uri.parse(location);
230
231 log.info("{s}", .{location});
232 var req = try client.request(.GET, uri, h, .{});
233 defer req.deinit();
234
235 try req.start();
236 try req.wait();
237
238 const body = try req.reader().readAllAlloc(calloc, 8192);
239 defer calloc.free(body);
240
241 try testing.expectEqualStrings("Hello, World!\n", body);
242 try testing.expectEqualStrings("text/plain", req.response.headers.getFirstValue("content-type").?);
243 }
244
245 { // send head request and not read chunked
246 var h = http.Headers{ .allocator = calloc };
247 defer h.deinit();
248
249 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get?chunked", .{port});
250 defer calloc.free(location);
251 const uri = try std.Uri.parse(location);
252
253 log.info("{s}", .{location});
254 var req = try client.request(.HEAD, uri, h, .{});
255 defer req.deinit();
256
257 try req.start();
258 try req.wait();
259
260 const body = try req.reader().readAllAlloc(calloc, 8192);
261 defer calloc.free(body);
262
263 try testing.expectEqualStrings("", body);
264 try testing.expectEqualStrings("text/plain", req.response.headers.getFirstValue("content-type").?);
265 try testing.expectEqualStrings("chunked", req.response.headers.getFirstValue("transfer-encoding").?);
266 }
267
268 { // check trailing headers
269 var h = http.Headers{ .allocator = calloc };
270 defer h.deinit();
271
272 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/trailer", .{port});
273 defer calloc.free(location);
274 const uri = try std.Uri.parse(location);
275
276 log.info("{s}", .{location});
277 var req = try client.request(.GET, uri, h, .{});
278 defer req.deinit();
279
280 try req.start();
281 try req.wait();
282
283 const body = try req.reader().readAllAlloc(calloc, 8192);
284 defer calloc.free(body);
285
286 try testing.expectEqualStrings("Hello, World!\n", body);
287 try testing.expectEqualStrings("aaaa", req.response.headers.getFirstValue("x-checksum").?);
288 }
289
290 { // send content-length request
291 var h = http.Headers{ .allocator = calloc };
292 defer h.deinit();
293
294 try h.append("content-type", "text/plain");
295
296 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/echo-content", .{port});
297 defer calloc.free(location);
298 const uri = try std.Uri.parse(location);
299
300 log.info("{s}", .{location});
301 var req = try client.request(.POST, uri, h, .{});
302 defer req.deinit();
303
304 req.transfer_encoding = .{ .content_length = 14 };
305
306 try req.start();
307 try req.writeAll("Hello, ");
308 try req.writeAll("World!\n");
309 try req.finish();
310
311 try req.wait();
312
313 const body = try req.reader().readAllAlloc(calloc, 8192);
314 defer calloc.free(body);
315
316 try testing.expectEqualStrings("Hello, World!\n", body);
317 }
318
319 { // send chunked request
320 var h = http.Headers{ .allocator = calloc };
321 defer h.deinit();
322
323 try h.append("content-type", "text/plain");
324
325 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/echo-content", .{port});
326 defer calloc.free(location);
327 const uri = try std.Uri.parse(location);
328
329 log.info("{s}", .{location});
330 var req = try client.request(.POST, uri, h, .{});
331 defer req.deinit();
332
333 req.transfer_encoding = .chunked;
334
335 try req.start();
336 try req.writeAll("Hello, ");
337 try req.writeAll("World!\n");
338 try req.finish();
339
340 try req.wait();
341
342 const body = try req.reader().readAllAlloc(calloc, 8192);
343 defer calloc.free(body);
344
345 try testing.expectEqualStrings("Hello, World!\n", body);
346 }
347
348 { // relative redirect
349 var h = http.Headers{ .allocator = calloc };
350 defer h.deinit();
351
352 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/1", .{port});
353 defer calloc.free(location);
354 const uri = try std.Uri.parse(location);
355
356 log.info("{s}", .{location});
357 var req = try client.request(.GET, uri, h, .{});
358 defer req.deinit();
359
360 try req.start();
361 try req.wait();
362
363 const body = try req.reader().readAllAlloc(calloc, 8192);
364 defer calloc.free(body);
365
366 try testing.expectEqualStrings("Hello, World!\n", body);
367 }
368
369 { // redirect from root
370 var h = http.Headers{ .allocator = calloc };
371 defer h.deinit();
372
373 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/2", .{port});
374 defer calloc.free(location);
375 const uri = try std.Uri.parse(location);
376
377 log.info("{s}", .{location});
378 var req = try client.request(.GET, uri, h, .{});
379 defer req.deinit();
380
381 try req.start();
382 try req.wait();
383
384 const body = try req.reader().readAllAlloc(calloc, 8192);
385 defer calloc.free(body);
386
387 try testing.expectEqualStrings("Hello, World!\n", body);
388 }
389
390 { // absolute redirect
391 var h = http.Headers{ .allocator = calloc };
392 defer h.deinit();
393
394 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/3", .{port});
395 defer calloc.free(location);
396 const uri = try std.Uri.parse(location);
397
398 log.info("{s}", .{location});
399 var req = try client.request(.GET, uri, h, .{});
400 defer req.deinit();
401
402 try req.start();
403 try req.wait();
404
405 const body = try req.reader().readAllAlloc(calloc, 8192);
406 defer calloc.free(body);
407
408 try testing.expectEqualStrings("Hello, World!\n", body);
409 }
410
411 { // too many redirects
412 var h = http.Headers{ .allocator = calloc };
413 defer h.deinit();
414
415 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/4", .{port});
416 defer calloc.free(location);
417 const uri = try std.Uri.parse(location);
418
419 log.info("{s}", .{location});
420 var req = try client.request(.GET, uri, h, .{});
421 defer req.deinit();
422
423 try req.start();
424 req.wait() catch |err| switch (err) {
425 error.TooManyHttpRedirects => {},
426 else => return err,
427 };
428 }
429
430 killServer(server.socket.listen_address);
431 server_thread.join();
432}