1const builtin = @import("builtin");
2
3const std = @import("std");
4const http = std.http;
5const mem = std.mem;
6const net = std.Io.net;
7const Io = std.Io;
8const expect = std.testing.expect;
9const expectEqual = std.testing.expectEqual;
10const expectEqualStrings = std.testing.expectEqualStrings;
11const expectError = std.testing.expectError;
12
13test "content length reader state update" {
14 var in = Io.Reader.fixed("HTTP/1.1 200 OK\r\nContent-Length: 6\r\n\r\nHello!\r\nHTTP/1.1 200 OK\r\n\r\n");
15 var reader: http.Reader = .{
16 .in = &in,
17 .interface = undefined,
18 .state = .ready,
19 .max_head_len = 1024,
20 };
21
22 _ = try reader.receiveHead();
23 var body: [6]u8 = undefined;
24 _ = try reader.bodyReader(&.{}, .none, body.len).readSliceAll(&body);
25 try expectEqual(.ready, reader.state);
26 _ = try reader.receiveHead();
27
28 in.seek = 0;
29 _ = try reader.receiveHead();
30 try reader.bodyReader(&.{}, .none, body.len).discardAll(body.len);
31 try expectEqual(.ready, reader.state);
32 _ = try reader.receiveHead();
33}
34
35test "trailers" {
36 if (builtin.os.tag == .openbsd) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/30806
37
38 const io = std.testing.io;
39 const test_server = try createTestServer(io, struct {
40 fn run(test_server: *TestServer) anyerror!void {
41 const net_server = &test_server.net_server;
42 var recv_buffer: [1024]u8 = undefined;
43 var send_buffer: [1024]u8 = undefined;
44 var remaining: usize = 1;
45 while (remaining != 0) : (remaining -= 1) {
46 var stream = try net_server.accept(io);
47 defer stream.close(io);
48
49 var connection_br = stream.reader(io, &recv_buffer);
50 var connection_bw = stream.writer(io, &send_buffer);
51 var server = http.Server.init(&connection_br.interface, &connection_bw.interface);
52
53 try expectEqual(.ready, server.reader.state);
54 var request = try server.receiveHead();
55 try serve(&request);
56 try expectEqual(.ready, server.reader.state);
57 }
58 }
59
60 fn serve(request: *http.Server.Request) !void {
61 try expectEqualStrings(request.head.target, "/trailer");
62
63 var response = try request.respondStreaming(&.{}, .{});
64 try response.writer.writeAll("Hello, ");
65 try response.flush();
66 try response.writer.writeAll("World!\n");
67 try response.flush();
68 try response.endChunked(.{
69 .trailers = &.{
70 .{ .name = "X-Checksum", .value = "aaaa" },
71 },
72 });
73 }
74 });
75 defer test_server.destroy();
76
77 const gpa = std.testing.allocator;
78
79 var client: http.Client = .{ .allocator = gpa, .io = io };
80 defer client.deinit();
81
82 const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/trailer", .{
83 test_server.port(),
84 });
85 defer gpa.free(location);
86 const uri = try std.Uri.parse(location);
87
88 {
89 var req = try client.request(.GET, uri, .{});
90 defer req.deinit();
91
92 try req.sendBodiless();
93 var response = try req.receiveHead(&.{});
94
95 {
96 var it = response.head.iterateHeaders();
97 const header = it.next().?;
98 try expectEqualStrings("transfer-encoding", header.name);
99 try expectEqualStrings("chunked", header.value);
100 try expectEqual(null, it.next());
101 }
102
103 const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
104 defer gpa.free(body);
105
106 try expectEqualStrings("Hello, World!\n", body);
107
108 {
109 var it = response.iterateTrailers();
110 const header = it.next().?;
111 try expectEqualStrings("X-Checksum", header.name);
112 try expectEqualStrings("aaaa", header.value);
113 try expectEqual(null, it.next());
114 }
115 }
116
117 // connection has been kept alive
118 try expect(client.connection_pool.free_len == 1);
119}
120
121test "HTTP server handles a chunked transfer coding request" {
122 if (builtin.os.tag == .openbsd) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/30806
123
124 const io = std.testing.io;
125 const test_server = try createTestServer(io, struct {
126 fn run(test_server: *TestServer) anyerror!void {
127 const net_server = &test_server.net_server;
128 var recv_buffer: [8192]u8 = undefined;
129 var send_buffer: [500]u8 = undefined;
130 var stream = try net_server.accept(io);
131 defer stream.close(io);
132
133 var connection_br = stream.reader(io, &recv_buffer);
134 var connection_bw = stream.writer(io, &send_buffer);
135 var server = http.Server.init(&connection_br.interface, &connection_bw.interface);
136 var request = try server.receiveHead();
137
138 try expect(request.head.transfer_encoding == .chunked);
139
140 var buf: [128]u8 = undefined;
141 var br = try request.readerExpectContinue(&.{});
142 const n = try br.readSliceShort(&buf);
143 try expectEqualStrings("ABCD", buf[0..n]);
144
145 try request.respond("message from server!\n", .{
146 .extra_headers = &.{
147 .{ .name = "content-type", .value = "text/plain" },
148 },
149 .keep_alive = false,
150 });
151 }
152 });
153 defer test_server.destroy();
154
155 const request_bytes =
156 "POST / HTTP/1.1\r\n" ++
157 "Content-Type: text/plain\r\n" ++
158 "Transfer-Encoding: chunked\r\n" ++
159 "\r\n" ++
160 "1\r\n" ++
161 "A\r\n" ++
162 "1\r\n" ++
163 "B\r\n" ++
164 "2\r\n" ++
165 "CD\r\n" ++
166 "0\r\n" ++
167 "\r\n";
168
169 const host_name: net.HostName = try .init("127.0.0.1");
170 var stream = try host_name.connect(io, test_server.port(), .{ .mode = .stream });
171 defer stream.close(io);
172 var stream_writer = stream.writer(io, &.{});
173 try stream_writer.interface.writeAll(request_bytes);
174
175 const gpa = std.testing.allocator;
176 const expected_response =
177 "HTTP/1.1 200 OK\r\n" ++
178 "connection: close\r\n" ++
179 "content-length: 21\r\n" ++
180 "content-type: text/plain\r\n" ++
181 "\r\n" ++
182 "message from server!\n";
183 var stream_reader = stream.reader(io, &.{});
184 const response = try stream_reader.interface.allocRemaining(gpa, .limited(expected_response.len + 1));
185 defer gpa.free(response);
186 try expectEqualStrings(expected_response, response);
187}
188
189test "echo content server" {
190 if (builtin.os.tag == .openbsd) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/30806
191
192 const io = std.testing.io;
193 const test_server = try createTestServer(io, struct {
194 fn run(test_server: *TestServer) anyerror!void {
195 const net_server = &test_server.net_server;
196 var recv_buffer: [1024]u8 = undefined;
197 var send_buffer: [100]u8 = undefined;
198
199 accept: while (!test_server.shutting_down) {
200 var stream = try net_server.accept(io);
201 defer stream.close(io);
202
203 var connection_br = stream.reader(io, &recv_buffer);
204 var connection_bw = stream.writer(io, &send_buffer);
205 var http_server = http.Server.init(&connection_br.interface, &connection_bw.interface);
206
207 while (http_server.reader.state == .ready) {
208 var request = http_server.receiveHead() catch |err| switch (err) {
209 error.HttpConnectionClosing => continue :accept,
210 else => |e| return e,
211 };
212 if (mem.eql(u8, request.head.target, "/end")) {
213 return request.respond("", .{ .keep_alive = false });
214 }
215 if (request.head.expect) |expect_header_value| {
216 if (mem.eql(u8, expect_header_value, "garbage")) {
217 try expectError(error.HttpExpectationFailed, request.readerExpectContinue(&.{}));
218 request.head.expect = null;
219 try request.respond("", .{
220 .keep_alive = false,
221 .status = .expectation_failed,
222 });
223 continue;
224 }
225 }
226 handleRequest(&request) catch |err| {
227 // This message helps the person troubleshooting determine whether
228 // output comes from the server thread or the client thread.
229 std.debug.print("handleRequest failed with '{s}'\n", .{@errorName(err)});
230 return err;
231 };
232 }
233 }
234 }
235
236 fn handleRequest(request: *http.Server.Request) !void {
237 //std.debug.print("server received {s} {s} {s}\n", .{
238 // @tagName(request.head.method),
239 // @tagName(request.head.version),
240 // request.head.target,
241 //});
242
243 try expect(mem.startsWith(u8, request.head.target, "/echo-content"));
244 try expectEqualStrings("text/plain", request.head.content_type.?);
245
246 // head strings expire here
247 const body = try (try request.readerExpectContinue(&.{})).allocRemaining(std.testing.allocator, .unlimited);
248 defer std.testing.allocator.free(body);
249
250 try expectEqualStrings("Hello, World!\n", body);
251
252 var response = try request.respondStreaming(&.{}, .{
253 .content_length = switch (request.head.transfer_encoding) {
254 .chunked => null,
255 .none => len: {
256 try expectEqual(14, request.head.content_length.?);
257 break :len 14;
258 },
259 },
260 });
261 try response.flush(); // Test an early flush to send the HTTP headers before the body.
262 const w = &response.writer;
263 try w.writeAll("Hello, ");
264 try w.writeAll("World!\n");
265 try response.end();
266 //std.debug.print(" server finished responding\n", .{});
267 }
268 });
269 defer test_server.destroy();
270
271 {
272 var client: http.Client = .{ .allocator = std.testing.allocator, .io = io };
273 defer client.deinit();
274
275 try echoTests(&client, test_server.port());
276 }
277}
278
279test "Server.Request.respondStreaming non-chunked, unknown content-length" {
280 if (builtin.os.tag == .openbsd) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/30806
281
282 const io = std.testing.io;
283
284 if (builtin.cpu.arch == .aarch64 and builtin.os.tag == .windows) {
285 // https://github.com/ziglang/zig/issues/21457
286 return error.SkipZigTest;
287 }
288
289 // In this case, the response is expected to stream until the connection is
290 // closed, indicating the end of the body.
291 const test_server = try createTestServer(io, struct {
292 fn run(test_server: *TestServer) anyerror!void {
293 const net_server = &test_server.net_server;
294 var recv_buffer: [1000]u8 = undefined;
295 var send_buffer: [500]u8 = undefined;
296 var remaining: usize = 1;
297 while (remaining != 0) : (remaining -= 1) {
298 var stream = try net_server.accept(io);
299 defer stream.close(io);
300
301 var connection_br = stream.reader(io, &recv_buffer);
302 var connection_bw = stream.writer(io, &send_buffer);
303 var server = http.Server.init(&connection_br.interface, &connection_bw.interface);
304
305 try expectEqual(.ready, server.reader.state);
306 var request = try server.receiveHead();
307 try expectEqualStrings(request.head.target, "/foo");
308 var buf: [30]u8 = undefined;
309 var response = try request.respondStreaming(&buf, .{
310 .respond_options = .{
311 .transfer_encoding = .none,
312 },
313 });
314 const w = &response.writer;
315 for (0..500) |i| {
316 try w.print("{d}, ah ha ha!\n", .{i});
317 }
318 try w.flush();
319 try response.end();
320 try expectEqual(.closing, server.reader.state);
321 }
322 }
323 });
324 defer test_server.destroy();
325
326 const request_bytes = "GET /foo HTTP/1.1\r\n\r\n";
327 const host_name: net.HostName = try .init("127.0.0.1");
328 var stream = try host_name.connect(io, test_server.port(), .{ .mode = .stream });
329 defer stream.close(io);
330 var stream_writer = stream.writer(io, &.{});
331 try stream_writer.interface.writeAll(request_bytes);
332
333 var stream_reader = stream.reader(io, &.{});
334 const gpa = std.testing.allocator;
335 const response = try stream_reader.interface.allocRemaining(gpa, .unlimited);
336 defer gpa.free(response);
337
338 var expected_response = std.array_list.Managed(u8).init(gpa);
339 defer expected_response.deinit();
340
341 try expected_response.appendSlice("HTTP/1.1 200 OK\r\nconnection: close\r\n\r\n");
342
343 {
344 var total: usize = 0;
345 for (0..500) |i| {
346 var buf: [30]u8 = undefined;
347 const line = try std.mem.print(&buf, "{d}, ah ha ha!\n", .{i});
348 try expected_response.appendSlice(line);
349 total += line.len;
350 }
351 try expectEqual(7390, total);
352 }
353
354 try expectEqualStrings(expected_response.items, response);
355}
356
357test "receiving arbitrary http headers from the client" {
358 if (builtin.os.tag == .openbsd) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/30806
359
360 const io = std.testing.io;
361
362 const test_server = try createTestServer(io, struct {
363 fn run(test_server: *TestServer) anyerror!void {
364 const net_server = &test_server.net_server;
365 var recv_buffer: [666]u8 = undefined;
366 var send_buffer: [777]u8 = undefined;
367 var remaining: usize = 1;
368 while (remaining != 0) : (remaining -= 1) {
369 var stream = try net_server.accept(io);
370 defer stream.close(io);
371
372 var connection_br = stream.reader(io, &recv_buffer);
373 var connection_bw = stream.writer(io, &send_buffer);
374 var server = http.Server.init(&connection_br.interface, &connection_bw.interface);
375
376 try expectEqual(.ready, server.reader.state);
377 var request = try server.receiveHead();
378 try expectEqualStrings("/bar", request.head.target);
379 var it = request.iterateHeaders();
380 {
381 const header = it.next().?;
382 try expectEqualStrings("CoNneCtIoN", header.name);
383 try expectEqualStrings("close", header.value);
384 try expect(!it.is_trailer);
385 }
386 {
387 const header = it.next().?;
388 try expectEqualStrings("aoeu", header.name);
389 try expectEqualStrings("asdf", header.value);
390 try expect(!it.is_trailer);
391 }
392 try request.respond("", .{});
393 }
394 }
395 });
396 defer test_server.destroy();
397
398 const request_bytes = "GET /bar HTTP/1.1\r\n" ++
399 "CoNneCtIoN:close\r\n" ++
400 "aoeu: asdf \r\n" ++
401 "\r\n";
402 const host_name: net.HostName = try .init("127.0.0.1");
403 var stream = try host_name.connect(io, test_server.port(), .{ .mode = .stream });
404 defer stream.close(io);
405 var stream_writer = stream.writer(io, &.{});
406 try stream_writer.interface.writeAll(request_bytes);
407
408 var stream_reader = stream.reader(io, &.{});
409 const gpa = std.testing.allocator;
410 const response = try stream_reader.interface.allocRemaining(gpa, .unlimited);
411 defer gpa.free(response);
412
413 var expected_response = std.array_list.Managed(u8).init(gpa);
414 defer expected_response.deinit();
415
416 try expected_response.appendSlice("HTTP/1.1 200 OK\r\n");
417 try expected_response.appendSlice("connection: close\r\n");
418 try expected_response.appendSlice("content-length: 0\r\n\r\n");
419 try expectEqualStrings(expected_response.items, response);
420}
421
422test "general client/server API coverage" {
423 if (builtin.os.tag == .netbsd) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/36649
424 if (builtin.os.tag == .openbsd) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/30806
425
426 const io = std.testing.io;
427
428 const test_server = try createTestServer(io, struct {
429 fn run(test_server: *TestServer) anyerror!void {
430 const net_server = &test_server.net_server;
431 var recv_buffer: [1024]u8 = undefined;
432 var send_buffer: [100]u8 = undefined;
433
434 outer: while (!test_server.shutting_down) {
435 var stream = try net_server.accept(io);
436 defer stream.close(io);
437
438 var connection_br = stream.reader(io, &recv_buffer);
439 var connection_bw = stream.writer(io, &send_buffer);
440 var http_server = http.Server.init(&connection_br.interface, &connection_bw.interface);
441
442 while (http_server.reader.state == .ready) {
443 var request = http_server.receiveHead() catch |err| switch (err) {
444 error.HttpConnectionClosing => continue :outer,
445 else => |e| return e,
446 };
447
448 try handleRequest(&request, net_server.socket.address.getPort());
449 }
450 }
451 }
452
453 fn handleRequest(request: *http.Server.Request, listen_port: u16) !void {
454 const log = std.log.scoped(.server);
455 const gpa = std.testing.allocator;
456
457 log.info("{t} {t} {s}", .{ request.head.method, request.head.version, request.head.target });
458 const target = try gpa.dupe(u8, request.head.target);
459 defer gpa.free(target);
460
461 const reader = (try request.readerExpectContinue(&.{}));
462 const body = try reader.allocRemaining(gpa, .unlimited);
463 defer gpa.free(body);
464
465 if (mem.startsWith(u8, target, "/get")) {
466 var response = try request.respondStreaming(&.{}, .{
467 .content_length = if (mem.find(u8, target, "?chunked") == null)
468 14
469 else
470 null,
471 .respond_options = .{
472 .extra_headers = &.{
473 .{ .name = "content-type", .value = "text/plain" },
474 },
475 },
476 });
477 const w = &response.writer;
478 try w.writeAll("Hello, ");
479 try w.writeAll("World!\n");
480 try response.end();
481 // Writing again would cause an assertion failure.
482 } else if (mem.startsWith(u8, target, "/large")) {
483 var response = try request.respondStreaming(&.{}, .{
484 .content_length = 14 * 1024 + 14 * 10,
485 });
486
487 try response.flush(); // Test an early flush to send the HTTP headers before the body.
488
489 const w = &response.writer;
490
491 var i: u32 = 0;
492 while (i < 5) : (i += 1) {
493 try w.writeAll("Hello, World!\n");
494 }
495
496 var vec: [1][]const u8 = .{"Hello, World!\n"};
497 try w.writeSplatAll(&vec, 1024);
498
499 i = 0;
500 while (i < 5) : (i += 1) {
501 try w.writeAll("Hello, World!\n");
502 }
503
504 try response.end();
505 } else if (mem.eql(u8, target, "/redirect/1")) {
506 var response = try request.respondStreaming(&.{}, .{
507 .respond_options = .{
508 .status = .found,
509 .extra_headers = &.{
510 .{ .name = "location", .value = "../../get" },
511 },
512 },
513 });
514
515 const w = &response.writer;
516 try w.writeAll("Hello, ");
517 try w.writeAll("Redirected!\n");
518 try response.end();
519 } else if (mem.eql(u8, target, "/redirect/2")) {
520 try request.respond("Hello, Redirected!\n", .{
521 .status = .found,
522 .extra_headers = &.{
523 .{ .name = "location", .value = "/redirect/1" },
524 },
525 });
526 } else if (mem.eql(u8, target, "/redirect/3")) {
527 const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/redirect/2", .{
528 listen_port,
529 });
530 defer gpa.free(location);
531
532 try request.respond("Hello, Redirected!\n", .{
533 .status = .found,
534 .extra_headers = &.{
535 .{ .name = "location", .value = location },
536 },
537 });
538 } else if (mem.eql(u8, target, "/redirect/4")) {
539 try request.respond("Hello, Redirected!\n", .{
540 .status = .found,
541 .extra_headers = &.{
542 .{ .name = "location", .value = "/redirect/3" },
543 },
544 });
545 } else if (mem.eql(u8, target, "/redirect/5")) {
546 try request.respond("Hello, Redirected!\n", .{
547 .status = .found,
548 .extra_headers = &.{
549 .{ .name = "location", .value = "/%2525" },
550 },
551 });
552 } else if (mem.eql(u8, target, "/%2525")) {
553 try request.respond("Encoded redirect successful!\n", .{});
554 } else if (mem.eql(u8, target, "/redirect/invalid")) {
555 const invalid_port = try getUnusedTcpPort();
556 const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}", .{invalid_port});
557 defer gpa.free(location);
558
559 try request.respond("", .{
560 .status = .found,
561 .extra_headers = &.{
562 .{ .name = "location", .value = location },
563 },
564 });
565 } else if (mem.eql(u8, target, "/empty")) {
566 try request.respond("", .{
567 .extra_headers = &.{
568 .{ .name = "empty", .value = "" },
569 },
570 });
571 } else {
572 try request.respond("", .{ .status = .not_found });
573 }
574 }
575
576 fn getUnusedTcpPort() !u16 {
577 const addr = try net.IpAddress.parse("127.0.0.1", 0);
578 var s = try addr.listen(io, .{});
579 defer s.deinit(io);
580 return s.socket.address.getPort();
581 }
582 });
583 defer test_server.destroy();
584
585 const log = std.log.scoped(.client);
586
587 const gpa = std.testing.allocator;
588 var client: http.Client = .{ .allocator = gpa, .io = io };
589 defer client.deinit();
590
591 const port = test_server.port();
592
593 { // read content-length response
594 const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/get", .{port});
595 defer gpa.free(location);
596 const uri = try std.Uri.parse(location);
597
598 log.info("{s}", .{location});
599 var redirect_buffer: [1024]u8 = undefined;
600 var req = try client.request(.GET, uri, .{});
601 defer req.deinit();
602
603 try req.sendBodiless();
604 var response = try req.receiveHead(&redirect_buffer);
605
606 try expectEqualStrings("text/plain", response.head.content_type.?);
607
608 const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
609 defer gpa.free(body);
610
611 try expectEqualStrings("Hello, World!\n", body);
612 }
613
614 // connection has been kept alive
615 try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
616
617 { // read large content-length response
618 const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/large", .{port});
619 defer gpa.free(location);
620 const uri = try std.Uri.parse(location);
621
622 log.info("{s}", .{location});
623 var redirect_buffer: [1024]u8 = undefined;
624 var req = try client.request(.GET, uri, .{});
625 defer req.deinit();
626
627 try req.sendBodiless();
628 var response = try req.receiveHead(&redirect_buffer);
629
630 const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
631 defer gpa.free(body);
632
633 try expectEqual(@as(usize, 14 * 1024 + 14 * 10), body.len);
634 }
635
636 // connection has been kept alive
637 try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
638
639 { // send head request and not read chunked
640 const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/get", .{port});
641 defer gpa.free(location);
642 const uri = try std.Uri.parse(location);
643
644 log.info("{s}", .{location});
645 var redirect_buffer: [1024]u8 = undefined;
646 var req = try client.request(.HEAD, uri, .{});
647 defer req.deinit();
648
649 try req.sendBodiless();
650 var response = try req.receiveHead(&redirect_buffer);
651
652 try expectEqualStrings("text/plain", response.head.content_type.?);
653 try expectEqual(14, response.head.content_length.?);
654
655 const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
656 defer gpa.free(body);
657
658 try expectEqualStrings("", body);
659 }
660
661 // connection has been kept alive
662 try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
663
664 { // read chunked response
665 const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/get?chunked", .{port});
666 defer gpa.free(location);
667 const uri = try std.Uri.parse(location);
668
669 log.info("{s}", .{location});
670 var redirect_buffer: [1024]u8 = undefined;
671 var req = try client.request(.GET, uri, .{});
672 defer req.deinit();
673
674 try req.sendBodiless();
675 var response = try req.receiveHead(&redirect_buffer);
676
677 try expectEqualStrings("text/plain", response.head.content_type.?);
678
679 const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
680 defer gpa.free(body);
681
682 try expectEqualStrings("Hello, World!\n", body);
683 }
684
685 // connection has been kept alive
686 try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
687
688 { // send head request and not read chunked
689 const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/get?chunked", .{port});
690 defer gpa.free(location);
691 const uri = try std.Uri.parse(location);
692
693 log.info("{s}", .{location});
694 var redirect_buffer: [1024]u8 = undefined;
695 var req = try client.request(.HEAD, uri, .{});
696 defer req.deinit();
697
698 try req.sendBodiless();
699 var response = try req.receiveHead(&redirect_buffer);
700
701 try expectEqualStrings("text/plain", response.head.content_type.?);
702 try expect(response.head.transfer_encoding == .chunked);
703
704 const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
705 defer gpa.free(body);
706
707 try expectEqualStrings("", body);
708 }
709
710 // connection has been kept alive
711 try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
712
713 { // read content-length response with connection close
714 const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/get", .{port});
715 defer gpa.free(location);
716 const uri = try std.Uri.parse(location);
717
718 log.info("{s}", .{location});
719 var redirect_buffer: [1024]u8 = undefined;
720 var req = try client.request(.GET, uri, .{
721 .keep_alive = false,
722 });
723 defer req.deinit();
724
725 try req.sendBodiless();
726 var response = try req.receiveHead(&redirect_buffer);
727
728 try expectEqualStrings("text/plain", response.head.content_type.?);
729
730 const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
731 defer gpa.free(body);
732
733 try expectEqualStrings("Hello, World!\n", body);
734 }
735
736 // connection has been closed
737 try expect(client.connection_pool.free_len == 0);
738
739 { // handle empty header field value
740 const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/empty", .{port});
741 defer gpa.free(location);
742 const uri = try std.Uri.parse(location);
743
744 log.info("{s}", .{location});
745 var redirect_buffer: [1024]u8 = undefined;
746 var req = try client.request(.GET, uri, .{
747 .extra_headers = &.{
748 .{ .name = "empty", .value = "" },
749 },
750 });
751 defer req.deinit();
752
753 try req.sendBodiless();
754 var response = try req.receiveHead(&redirect_buffer);
755
756 try std.testing.expectEqual(.ok, response.head.status);
757
758 var it = response.head.iterateHeaders();
759 {
760 const header = it.next().?;
761 try expect(!it.is_trailer);
762 try expectEqualStrings("content-length", header.name);
763 try expectEqualStrings("0", header.value);
764 }
765
766 const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
767 defer gpa.free(body);
768
769 try expectEqualStrings("", body);
770
771 {
772 const header = it.next().?;
773 try expect(!it.is_trailer);
774 try expectEqualStrings("empty", header.name);
775 try expectEqualStrings("", header.value);
776 }
777 try expectEqual(null, it.next());
778 }
779
780 // connection has been kept alive
781 try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
782
783 { // relative redirect
784 const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/redirect/1", .{port});
785 defer gpa.free(location);
786 const uri = try std.Uri.parse(location);
787
788 log.info("{s}", .{location});
789 var redirect_buffer: [1024]u8 = undefined;
790 var req = try client.request(.GET, uri, .{});
791 defer req.deinit();
792
793 try req.sendBodiless();
794 var response = try req.receiveHead(&redirect_buffer);
795
796 const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
797 defer gpa.free(body);
798
799 try expectEqualStrings("Hello, World!\n", body);
800 }
801
802 // connection has been kept alive
803 try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
804
805 { // redirect from root
806 const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/redirect/2", .{port});
807 defer gpa.free(location);
808 const uri = try std.Uri.parse(location);
809
810 log.info("{s}", .{location});
811 var redirect_buffer: [1024]u8 = undefined;
812 var req = try client.request(.GET, uri, .{});
813 defer req.deinit();
814
815 try req.sendBodiless();
816 var response = try req.receiveHead(&redirect_buffer);
817
818 const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
819 defer gpa.free(body);
820
821 try expectEqualStrings("Hello, World!\n", body);
822 }
823
824 // connection has been kept alive
825 try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
826
827 { // absolute redirect
828 const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/redirect/3", .{port});
829 defer gpa.free(location);
830 const uri = try std.Uri.parse(location);
831
832 log.info("{s}", .{location});
833 var redirect_buffer: [1024]u8 = undefined;
834 var req = try client.request(.GET, uri, .{});
835 defer req.deinit();
836
837 try req.sendBodiless();
838 var response = try req.receiveHead(&redirect_buffer);
839
840 const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
841 defer gpa.free(body);
842
843 try expectEqualStrings("Hello, World!\n", body);
844 }
845
846 // connection has been kept alive
847 try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
848
849 { // too many redirects
850 const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/redirect/4", .{port});
851 defer gpa.free(location);
852 const uri = try std.Uri.parse(location);
853
854 log.info("{s}", .{location});
855 var redirect_buffer: [1024]u8 = undefined;
856 var req = try client.request(.GET, uri, .{});
857 defer req.deinit();
858
859 try req.sendBodiless();
860 if (req.receiveHead(&redirect_buffer)) |_| {
861 return error.TestFailed;
862 } else |err| switch (err) {
863 error.TooManyHttpRedirects => {},
864 else => return err,
865 }
866 }
867
868 { // redirect to encoded url
869 const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/redirect/5", .{port});
870 defer gpa.free(location);
871 const uri = try std.Uri.parse(location);
872
873 log.info("{s}", .{location});
874 var redirect_buffer: [1024]u8 = undefined;
875 var req = try client.request(.GET, uri, .{});
876 defer req.deinit();
877
878 try req.sendBodiless();
879 var response = try req.receiveHead(&redirect_buffer);
880
881 const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
882 defer gpa.free(body);
883
884 try expectEqualStrings("Encoded redirect successful!\n", body);
885 }
886
887 // connection has been kept alive
888 try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
889
890 { // check client without segfault by connection error after redirection
891 const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/redirect/invalid", .{port});
892 defer gpa.free(location);
893 const uri = try std.Uri.parse(location);
894
895 log.info("{s}", .{location});
896 var redirect_buffer: [1024]u8 = undefined;
897 var req = try client.request(.GET, uri, .{});
898 defer req.deinit();
899
900 try req.sendBodiless();
901 const result = req.receiveHead(&redirect_buffer);
902
903 // a proxy without an upstream is likely to return a 5xx status.
904 if (client.http_proxy == null) {
905 try expectError(error.ConnectionRefused, result); // expects not segfault but the regular error
906 }
907 }
908
909 // connection has been kept alive
910 try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
911}
912
913test "Server streams both reading and writing" {
914 if (builtin.os.tag == .openbsd) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/30806
915
916 const io = std.testing.io;
917
918 const test_server = try createTestServer(io, struct {
919 fn run(test_server: *TestServer) anyerror!void {
920 const net_server = &test_server.net_server;
921 var recv_buffer: [1024]u8 = undefined;
922 var send_buffer: [777]u8 = undefined;
923
924 var stream = try net_server.accept(io);
925 defer stream.close(io);
926
927 var connection_br = stream.reader(io, &recv_buffer);
928 var connection_bw = stream.writer(io, &send_buffer);
929 var server = http.Server.init(&connection_br.interface, &connection_bw.interface);
930 var request = try server.receiveHead();
931 var read_buffer: [100]u8 = undefined;
932 var br = try request.readerExpectContinue(&read_buffer);
933 var response = try request.respondStreaming(&.{}, .{
934 .respond_options = .{
935 .transfer_encoding = .none, // Causes keep_alive=false
936 },
937 });
938 const w = &response.writer;
939
940 while (true) {
941 try response.flush();
942 const buf = br.peekGreedy(1) catch |err| switch (err) {
943 error.EndOfStream => break,
944 error.ReadFailed => |e| return e,
945 };
946 br.toss(buf.len);
947 for (buf) |*b| b.* = std.ascii.toUpper(b.*);
948 try w.writeAll(buf);
949 }
950 try response.end();
951 }
952 });
953 defer test_server.destroy();
954
955 var client: http.Client = .{
956 .allocator = std.testing.allocator,
957 .io = io,
958 };
959 defer client.deinit();
960
961 var redirect_buffer: [555]u8 = undefined;
962 var req = try client.request(.POST, .{
963 .scheme = "http",
964 .host = .{ .raw = "127.0.0.1" },
965 .port = test_server.port(),
966 .path = .{ .percent_encoded = "/" },
967 }, .{});
968 defer req.deinit();
969
970 req.transfer_encoding = .chunked;
971 var body_writer = try req.sendBody(&.{});
972 var response = try req.receiveHead(&redirect_buffer);
973
974 try body_writer.writer.writeAll("one ");
975 try body_writer.writer.writeAll("fish");
976 try body_writer.end();
977
978 const body = try response.reader(&.{}).allocRemaining(std.testing.allocator, .unlimited);
979 defer std.testing.allocator.free(body);
980
981 try expectEqualStrings("ONE FISH", body);
982}
983
984fn echoTests(client: *http.Client, port: u16) !void {
985 const gpa = std.testing.allocator;
986 var location_buffer: [100]u8 = undefined;
987
988 { // send content-length request
989 const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/echo-content", .{port});
990 defer gpa.free(location);
991 const uri = try std.Uri.parse(location);
992
993 var redirect_buffer: [1024]u8 = undefined;
994 var req = try client.request(.POST, uri, .{
995 .extra_headers = &.{
996 .{ .name = "content-type", .value = "text/plain" },
997 },
998 });
999 defer req.deinit();
1000
1001 req.transfer_encoding = .{ .content_length = 14 };
1002
1003 var body_writer = try req.sendBody(&.{});
1004 try body_writer.writer.writeAll("Hello, ");
1005 try body_writer.writer.writeAll("World!\n");
1006 try body_writer.end();
1007
1008 var response = try req.receiveHead(&redirect_buffer);
1009
1010 const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
1011 defer gpa.free(body);
1012
1013 try expectEqualStrings("Hello, World!\n", body);
1014 }
1015
1016 // connection has been kept alive
1017 try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
1018
1019 { // send chunked request
1020 const uri = try std.Uri.parse(try std.mem.print(
1021 &location_buffer,
1022 "http://127.0.0.1:{d}/echo-content",
1023 .{port},
1024 ));
1025
1026 var redirect_buffer: [1024]u8 = undefined;
1027 var req = try client.request(.POST, uri, .{
1028 .extra_headers = &.{
1029 .{ .name = "content-type", .value = "text/plain" },
1030 },
1031 });
1032 defer req.deinit();
1033
1034 req.transfer_encoding = .chunked;
1035
1036 var body_writer = try req.sendBody(&.{});
1037 try body_writer.writer.writeAll("Hello, ");
1038 try body_writer.writer.writeAll("World!\n");
1039 try body_writer.end();
1040
1041 var response = try req.receiveHead(&redirect_buffer);
1042
1043 const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
1044 defer gpa.free(body);
1045
1046 try expectEqualStrings("Hello, World!\n", body);
1047 }
1048
1049 // connection has been kept alive
1050 try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
1051
1052 { // Client.fetch()
1053
1054 const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/echo-content#fetch", .{port});
1055 defer gpa.free(location);
1056
1057 var body: std.Io.Writer.Allocating = .init(gpa);
1058 defer body.deinit();
1059 try body.ensureUnusedCapacity(64);
1060
1061 const res = try client.fetch(.{
1062 .location = .{ .url = location },
1063 .method = .POST,
1064 .payload = "Hello, World!\n",
1065 .extra_headers = &.{
1066 .{ .name = "content-type", .value = "text/plain" },
1067 },
1068 .response_writer = &body.writer,
1069 });
1070 try expectEqual(.ok, res.status);
1071 try expectEqualStrings("Hello, World!\n", body.written());
1072 }
1073
1074 { // expect: 100-continue
1075 const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/echo-content#expect-100", .{port});
1076 defer gpa.free(location);
1077 const uri = try std.Uri.parse(location);
1078
1079 var redirect_buffer: [1024]u8 = undefined;
1080 var req = try client.request(.POST, uri, .{
1081 .extra_headers = &.{
1082 .{ .name = "expect", .value = "100-continue" },
1083 .{ .name = "content-type", .value = "text/plain" },
1084 },
1085 });
1086 defer req.deinit();
1087
1088 req.transfer_encoding = .chunked;
1089
1090 var body_writer = try req.sendBody(&.{});
1091 try body_writer.writer.writeAll("Hello, ");
1092 try body_writer.writer.writeAll("World!\n");
1093 try body_writer.end();
1094
1095 var response = try req.receiveHead(&redirect_buffer);
1096 try expectEqual(.ok, response.head.status);
1097
1098 const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited);
1099 defer gpa.free(body);
1100
1101 try expectEqualStrings("Hello, World!\n", body);
1102 }
1103
1104 { // expect: garbage
1105 const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/echo-content#expect-garbage", .{port});
1106 defer gpa.free(location);
1107 const uri = try std.Uri.parse(location);
1108
1109 var redirect_buffer: [1024]u8 = undefined;
1110 var req = try client.request(.POST, uri, .{
1111 .extra_headers = &.{
1112 .{ .name = "content-type", .value = "text/plain" },
1113 .{ .name = "expect", .value = "garbage" },
1114 },
1115 });
1116 defer req.deinit();
1117
1118 req.transfer_encoding = .chunked;
1119
1120 var body_writer = try req.sendBody(&.{});
1121 try body_writer.flush();
1122 var response = try req.receiveHead(&redirect_buffer);
1123 try expectEqual(.expectation_failed, response.head.status);
1124 _ = try response.reader(&.{}).discardRemaining();
1125 }
1126}
1127
1128const TestServer = struct {
1129 io: Io,
1130 shutting_down: bool,
1131 server_thread: std.Thread,
1132 net_server: net.Server,
1133
1134 fn destroy(self: *@This()) void {
1135 const io = self.io;
1136 self.shutting_down = true;
1137 var stream = self.net_server.socket.address.connect(io, .{ .mode = .stream }) catch
1138 @panic("shutdown failure");
1139 stream.close(io);
1140
1141 self.server_thread.join();
1142 self.net_server.deinit(io);
1143 std.testing.allocator.destroy(self);
1144 }
1145
1146 fn port(self: @This()) u16 {
1147 return self.net_server.socket.address.getPort();
1148 }
1149};
1150
1151fn createTestServer(io: Io, S: type) !*TestServer {
1152 if (builtin.single_threaded) return error.SkipZigTest;
1153
1154 const address = try net.IpAddress.parse("127.0.0.1", 0);
1155
1156 const gpa = std.testing.allocator;
1157
1158 const test_server = try gpa.create(TestServer);
1159 errdefer gpa.destroy(test_server);
1160
1161 var net_server = try address.listen(io, .{ .reuse_address = true });
1162 errdefer net_server.deinit(io);
1163
1164 // populate `test_server` first so `S.run` can use it
1165 test_server.* = .{
1166 .io = io,
1167 .net_server = net_server,
1168 .shutting_down = false,
1169 .server_thread = undefined, // set below
1170 };
1171
1172 test_server.server_thread = try .spawn(.{}, S.run, .{test_server});
1173 errdefer comptime unreachable;
1174
1175 return test_server;
1176}
1177
1178test "redirect to different connection" {
1179 if (builtin.os.tag == .openbsd) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/30806
1180
1181 const io = std.testing.io;
1182 const test_server_new = try createTestServer(io, struct {
1183 fn run(test_server: *TestServer) anyerror!void {
1184 const net_server = &test_server.net_server;
1185 var recv_buffer: [888]u8 = undefined;
1186 var send_buffer: [777]u8 = undefined;
1187
1188 var stream = try net_server.accept(io);
1189 defer stream.close(io);
1190
1191 var connection_br = stream.reader(io, &recv_buffer);
1192 var connection_bw = stream.writer(io, &send_buffer);
1193 var server = http.Server.init(&connection_br.interface, &connection_bw.interface);
1194 var request = try server.receiveHead();
1195 try expectEqualStrings(request.head.target, "/ok");
1196 try request.respond("good job, you pass", .{});
1197 }
1198 });
1199 defer test_server_new.destroy();
1200
1201 const global = struct {
1202 var other_port: ?u16 = null;
1203 };
1204 global.other_port = test_server_new.port();
1205
1206 const test_server_orig = try createTestServer(io, struct {
1207 fn run(test_server: *TestServer) anyerror!void {
1208 const net_server = &test_server.net_server;
1209 var recv_buffer: [999]u8 = undefined;
1210 var send_buffer: [100]u8 = undefined;
1211
1212 var stream = try net_server.accept(io);
1213 defer stream.close(io);
1214
1215 var loc_buf: [50]u8 = undefined;
1216 const new_loc = try std.mem.print(&loc_buf, "http://127.0.0.1:{d}/ok", .{
1217 global.other_port.?,
1218 });
1219
1220 var connection_br = stream.reader(io, &recv_buffer);
1221 var connection_bw = stream.writer(io, &send_buffer);
1222 var server = http.Server.init(&connection_br.interface, &connection_bw.interface);
1223 var request = try server.receiveHead();
1224 try expectEqualStrings(request.head.target, "/help");
1225 try request.respond("", .{
1226 .status = .found,
1227 .extra_headers = &.{
1228 .{ .name = "location", .value = new_loc },
1229 },
1230 });
1231 }
1232 });
1233 defer test_server_orig.destroy();
1234
1235 const gpa = std.testing.allocator;
1236
1237 var client: http.Client = .{
1238 .allocator = gpa,
1239 .io = io,
1240 };
1241 defer client.deinit();
1242
1243 var loc_buf: [100]u8 = undefined;
1244 const location = try std.mem.print(&loc_buf, "http://127.0.0.1:{d}/help", .{
1245 test_server_orig.port(),
1246 });
1247 const uri = try std.Uri.parse(location);
1248
1249 {
1250 var redirect_buffer: [666]u8 = undefined;
1251 var req = try client.request(.GET, uri, .{});
1252 defer req.deinit();
1253
1254 try req.sendBodiless();
1255 var response = try req.receiveHead(&redirect_buffer);
1256 var reader = response.reader(&.{});
1257
1258 const body = try reader.allocRemaining(gpa, .unlimited);
1259 defer gpa.free(body);
1260
1261 try expectEqualStrings("good job, you pass", body);
1262 }
1263}
1264
1265test "boot failed connections from the pool" {
1266 if (builtin.os.tag == .openbsd) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/30806
1267
1268 const io = std.testing.io;
1269 const gpa = std.testing.allocator;
1270
1271 const test_server_orig = try createTestServer(io, struct {
1272 fn run(test_server: *TestServer) anyerror!void {
1273 const net_server = &test_server.net_server;
1274 var recv_buffer: [500]u8 = undefined;
1275 var send_buffer: [500]u8 = undefined;
1276
1277 accept: while (!test_server.shutting_down) {
1278 var stream = try net_server.accept(io);
1279 defer stream.close(io);
1280
1281 for (0..2) |i| {
1282 var connection_br = stream.reader(io, &recv_buffer);
1283 var connection_bw = stream.writer(io, &send_buffer);
1284 var server = http.Server.init(&connection_br.interface, &connection_bw.interface);
1285 var request = server.receiveHead() catch |err| switch (err) {
1286 error.HttpConnectionClosing => continue :accept,
1287 else => |e| return e,
1288 };
1289 if (i == 0) try request.respond("hello", .{});
1290 }
1291 }
1292 }
1293 });
1294 defer test_server_orig.destroy();
1295
1296 var client: http.Client = .{
1297 .allocator = gpa,
1298 .io = io,
1299 };
1300 defer client.deinit();
1301
1302 var loc_buf: [100]u8 = undefined;
1303 const location = try std.mem.print(&loc_buf, "http://127.0.0.1:{d}/", .{
1304 test_server_orig.port(),
1305 });
1306 const uri = try std.Uri.parse(location);
1307
1308 {
1309 const response = try client.fetch(.{ .location = .{ .uri = uri } });
1310 try expectEqual(.ok, response.status);
1311 }
1312 {
1313 try expectError(error.HttpConnectionClosing, client.fetch(.{ .location = .{ .uri = uri } }));
1314 }
1315 {
1316 const response = try client.fetch(.{ .location = .{ .uri = uri } });
1317 try expectEqual(.ok, response.status);
1318 }
1319 {
1320 try expectError(error.HttpConnectionClosing, client.fetch(.{ .location = .{ .uri = uri } }));
1321 }
1322}