authorgravatar for truemedian@gmail.comNameless <truemedian@gmail.com> 2023-08-22 10:05:03-05:00
committergravatar for truemedian@gmail.comNameless <truemedian@gmail.com> 2023-08-29 21:42:53-05:00
logaa090a49d94155c4804644377db110f3b13f0500
tree06226127b3e279414c6a8f1ca2a12f7c6b75f308
parent5d40338f21b468c82d4bc2a1ac0a35c643126e74
signaturelock-open Commit is signed but in an unrecognized format.

std.http: handle expect:100-continue and continue responses


4 files changed, 156 insertions(+), 42 deletions(-)

lib/std/http/Client.zig+40-5
...@@ -478,6 +478,7 @@ pub const Request = struct {...@@ -478,6 +478,7 @@ pub const Request = struct {
478 .zstd => |*zstd| zstd.deinit(),478 .zstd => |*zstd| zstd.deinit(),
479 }479 }
480480
481 req.headers.deinit();
481 req.response.headers.deinit();482 req.response.headers.deinit();
482483
483 if (req.response.parser.header_bytes_owned) {484 if (req.response.parser.header_bytes_owned) {
...@@ -667,17 +668,19 @@ pub const Request = struct {...@@ -667,17 +668,19 @@ pub const Request = struct {
667668
668 try req.response.parse(req.response.parser.header_bytes.items, false);669 try req.response.parse(req.response.parser.header_bytes.items, false);
669670
670 if (req.response.status == .switching_protocols) {671 if (req.response.status == .@"continue") {
671 req.connection.?.data.closing = false;672 req.response.parser.done = true; // we're done parsing the continue response, reset to prepare for the real response
672 req.response.parser.done = true;673 req.response.parser.reset();
674 break;
673 }675 }
674676
675 if (req.method == .CONNECT and req.response.status == .ok) {677 // we're switching protocols, so this connection is no longer doing http
678 if (req.response.status == .switching_protocols or (req.method == .CONNECT and req.response.status == .ok)) {
676 req.connection.?.data.closing = false;679 req.connection.?.data.closing = false;
677 req.response.parser.done = true;680 req.response.parser.done = true;
678 }681 }
679682
680 // we default to using keep-alive if not provided683 // we default to using keep-alive if not provided in the client if the server asks for it
681 const req_connection = req.headers.getFirstValue("connection");684 const req_connection = req.headers.getFirstValue("connection");
682 const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?);685 const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?);
683686
...@@ -955,6 +958,38 @@ pub fn connectUnproxied(client: *Client, host: []const u8, port: u16, protocol:...@@ -955,6 +958,38 @@ pub fn connectUnproxied(client: *Client, host: []const u8, port: u16, protocol:
955 return conn;958 return conn;
956}959}
957960
961pub const ConnectUnixError = Allocator.Error || std.os.SocketError || error{NameTooLong} || std.os.ConnectError;
962
963pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*ConnectionPool.Node {
964 if (client.connection_pool.findConnection(.{
965 .host = path,
966 .port = 0,
967 .is_tls = false,
968 })) |node|
969 return node;
970
971 const conn = try client.allocator.create(ConnectionPool.Node);
972 errdefer client.allocator.destroy(conn);
973 conn.* = .{ .data = undefined };
974
975 const stream = try std.net.connectUnixSocket(path);
976 errdefer stream.close();
977
978 conn.data = .{
979 .stream = stream,
980 .tls_client = undefined,
981 .protocol = .plain,
982
983 .host = try client.allocator.dupe(u8, path),
984 .port = 0,
985 };
986 errdefer client.allocator.free(conn.data.host);
987
988 client.connection_pool.addUsed(conn);
989
990 return conn;
991}
992
958// Prevents a dependency loop in request()993// Prevents a dependency loop in request()
959const ConnectErrorPartial = ConnectUnproxiedError || error{ UnsupportedUrlScheme, ConnectionRefused };994const ConnectErrorPartial = ConnectUnproxiedError || error{ UnsupportedUrlScheme, ConnectionRefused };
960pub const ConnectError = ConnectErrorPartial || RequestError;995pub const ConnectError = ConnectErrorPartial || RequestError;
lib/std/http/Server.zig+41-33
...@@ -411,48 +411,52 @@ pub const Response = struct {...@@ -411,48 +411,52 @@ pub const Response = struct {
411 }411 }
412 try w.writeAll("\r\n");412 try w.writeAll("\r\n");
413413
414 if (!res.headers.contains("server")) {414 if (res.status == .@"continue") {
415 try w.writeAll("Server: zig (std.http)\r\n");415 res.state = .waited; // we still need to send another request after this
416 }416 } else {
417 if (!res.headers.contains("server")) {
418 try w.writeAll("Server: zig (std.http)\r\n");
419 }
417420
418 if (!res.headers.contains("connection")) {421 if (!res.headers.contains("connection")) {
419 const req_connection = res.request.headers.getFirstValue("connection");422 const req_connection = res.request.headers.getFirstValue("connection");
420 const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?);423 const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?);
421424
422 if (req_keepalive) {425 if (req_keepalive) {
423 try w.writeAll("Connection: keep-alive\r\n");426 try w.writeAll("Connection: keep-alive\r\n");
424 } else {427 } else {
425 try w.writeAll("Connection: close\r\n");428 try w.writeAll("Connection: close\r\n");
429 }
426 }430 }
427 }
428431
429 const has_transfer_encoding = res.headers.contains("transfer-encoding");432 const has_transfer_encoding = res.headers.contains("transfer-encoding");
430 const has_content_length = res.headers.contains("content-length");433 const has_content_length = res.headers.contains("content-length");
431434
432 if (!has_transfer_encoding and !has_content_length) {435 if (!has_transfer_encoding and !has_content_length) {
433 switch (res.transfer_encoding) {436 switch (res.transfer_encoding) {
434 .chunked => try w.writeAll("Transfer-Encoding: chunked\r\n"),437 .chunked => try w.writeAll("Transfer-Encoding: chunked\r\n"),
435 .content_length => |content_length| try w.print("Content-Length: {d}\r\n", .{content_length}),438 .content_length => |content_length| try w.print("Content-Length: {d}\r\n", .{content_length}),
436 .none => {},439 .none => {},
437 }
438 } else {
439 if (has_content_length) {
440 const content_length = std.fmt.parseInt(u64, res.headers.getFirstValue("content-length").?, 10) catch return error.InvalidContentLength;
441
442 res.transfer_encoding = .{ .content_length = content_length };
443 } else if (has_transfer_encoding) {
444 const transfer_encoding = res.headers.getFirstValue("transfer-encoding").?;
445 if (std.mem.eql(u8, transfer_encoding, "chunked")) {
446 res.transfer_encoding = .chunked;
447 } else {
448 return error.UnsupportedTransferEncoding;
449 }440 }
450 } else {441 } else {
451 res.transfer_encoding = .none;442 if (has_content_length) {
443 const content_length = std.fmt.parseInt(u64, res.headers.getFirstValue("content-length").?, 10) catch return error.InvalidContentLength;
444
445 res.transfer_encoding = .{ .content_length = content_length };
446 } else if (has_transfer_encoding) {
447 const transfer_encoding = res.headers.getFirstValue("transfer-encoding").?;
448 if (std.mem.eql(u8, transfer_encoding, "chunked")) {
449 res.transfer_encoding = .chunked;
450 } else {
451 return error.UnsupportedTransferEncoding;
452 }
453 } else {
454 res.transfer_encoding = .none;
455 }
452 }456 }
453 }
454457
455 try w.print("{}", .{res.headers});458 try w.print("{}", .{res.headers});
459 }
456460
457 try w.writeAll("\r\n");461 try w.writeAll("\r\n");
458462
...@@ -516,6 +520,10 @@ pub const Response = struct {...@@ -516,6 +520,10 @@ pub const Response = struct {
516 res.request.parser.done = true;520 res.request.parser.done = true;
517 }521 }
518522
523 if (res.request.method == .HEAD) {
524 res.request.parser.done = true;
525 }
526
519 if (!res.request.parser.done) {527 if (!res.request.parser.done) {
520 if (res.request.transfer_compression) |tc| switch (tc) {528 if (res.request.transfer_compression) |tc| switch (tc) {
521 .compress => return error.CompressionNotSupported,529 .compress => return error.CompressionNotSupported,
lib/std/http/protocol.zig+6-3
...@@ -534,9 +534,9 @@ pub const HeadersParser = struct {...@@ -534,9 +534,9 @@ pub const HeadersParser = struct {
534534
535 if (r.next_chunk_length == 0) r.done = true;535 if (r.next_chunk_length == 0) r.done = true;
536536
537 return 0;537 return out_index;
538 } else {538 } else if (out_index < buffer.len) {
539 const out_avail = buffer.len;539 const out_avail = buffer.len - out_index;
540540
541 const can_read = @as(usize, @intCast(@min(data_avail, out_avail)));541 const can_read = @as(usize, @intCast(@min(data_avail, out_avail)));
542 const nread = try conn.read(buffer[0..can_read]);542 const nread = try conn.read(buffer[0..can_read]);
...@@ -545,6 +545,8 @@ pub const HeadersParser = struct {...@@ -545,6 +545,8 @@ pub const HeadersParser = struct {
545 if (r.next_chunk_length == 0) r.done = true;545 if (r.next_chunk_length == 0) r.done = true;
546546
547 return nread;547 return nread;
548 } else {
549 return out_index;
548 }550 }
549 },551 },
550 .chunk_data_suffix, .chunk_data_suffix_r, .chunk_head_size, .chunk_head_ext, .chunk_head_r => {552 .chunk_data_suffix, .chunk_data_suffix_r, .chunk_head_size, .chunk_head_ext, .chunk_head_r => {
...@@ -558,6 +560,7 @@ pub const HeadersParser = struct {...@@ -558,6 +560,7 @@ pub const HeadersParser = struct {
558 .chunk_data => if (r.next_chunk_length == 0) {560 .chunk_data => if (r.next_chunk_length == 0) {
559 if (std.mem.eql(u8, conn.peek(), "\r\n")) {561 if (std.mem.eql(u8, conn.peek(), "\r\n")) {
560 r.state = .finished;562 r.state = .finished;
563 r.done = true;
561 } else {564 } else {
562 // The trailer section is formatted identically to the header section.565 // The trailer section is formatted identically to the header section.
563 r.state = .seen_rn;566 r.state = .seen_rn;
test/standalone/http.zig+69-1
...@@ -22,6 +22,18 @@ fn handleRequest(res: *Server.Response) !void {...@@ -22,6 +22,18 @@ fn handleRequest(res: *Server.Response) !void {
2222
23 log.info("{s} {s} {s}", .{ @tagName(res.request.method), @tagName(res.request.version), res.request.target });23 log.info("{s} {s} {s}", .{ @tagName(res.request.method), @tagName(res.request.version), res.request.target });
2424
25 if (res.request.headers.contains("expect")) {
26 if (mem.eql(u8, res.request.headers.getFirstValue("expect").?, "100-continue")) {
27 res.status = .@"continue";
28 try res.do();
29 res.status = .ok;
30 } else {
31 res.status = .expectation_failed;
32 try res.do();
33 return;
34 }
35 }
36
25 const body = try res.reader().readAllAlloc(salloc, 8192);37 const body = try res.reader().readAllAlloc(salloc, 8192);
26 defer salloc.free(body);38 defer salloc.free(body);
2739
...@@ -62,7 +74,7 @@ fn handleRequest(res: *Server.Response) !void {...@@ -62,7 +74,7 @@ fn handleRequest(res: *Server.Response) !void {
62 }74 }
6375
64 try res.finish();76 try res.finish();
65 } else if (mem.eql(u8, res.request.target, "/echo-content")) {77 } else if (mem.startsWith(u8, res.request.target, "/echo-content")) {
66 try testing.expectEqualStrings("Hello, World!\n", body);78 try testing.expectEqualStrings("Hello, World!\n", body);
67 try testing.expectEqualStrings("text/plain", res.request.headers.getFirstValue("content-type").?);79 try testing.expectEqualStrings("text/plain", res.request.headers.getFirstValue("content-type").?);
6880
...@@ -592,6 +604,62 @@ pub fn main() !void {...@@ -592,6 +604,62 @@ pub fn main() !void {
592 try testing.expectEqualStrings("Hello, World!\n", res.body.?);604 try testing.expectEqualStrings("Hello, World!\n", res.body.?);
593 }605 }
594606
607 { // expect: 100-continue
608 var h = http.Headers{ .allocator = calloc };
609 defer h.deinit();
610
611 try h.append("expect", "100-continue");
612 try h.append("content-type", "text/plain");
613
614 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/echo-content#expect-100", .{port});
615 defer calloc.free(location);
616 const uri = try std.Uri.parse(location);
617
618 log.info("{s}", .{location});
619 var req = try client.request(.POST, uri, h, .{});
620 defer req.deinit();
621
622 req.transfer_encoding = .chunked;
623
624 try req.start();
625 try req.wait();
626 try testing.expectEqual(http.Status.@"continue", req.response.status);
627
628 try req.writeAll("Hello, ");
629 try req.writeAll("World!\n");
630 try req.finish();
631
632 try req.wait();
633 try testing.expectEqual(http.Status.ok, req.response.status);
634
635 const body = try req.reader().readAllAlloc(calloc, 8192);
636 defer calloc.free(body);
637
638 try testing.expectEqualStrings("Hello, World!\n", body);
639 }
640
641 { // expect: garbage
642 var h = http.Headers{ .allocator = calloc };
643 defer h.deinit();
644
645 try h.append("content-type", "text/plain");
646 try h.append("expect", "garbage");
647
648 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/echo-content#expect-garbage", .{port});
649 defer calloc.free(location);
650 const uri = try std.Uri.parse(location);
651
652 log.info("{s}", .{location});
653 var req = try client.request(.POST, uri, h, .{});
654 defer req.deinit();
655
656 req.transfer_encoding = .chunked;
657
658 try req.start();
659 try req.wait();
660 try testing.expectEqual(http.Status.expectation_failed, req.response.status);
661 }
662
595 { // issue 16282 *** This test leaves the client in an invalid state, it must be last ***663 { // issue 16282 *** This test leaves the client in an invalid state, it must be last ***
596 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get", .{port});664 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get", .{port});
597 defer calloc.free(location);665 defer calloc.free(location);