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