authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-02-12 23:03:06-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-02-23 02:37:11-07:00
log4d401e6159be774537bfcf8b57db7db1b44979e1
tree9dbfdee96b9c2cb779000c4689e4fe421cf35346
parentf46447e6a1cda2b3b2e0ee90a68b2cca112f8742

std.http: remove Headers API

I originally removed these in 402f967ed5339fa3d828b7fe1d57cdb5bf38dbf2. I allowed them to be added back in #15299 because they were smuggled in alongside a bug fix, however, I wasn't kidding when I said that I wanted to take the design of std.http in a different direction than using this data structure. Instead, some headers are provided via explicit field names populated while parsing the HTTP request/response, and some are provided via new fields that support passing extra, arbitrary headers. This resulted in simplification of logic in many places, as well as elimination of the possibility of failure in many places. There is less deinitialization code happening now. Furthermore, it made it no longer necessary to clone the headers data structure in order to handle redirects. http_proxy and https_proxy fields are now pointers since it is common for them to be unpopulated. loadDefaultProxies is changed into initDefaultProxies to communicate that it does not actually load anything from disk or from the network. The function now is leaky; the API user must pass an already instantiated arena allocator. Removes the need to deinitialize proxies. Before, proxies stored arbitrary sets of headers. Now they only store the authorization value. Removed the duplicated code between https_proxy and http_proxy. Finally, parsing failures of the environment variables result in errors being emitted rather than silently ignoring the proxy. error.CompressionNotSupported is renamed to error.CompressionUnsupported, matching the naming convention from all the other errors in the same set. Removed documentation comments that were redundant with field and type names. Disabling zstd decompression in the server for now; see #18937. I found some apparently dead code in src/Package/Fetch/git.zig. I want to check with Ian about this. I discovered that test/standalone/http.zig is dead code, it is only being compiled but not being run. Furthermore it hangs at the end if you run it manually. The previous commits in this branch were written under the assumption that this test was being run with `zig build test-standalone`.

8 files changed, 490 insertions(+), 1108 deletions(-)

lib/std/http.zig+6-5
......@@ -3,10 +3,6 @@ const std = @import("std.zig");
33pub const Client = @import("http/Client.zig");
44pub const Server = @import("http/Server.zig");
55pub const protocol = @import("http/protocol.zig");
6const headers = @import("http/Headers.zig");
7
8pub const Headers = headers.Headers;
9pub const Field = headers.Field;
106
117pub const Version = enum {
128 @"HTTP/1.0",
......@@ -18,7 +14,7 @@ pub const Version = enum {
1814/// https://datatracker.ietf.org/doc/html/rfc7231#section-4 Initial definition
1915///
2016/// https://datatracker.ietf.org/doc/html/rfc5789#section-2 PATCH
21pub const Method = enum(u64) { // TODO: should be u192 or u256, but neither is supported by the C backend, and therefore cannot pass CI
17pub const Method = enum(u64) {
2218 GET = parse("GET"),
2319 HEAD = parse("HEAD"),
2420 POST = parse("POST"),
......@@ -309,6 +305,11 @@ pub const Connection = enum {
309305 close,
310306};
311307
308pub const Header = struct {
309 name: []const u8,
310 value: []const u8,
311};
312
312313test {
313314 _ = Client;
314315 _ = Method;
lib/std/http/Client.zig+288-299
......@@ -33,13 +33,14 @@ next_https_rescan_certs: bool = true,
3333/// The pool of connections that can be reused (and currently in use).
3434connection_pool: ConnectionPool = .{},
3535
36/// This is the proxy that will handle http:// connections. It *must not* be
37/// modified when the client has any active connections.
38http_proxy: ?Proxy = null,
39
40/// This is the proxy that will handle https:// connections. It *must not* be
41/// modified when the client has any active connections.
42https_proxy: ?Proxy = null,
36/// If populated, all http traffic travels through this third party.
37/// This field cannot be modified while the client has active connections.
38/// Pointer to externally-owned memory.
39http_proxy: ?*Proxy = null,
40/// If populated, all https traffic travels through this third party.
41/// This field cannot be modified while the client has active connections.
42/// Pointer to externally-owned memory.
43https_proxy: ?*Proxy = null,
4344
4445/// A set of linked lists of connections that can be reused.
4546pub const ConnectionPool = struct {
......@@ -422,7 +423,7 @@ pub const Response = struct {
422423 HttpTransferEncodingUnsupported,
423424 HttpConnectionHeaderUnsupported,
424425 InvalidContentLength,
425 CompressionNotSupported,
426 CompressionUnsupported,
426427 };
427428
428429 pub fn parse(res: *Response, bytes: []const u8, trailing: bool) ParseError!void {
......@@ -445,8 +446,6 @@ pub const Response = struct {
445446 res.status = status;
446447 res.reason = reason;
447448
448 res.headers.clearRetainingCapacity();
449
450449 while (it.next()) |line| {
451450 if (line.len == 0) return error.HttpHeadersInvalid;
452451 switch (line[0]) {
......@@ -458,11 +457,17 @@ pub const Response = struct {
458457 const header_name = line_it.next() orelse return error.HttpHeadersInvalid;
459458 const header_value = line_it.rest();
460459
461 try res.headers.append(header_name, header_value);
462
463460 if (trailing) continue;
464461
465 if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) {
462 if (std.ascii.eqlIgnoreCase(header_name, "connection")) {
463 res.keep_alive = !std.ascii.eqlIgnoreCase(header_value, "close");
464 } else if (std.ascii.eqlIgnoreCase(header_name, "content-type")) {
465 res.content_type = header_value;
466 } else if (std.ascii.eqlIgnoreCase(header_name, "location")) {
467 res.location = header_value;
468 } else if (std.ascii.eqlIgnoreCase(header_name, "content-disposition")) {
469 res.content_disposition = header_value;
470 } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) {
466471 // Transfer-Encoding: second, first
467472 // Transfer-Encoding: deflate, chunked
468473 var iter = mem.splitBackwardsScalar(u8, header_value, ',');
......@@ -531,15 +536,19 @@ pub const Response = struct {
531536 try expectEqual(@as(u10, 999), parseInt3("999"));
532537 }
533538
534 /// The HTTP version this response is using.
535539 version: http.Version,
536
537 /// The status code of the response.
538540 status: http.Status,
539
540 /// The reason phrase of the response.
541541 reason: []const u8,
542542
543 /// Points into the user-provided `server_header_buffer`.
544 location: ?[]const u8 = null,
545 /// Points into the user-provided `server_header_buffer`.
546 content_type: ?[]const u8 = null,
547 /// Points into the user-provided `server_header_buffer`.
548 content_disposition: ?[]const u8 = null,
549
550 keep_alive: bool = false,
551
543552 /// If present, the number of bytes in the response body.
544553 content_length: ?u64 = null,
545554
......@@ -549,12 +558,11 @@ pub const Response = struct {
549558 /// If present, the compression of the response body, otherwise identity (no compression).
550559 transfer_compression: http.ContentEncoding = .identity,
551560
552 /// The headers received from the server.
553 headers: http.Headers,
554561 parser: proto.HeadersParser,
555562 compression: Compression = .none,
556563
557 /// Whether the response body should be skipped. Any data read from the response body will be discarded.
564 /// Whether the response body should be skipped. Any data read from the
565 /// response body will be discarded.
558566 skip: bool = false,
559567};
560568
......@@ -562,24 +570,15 @@ pub const Response = struct {
562570///
563571/// Order of operations: open -> send[ -> write -> finish] -> wait -> read
564572pub const Request = struct {
565 /// The uri that this request is being sent to.
566573 uri: Uri,
567
568 /// The client that this request was created from.
569574 client: *Client,
570
571 /// Underlying connection to the server. This is null when the connection is released.
575 /// This is null when the connection is released.
572576 connection: ?*Connection,
577 keep_alive: bool,
573578
574579 method: http.Method,
575580 version: http.Version = .@"HTTP/1.1",
576
577 /// The list of HTTP request headers.
578 headers: http.Headers,
579
580 /// The transfer encoding of the request body.
581 transfer_encoding: RequestTransfer = .none,
582
581 transfer_encoding: RequestTransfer,
583582 redirect_behavior: RedirectBehavior,
584583
585584 /// Whether the request should handle a 100-continue response before sending the request body.
......@@ -593,6 +592,34 @@ pub const Request = struct {
593592 /// Used as a allocator for resolving redirects locations.
594593 arena: std.heap.ArenaAllocator,
595594
595 /// Standard headers that have default, but overridable, behavior.
596 headers: Headers,
597
598 /// These headers are kept including when following a redirect to a
599 /// different domain.
600 /// Externally-owned; must outlive the Request.
601 extra_headers: []const http.Header,
602
603 /// These headers are stripped when following a redirect to a different
604 /// domain.
605 /// Externally-owned; must outlive the Request.
606 privileged_headers: []const http.Header,
607
608 pub const Headers = struct {
609 host: Value = .default,
610 authorization: Value = .default,
611 user_agent: Value = .default,
612 connection: Value = .default,
613 accept_encoding: Value = .default,
614 content_type: Value = .default,
615
616 pub const Value = union(enum) {
617 default,
618 omit,
619 override: []const u8,
620 };
621 };
622
596623 /// Any value other than `not_allowed` or `unhandled` means that integer represents
597624 /// how many remaining redirects are allowed.
598625 pub const RedirectBehavior = enum(u16) {
......@@ -621,9 +648,6 @@ pub const Request = struct {
621648 .zstd => |*zstd| zstd.deinit(),
622649 }
623650
624 req.headers.deinit();
625 req.response.headers.deinit();
626
627651 if (req.connection) |connection| {
628652 if (req.response.parser.state != .complete) {
629653 // If the response wasn't fully read, then we need to close the connection.
......@@ -664,14 +688,12 @@ pub const Request = struct {
664688 req.uri = uri;
665689 req.connection = try req.client.connect(host, port, protocol);
666690 req.redirect_behavior.subtractOne();
667 req.response.headers.clearRetainingCapacity();
668691 req.response.parser.reset();
669692
670693 req.response = .{
671694 .status = undefined,
672695 .reason = undefined,
673696 .version = undefined,
674 .headers = req.response.headers,
675697 .parser = req.response.parser,
676698 };
677699 }
......@@ -685,9 +707,11 @@ pub const Request = struct {
685707
686708 /// Send the HTTP request headers to the server.
687709 pub fn send(req: *Request, options: SendOptions) SendError!void {
688 if (!req.method.requestHasBody() and req.transfer_encoding != .none) return error.UnsupportedTransferEncoding;
710 if (!req.method.requestHasBody() and req.transfer_encoding != .none)
711 return error.UnsupportedTransferEncoding;
689712
690 const w = req.connection.?.writer();
713 const connection = req.connection.?;
714 const w = connection.writer();
691715
692716 try req.method.write(w);
693717 try w.writeByte(' ');
......@@ -696,9 +720,9 @@ pub const Request = struct {
696720 try req.uri.writeToStream(.{ .authority = true }, w);
697721 } else {
698722 try req.uri.writeToStream(.{
699 .scheme = req.connection.?.proxied,
700 .authentication = req.connection.?.proxied,
701 .authority = req.connection.?.proxied,
723 .scheme = connection.proxied,
724 .authentication = connection.proxied,
725 .authority = connection.proxied,
702726 .path = true,
703727 .query = true,
704728 .raw = options.raw_uri,
......@@ -708,97 +732,91 @@ pub const Request = struct {
708732 try w.writeAll(@tagName(req.version));
709733 try w.writeAll("\r\n");
710734
711 if (!req.headers.contains("host")) {
712 try w.writeAll("Host: ");
735 if (try emitOverridableHeader("host: ", req.headers.host, w)) {
736 try w.writeAll("host: ");
713737 try req.uri.writeToStream(.{ .authority = true }, w);
714738 try w.writeAll("\r\n");
715739 }
716740
717 if ((req.uri.user != null or req.uri.password != null) and
718 !req.headers.contains("authorization"))
719 {
720 try w.writeAll("Authorization: ");
721 const authorization = try req.connection.?.allocWriteBuffer(
722 @intCast(basic_authorization.valueLengthFromUri(req.uri)),
723 );
724 std.debug.assert(basic_authorization.value(req.uri, authorization).len == authorization.len);
725 try w.writeAll("\r\n");
741 if (try emitOverridableHeader("authorization: ", req.headers.authorization, w)) {
742 if (req.uri.user != null or req.uri.password != null) {
743 try w.writeAll("authorization: ");
744 const authorization = try connection.allocWriteBuffer(
745 @intCast(basic_authorization.valueLengthFromUri(req.uri)),
746 );
747 assert(basic_authorization.value(req.uri, authorization).len == authorization.len);
748 try w.writeAll("\r\n");
749 }
726750 }
727751
728 if (!req.headers.contains("user-agent")) {
729 try w.writeAll("User-Agent: zig/");
752 if (try emitOverridableHeader("user-agent: ", req.headers.user_agent, w)) {
753 try w.writeAll("user-agent: zig/");
730754 try w.writeAll(builtin.zig_version_string);
731755 try w.writeAll(" (std.http)\r\n");
732756 }
733757
734 if (!req.headers.contains("connection")) {
735 try w.writeAll("Connection: keep-alive\r\n");
758 if (try emitOverridableHeader("connection: ", req.headers.connection, w)) {
759 if (req.keep_alive) {
760 try w.writeAll("connection: keep-alive\r\n");
761 } else {
762 try w.writeAll("connection: close\r\n");
763 }
736764 }
737765
738 if (!req.headers.contains("accept-encoding")) {
739 try w.writeAll("Accept-Encoding: gzip, deflate, zstd\r\n");
766 if (try emitOverridableHeader("accept-encoding: ", req.headers.accept_encoding, w)) {
767 try w.writeAll("accept-encoding: gzip, deflate, zstd\r\n");
740768 }
741769
742 if (!req.headers.contains("te")) {
743 try w.writeAll("TE: gzip, deflate, trailers\r\n");
770 switch (req.transfer_encoding) {
771 .chunked => try w.writeAll("transfer-encoding: chunked\r\n"),
772 .content_length => |len| try w.print("content-length: {d}\r\n", .{len}),
773 .none => {},
744774 }
745775
746 const has_transfer_encoding = req.headers.contains("transfer-encoding");
747 const has_content_length = req.headers.contains("content-length");
748
749 if (!has_transfer_encoding and !has_content_length) {
750 switch (req.transfer_encoding) {
751 .chunked => try w.writeAll("Transfer-Encoding: chunked\r\n"),
752 .content_length => |content_length| try w.print("Content-Length: {d}\r\n", .{content_length}),
753 .none => {},
754 }
755 } else {
756 if (has_transfer_encoding) {
757 const transfer_encoding = req.headers.getFirstValue("transfer-encoding").?;
758 if (std.mem.eql(u8, transfer_encoding, "chunked")) {
759 req.transfer_encoding = .chunked;
760 } else {
761 return error.UnsupportedTransferEncoding;
762 }
763 } else if (has_content_length) {
764 const content_length = std.fmt.parseInt(u64, req.headers.getFirstValue("content-length").?, 10) catch return error.InvalidContentLength;
765
766 req.transfer_encoding = .{ .content_length = content_length };
767 } else {
768 req.transfer_encoding = .none;
769 }
776 if (try emitOverridableHeader("content-type: ", req.headers.content_type, w)) {
777 // The default is to omit content-type if not provided because
778 // "application/octet-stream" is redundant.
770779 }
771780
772 for (req.headers.list.items) |entry| {
773 if (entry.value.len == 0) continue;
781 for (req.extra_headers) |header| {
782 assert(header.value.len != 0);
774783
775 try w.writeAll(entry.name);
784 try w.writeAll(header.name);
776785 try w.writeAll(": ");
777 try w.writeAll(entry.value);
786 try w.writeAll(header.value);
778787 try w.writeAll("\r\n");
779788 }
780789
781 if (req.connection.?.proxied) {
782 const proxy_headers: ?http.Headers = switch (req.connection.?.protocol) {
783 .plain => if (req.client.http_proxy) |proxy| proxy.headers else null,
784 .tls => if (req.client.https_proxy) |proxy| proxy.headers else null,
785 };
786
787 if (proxy_headers) |headers| {
788 for (headers.list.items) |entry| {
789 if (entry.value.len == 0) continue;
790 if (connection.proxied) proxy: {
791 const proxy = switch (connection.protocol) {
792 .plain => req.client.http_proxy,
793 .tls => req.client.https_proxy,
794 } orelse break :proxy;
790795
791 try w.writeAll(entry.name);
792 try w.writeAll(": ");
793 try w.writeAll(entry.value);
794 try w.writeAll("\r\n");
795 }
796 }
796 const authorization = proxy.authorization orelse break :proxy;
797 try w.writeAll("proxy-authorization: ");
798 try w.writeAll(authorization);
799 try w.writeAll("\r\n");
797800 }
798801
799802 try w.writeAll("\r\n");
800803
801 try req.connection.?.flush();
804 try connection.flush();
805 }
806
807 /// Returns true if the default behavior is required, otherwise handles
808 /// writing (or not writing) the header.
809 fn emitOverridableHeader(prefix: []const u8, v: Headers.Value, w: anytype) !bool {
810 switch (v) {
811 .default => return true,
812 .omit => return false,
813 .override => |x| {
814 try w.writeAll(prefix);
815 try w.writeAll(x);
816 try w.writeAll("\r\n");
817 return false;
818 },
819 }
802820 }
803821
804822 const TransferReadError = Connection.ReadError || proto.HeadersParser.ReadError;
......@@ -829,7 +847,7 @@ pub const Request = struct {
829847 RedirectRequiresResend,
830848 HttpRedirectMissingLocation,
831849 CompressionInitializationFailed,
832 CompressionNotSupported,
850 CompressionUnsupported,
833851 };
834852
835853 /// Waits for a response from the server and parses any headers that are sent.
......@@ -843,12 +861,14 @@ pub const Request = struct {
843861 /// Must be called after `send` and, if any data was written to the request
844862 /// body, then also after `finish`.
845863 pub fn wait(req: *Request) WaitError!void {
864 const connection = req.connection.?;
865
846866 while (true) { // handle redirects
847867 while (true) { // read headers
848 try req.connection.?.fill();
868 try connection.fill();
849869
850 const nchecked = try req.response.parser.checkCompleteHead(req.connection.?.peek());
851 req.connection.?.drop(@intCast(nchecked));
870 const nchecked = try req.response.parser.checkCompleteHead(connection.peek());
871 connection.drop(@intCast(nchecked));
852872
853873 if (req.response.parser.state.isContent()) break;
854874 }
......@@ -856,44 +876,36 @@ pub const Request = struct {
856876 try req.response.parse(req.response.parser.get(), false);
857877
858878 if (req.response.status == .@"continue") {
859 req.response.parser.state = .complete; // we're done parsing the continue response, reset to prepare for the real response
879 // We're done parsing the continue response; reset to prepare
880 // for the real response.
881 req.response.parser.state = .complete;
860882 req.response.parser.reset();
861883
862884 if (req.handle_continue)
863885 continue;
864886
865 return; // we're not handling the 100-continue, return to the caller
887 return; // we're not handling the 100-continue
866888 }
867889
868890 // we're switching protocols, so this connection is no longer doing http
869891 if (req.method == .CONNECT and req.response.status.class() == .success) {
870 req.connection.?.closing = false;
892 connection.closing = false;
871893 req.response.parser.state = .complete;
872
873 return; // the connection is not HTTP past this point, return to the caller
894 return; // the connection is not HTTP past this point
874895 }
875896
876 // we default to using keep-alive if not provided in the client if the server asks for it
877 const req_connection = req.headers.getFirstValue("connection");
878 const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?);
897 connection.closing = !req.response.keep_alive or !req.keep_alive;
879898
880 const res_connection = req.response.headers.getFirstValue("connection");
881 const res_keepalive = res_connection != null and !std.ascii.eqlIgnoreCase("close", res_connection.?);
882 if (res_keepalive and (req_keepalive or req_connection == null)) {
883 req.connection.?.closing = false;
884 } else {
885 req.connection.?.closing = true;
886 }
887
888 // Any response to a HEAD request and any response with a 1xx (Informational), 204 (No Content), or 304 (Not Modified)
889 // status code is always terminated by the first empty line after the header fields, regardless of the header fields
890 // present in the message
899 // Any response to a HEAD request and any response with a 1xx
900 // (Informational), 204 (No Content), or 304 (Not Modified) status
901 // code is always terminated by the first empty line after the
902 // header fields, regardless of the header fields present in the
903 // message.
891904 if (req.method == .HEAD or req.response.status.class() == .informational or
892905 req.response.status == .no_content or req.response.status == .not_modified)
893906 {
894907 req.response.parser.state = .complete;
895
896 return; // the response is empty, no further setup or redirection is necessary
908 return; // The response is empty; no further setup or redirection is necessary.
897909 }
898910
899911 if (req.response.transfer_encoding != .none) {
......@@ -922,7 +934,7 @@ pub const Request = struct {
922934
923935 if (req.redirect_behavior == .not_allowed) return error.TooManyHttpRedirects;
924936
925 const location = req.response.headers.getFirstValue("location") orelse
937 const location = req.response.location orelse
926938 return error.HttpRedirectMissingLocation;
927939
928940 const arena = req.arena.allocator();
......@@ -932,42 +944,44 @@ pub const Request = struct {
932944 const new_url = Uri.parse(location_duped) catch try Uri.parseWithoutScheme(location_duped);
933945 const resolved_url = try req.uri.resolve(new_url, false, arena);
934946
935 // is the redirect location on the same domain, or a subdomain of the original request?
936947 const is_same_domain_or_subdomain =
937948 std.ascii.endsWithIgnoreCase(resolved_url.host.?, req.uri.host.?) and
938949 (resolved_url.host.?.len == req.uri.host.?.len or
939950 resolved_url.host.?[resolved_url.host.?.len - req.uri.host.?.len - 1] == '.');
940951
941 if (resolved_url.host == null or !is_same_domain_or_subdomain or !std.ascii.eqlIgnoreCase(resolved_url.scheme, req.uri.scheme)) {
942 // we're redirecting to a different domain, strip privileged headers like cookies
943 _ = req.headers.delete("authorization");
944 _ = req.headers.delete("www-authenticate");
945 _ = req.headers.delete("cookie");
946 _ = req.headers.delete("cookie2");
952 if (resolved_url.host == null or !is_same_domain_or_subdomain or
953 !std.ascii.eqlIgnoreCase(resolved_url.scheme, req.uri.scheme))
954 {
955 // When redirecting to a different domain, strip privileged headers.
956 req.privileged_headers = &.{};
947957 }
948958
949 if (req.response.status == .see_other or ((req.response.status == .moved_permanently or req.response.status == .found) and req.method == .POST)) {
950 // we're redirecting to a GET, so we need to change the method and remove the body
959 if (switch (req.response.status) {
960 .see_other => true,
961 .moved_permanently, .found => req.method == .POST,
962 else => false,
963 }) {
964 // A redirect to a GET must change the method and remove the body.
951965 req.method = .GET;
952966 req.transfer_encoding = .none;
953 _ = req.headers.delete("transfer-encoding");
954 _ = req.headers.delete("content-length");
955 _ = req.headers.delete("content-type");
967 req.headers.content_type = .omit;
956968 }
957969
958970 if (req.transfer_encoding != .none) {
959 return error.RedirectRequiresResend; // The request body has already been sent. The request is still in a valid state, but the redirect must be handled manually.
971 // The request body has already been sent. The request is
972 // still in a valid state, but the redirect must be handled
973 // manually.
974 return error.RedirectRequiresResend;
960975 }
961976
962977 try req.redirect(resolved_url);
963
964978 try req.send(.{});
965979 } else {
966980 req.response.skip = false;
967981 if (req.response.parser.state != .complete) {
968982 switch (req.response.transfer_compression) {
969983 .identity => req.response.compression = .none,
970 .compress, .@"x-compress" => return error.CompressionNotSupported,
984 .compress, .@"x-compress" => return error.CompressionUnsupported,
971985 .deflate => req.response.compression = .{
972986 .deflate = std.compress.zlib.decompressor(req.transferReader()),
973987 },
......@@ -1092,16 +1106,12 @@ pub const Request = struct {
10921106 }
10931107};
10941108
1095/// A HTTP proxy server.
10961109pub const Proxy = struct {
1097 allocator: Allocator,
1098 headers: http.Headers,
1099
11001110 protocol: Connection.Protocol,
11011111 host: []const u8,
1112 authorization: ?[]const u8,
11021113 port: u16,
1103
1104 supports_connect: bool = true,
1114 supports_connect: bool,
11051115};
11061116
11071117/// Release all associated resources with the client.
......@@ -1113,116 +1123,71 @@ pub fn deinit(client: *Client) void {
11131123
11141124 client.connection_pool.deinit(client.allocator);
11151125
1116 if (client.http_proxy) |*proxy| {
1117 proxy.allocator.free(proxy.host);
1118 proxy.headers.deinit();
1119 }
1120
1121 if (client.https_proxy) |*proxy| {
1122 proxy.allocator.free(proxy.host);
1123 proxy.headers.deinit();
1124 }
1125
11261126 if (!disable_tls)
11271127 client.ca_bundle.deinit(client.allocator);
11281128
11291129 client.* = undefined;
11301130}
11311131
1132/// Uses the *_proxy environment variable to set any unset proxies for the client.
1133/// This function *must not* be called when the client has any active connections.
1134pub fn loadDefaultProxies(client: *Client) !void {
1132/// Populates `http_proxy` and `http_proxy` via standard proxy environment variables.
1133/// Asserts the client has no active connections.
1134/// Uses `arena` for a few small allocations that must outlive the client, or
1135/// at least until those fields are set to different values.
1136pub fn initDefaultProxies(client: *Client, arena: Allocator) !void {
11351137 // Prevent any new connections from being created.
11361138 client.connection_pool.mutex.lock();
11371139 defer client.connection_pool.mutex.unlock();
11381140
1139 assert(client.connection_pool.used.first == null); // There are still active requests.
1141 assert(client.connection_pool.used.first == null); // There are active requests.
11401142
1141 if (client.http_proxy == null) http: {
1142 const content: []const u8 = if (std.process.hasEnvVarConstant("http_proxy"))
1143 try std.process.getEnvVarOwned(client.allocator, "http_proxy")
1144 else if (std.process.hasEnvVarConstant("HTTP_PROXY"))
1145 try std.process.getEnvVarOwned(client.allocator, "HTTP_PROXY")
1146 else if (std.process.hasEnvVarConstant("all_proxy"))
1147 try std.process.getEnvVarOwned(client.allocator, "all_proxy")
1148 else if (std.process.hasEnvVarConstant("ALL_PROXY"))
1149 try std.process.getEnvVarOwned(client.allocator, "ALL_PROXY")
1150 else
1151 break :http;
1152 defer client.allocator.free(content);
1153
1154 const uri = Uri.parse(content) catch
1155 Uri.parseWithoutScheme(content) catch
1156 break :http;
1157
1158 const protocol = if (uri.scheme.len == 0)
1159 .plain // No scheme, assume http://
1160 else
1161 protocol_map.get(uri.scheme) orelse break :http; // Unknown scheme, ignore
1162
1163 const host = if (uri.host) |host| try client.allocator.dupe(u8, host) else break :http; // Missing host, ignore
1164 client.http_proxy = .{
1165 .allocator = client.allocator,
1166 .headers = .{ .allocator = client.allocator },
1167
1168 .protocol = protocol,
1169 .host = host,
1170 .port = uri.port orelse switch (protocol) {
1171 .plain => 80,
1172 .tls => 443,
1173 },
1174 };
1143 if (client.http_proxy == null) {
1144 client.http_proxy = try createProxyFromEnvVar(arena, &.{
1145 "http_proxy", "HTTP_PROXY", "all_proxy", "ALL_PROXY",
1146 });
1147 }
11751148
1176 if (uri.user != null or uri.password != null) {
1177 const authorization = try client.allocator.alloc(u8, basic_authorization.valueLengthFromUri(uri));
1178 errdefer client.allocator.free(authorization);
1179 std.debug.assert(basic_authorization.value(uri, authorization).len == authorization.len);
1180 try client.http_proxy.?.headers.appendOwned(.{ .unowned = "proxy-authorization" }, .{ .owned = authorization });
1181 }
1149 if (client.https_proxy == null) {
1150 client.https_proxy = try createProxyFromEnvVar(arena, &.{
1151 "https_proxy", "HTTPS_PROXY", "all_proxy", "ALL_PROXY",
1152 });
11821153 }
1154}
11831155
1184 if (client.https_proxy == null) https: {
1185 const content: []const u8 = if (std.process.hasEnvVarConstant("https_proxy"))
1186 try std.process.getEnvVarOwned(client.allocator, "https_proxy")
1187 else if (std.process.hasEnvVarConstant("HTTPS_PROXY"))
1188 try std.process.getEnvVarOwned(client.allocator, "HTTPS_PROXY")
1189 else if (std.process.hasEnvVarConstant("all_proxy"))
1190 try std.process.getEnvVarOwned(client.allocator, "all_proxy")
1191 else if (std.process.hasEnvVarConstant("ALL_PROXY"))
1192 try std.process.getEnvVarOwned(client.allocator, "ALL_PROXY")
1193 else
1194 break :https;
1195 defer client.allocator.free(content);
1196
1197 const uri = Uri.parse(content) catch
1198 Uri.parseWithoutScheme(content) catch
1199 break :https;
1200
1201 const protocol = if (uri.scheme.len == 0)
1202 .plain // No scheme, assume http://
1203 else
1204 protocol_map.get(uri.scheme) orelse break :https; // Unknown scheme, ignore
1205
1206 const host = if (uri.host) |host| try client.allocator.dupe(u8, host) else break :https; // Missing host, ignore
1207 client.https_proxy = .{
1208 .allocator = client.allocator,
1209 .headers = .{ .allocator = client.allocator },
1210
1211 .protocol = protocol,
1212 .host = host,
1213 .port = uri.port orelse switch (protocol) {
1214 .plain => 80,
1215 .tls => 443,
1216 },
1156fn createProxyFromEnvVar(arena: Allocator, env_var_names: []const []const u8) !?*Proxy {
1157 const content = for (env_var_names) |name| {
1158 break std.process.getEnvVarOwned(arena, name) catch |err| switch (err) {
1159 error.EnvironmentVariableNotFound => continue,
1160 else => |e| return e,
12171161 };
1162 } else return null;
12181163
1219 if (uri.user != null or uri.password != null) {
1220 const authorization = try client.allocator.alloc(u8, basic_authorization.valueLengthFromUri(uri));
1221 errdefer client.allocator.free(authorization);
1222 std.debug.assert(basic_authorization.value(uri, authorization).len == authorization.len);
1223 try client.https_proxy.?.headers.appendOwned(.{ .unowned = "proxy-authorization" }, .{ .owned = authorization });
1224 }
1225 }
1164 const uri = Uri.parse(content) catch try Uri.parseWithoutScheme(content);
1165
1166 const protocol = if (uri.scheme.len == 0)
1167 .plain // No scheme, assume http://
1168 else
1169 protocol_map.get(uri.scheme) orelse return null; // Unknown scheme, ignore
1170
1171 const host = uri.host orelse return error.HttpProxyMissingHost;
1172
1173 const authorization: ?[]const u8 = if (uri.user != null or uri.password != null) a: {
1174 const authorization = try arena.alloc(u8, basic_authorization.valueLengthFromUri(uri));
1175 assert(basic_authorization.value(uri, authorization).len == authorization.len);
1176 break :a authorization;
1177 } else null;
1178
1179 const proxy = try arena.create(Proxy);
1180 proxy.* = .{
1181 .protocol = protocol,
1182 .host = host,
1183 .authorization = authorization,
1184 .port = uri.port orelse switch (protocol) {
1185 .plain => 80,
1186 .tls => 443,
1187 },
1188 .supports_connect = true,
1189 };
1190 return proxy;
12261191}
12271192
12281193pub const basic_authorization = struct {
......@@ -1244,8 +1209,8 @@ pub const basic_authorization = struct {
12441209 }
12451210
12461211 pub fn value(uri: Uri, out: []u8) []u8 {
1247 std.debug.assert(uri.user == null or uri.user.?.len <= max_user_len);
1248 std.debug.assert(uri.password == null or uri.password.?.len <= max_password_len);
1212 assert(uri.user == null or uri.user.?.len <= max_user_len);
1213 assert(uri.password == null or uri.password.?.len <= max_password_len);
12491214
12501215 @memcpy(out[0..prefix.len], prefix);
12511216
......@@ -1356,7 +1321,8 @@ pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*Connecti
13561321 return &conn.data;
13571322}
13581323
1359/// Connect to `tunnel_host:tunnel_port` using the specified proxy with HTTP CONNECT. This will reuse a connection if one is already open.
1324/// Connect to `tunnel_host:tunnel_port` using the specified proxy with HTTP
1325/// CONNECT. This will reuse a connection if one is already open.
13601326///
13611327/// This function is threadsafe.
13621328pub fn connectTunnel(
......@@ -1394,7 +1360,7 @@ pub fn connectTunnel(
13941360 };
13951361
13961362 var buffer: [8096]u8 = undefined;
1397 var req = client.open(.CONNECT, uri, proxy.headers, .{
1363 var req = client.open(.CONNECT, uri, .{
13981364 .redirect_behavior = .unhandled,
13991365 .connection = conn,
14001366 .server_header_buffer = &buffer,
......@@ -1436,42 +1402,44 @@ pub fn connectTunnel(
14361402const ConnectErrorPartial = ConnectTcpError || error{ UnsupportedUrlScheme, ConnectionRefused };
14371403pub const ConnectError = ConnectErrorPartial || RequestError;
14381404
1439/// Connect to `host:port` using the specified protocol. This will reuse a connection if one is already open.
1440/// If a proxy is configured for the client, then the proxy will be used to connect to the host.
1405/// Connect to `host:port` using the specified protocol. This will reuse a
1406/// connection if one is already open.
1407/// If a proxy is configured for the client, then the proxy will be used to
1408/// connect to the host.
14411409///
14421410/// This function is threadsafe.
1443pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectError!*Connection {
1444 // pointer required so that `supports_connect` can be updated if a CONNECT fails
1445 const potential_proxy: ?*Proxy = switch (protocol) {
1446 .plain => if (client.http_proxy) |*proxy_info| proxy_info else null,
1447 .tls => if (client.https_proxy) |*proxy_info| proxy_info else null,
1448 };
1449
1450 if (potential_proxy) |proxy| {
1451 // don't attempt to proxy the proxy thru itself.
1452 if (std.mem.eql(u8, proxy.host, host) and proxy.port == port and proxy.protocol == protocol) {
1453 return client.connectTcp(host, port, protocol);
1454 }
1455
1456 if (proxy.supports_connect) tunnel: {
1457 return connectTunnel(client, proxy, host, port) catch |err| switch (err) {
1458 error.TunnelNotSupported => break :tunnel,
1459 else => |e| return e,
1460 };
1461 }
1411pub fn connect(
1412 client: *Client,
1413 host: []const u8,
1414 port: u16,
1415 protocol: Connection.Protocol,
1416) ConnectError!*Connection {
1417 const proxy = switch (protocol) {
1418 .plain => client.http_proxy,
1419 .tls => client.https_proxy,
1420 } orelse return client.connectTcp(host, port, protocol);
1421
1422 // Prevent proxying through itself.
1423 if (std.mem.eql(u8, proxy.host, host) and proxy.port == port and proxy.protocol == protocol) {
1424 return client.connectTcp(host, port, protocol);
1425 }
14621426
1463 // fall back to using the proxy as a normal http proxy
1464 const conn = try client.connectTcp(proxy.host, proxy.port, proxy.protocol);
1465 errdefer {
1466 conn.closing = true;
1467 client.connection_pool.release(conn);
1468 }
1427 if (proxy.supports_connect) tunnel: {
1428 return connectTunnel(client, proxy, host, port) catch |err| switch (err) {
1429 error.TunnelNotSupported => break :tunnel,
1430 else => |e| return e,
1431 };
1432 }
14691433
1470 conn.proxied = true;
1471 return conn;
1434 // fall back to using the proxy as a normal http proxy
1435 const conn = try client.connectTcp(proxy.host, proxy.port, proxy.protocol);
1436 errdefer {
1437 conn.closing = true;
1438 client.connection_pool.release(conn);
14721439 }
14731440
1474 return client.connectTcp(host, port, protocol);
1441 conn.proxied = true;
1442 return conn;
14751443}
14761444
14771445pub const RequestError = ConnectTcpError || ConnectErrorPartial || Request.SendError ||
......@@ -1496,6 +1464,10 @@ pub const RequestOptions = struct {
14961464 /// you finish the request, then the request *will* deadlock.
14971465 handle_continue: bool = true,
14981466
1467 /// If false, close the connection after the one request. If true,
1468 /// participate in the client connection pool.
1469 keep_alive: bool = true,
1470
14991471 /// This field specifies whether to automatically follow redirects, and if
15001472 /// so, how many redirects to follow before returning an error.
15011473 ///
......@@ -1510,6 +1482,17 @@ pub const RequestOptions = struct {
15101482
15111483 /// Must be an already acquired connection.
15121484 connection: ?*Connection = null,
1485
1486 /// Standard headers that have default, but overridable, behavior.
1487 headers: Request.Headers = .{},
1488 /// These headers are kept including when following a redirect to a
1489 /// different domain.
1490 /// Externally-owned; must outlive the Request.
1491 extra_headers: []const http.Header = &.{},
1492 /// These headers are stripped when following a redirect to a different
1493 /// domain.
1494 /// Externally-owned; must outlive the Request.
1495 privileged_headers: []const http.Header = &.{},
15131496};
15141497
15151498pub const protocol_map = std.ComptimeStringMap(Connection.Protocol, .{
......@@ -1522,7 +1505,6 @@ pub const protocol_map = std.ComptimeStringMap(Connection.Protocol, .{
15221505/// Open a connection to the host specified by `uri` and prepare to send a HTTP request.
15231506///
15241507/// `uri` must remain alive during the entire request.
1525/// `headers` is cloned and may be freed after this function returns.
15261508///
15271509/// The caller is responsible for calling `deinit()` on the `Request`.
15281510/// This function is threadsafe.
......@@ -1530,7 +1512,6 @@ pub fn open(
15301512 client: *Client,
15311513 method: http.Method,
15321514 uri: Uri,
1533 headers: http.Headers,
15341515 options: RequestOptions,
15351516) RequestError!Request {
15361517 const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme;
......@@ -1560,19 +1541,22 @@ pub fn open(
15601541 .uri = uri,
15611542 .client = client,
15621543 .connection = conn,
1563 .headers = try headers.clone(client.allocator), // Headers must be cloned to properly handle header transformations in redirects.
1544 .keep_alive = options.keep_alive,
15641545 .method = method,
15651546 .version = options.version,
1547 .transfer_encoding = .none,
15661548 .redirect_behavior = options.redirect_behavior,
15671549 .handle_continue = options.handle_continue,
15681550 .response = .{
15691551 .status = undefined,
15701552 .reason = undefined,
15711553 .version = undefined,
1572 .headers = http.Headers{ .allocator = client.allocator, .owned = false },
15731554 .parser = proto.HeadersParser.init(options.server_header_buffer),
15741555 },
15751556 .arena = undefined,
1557 .headers = options.headers,
1558 .extra_headers = options.extra_headers,
1559 .privileged_headers = options.privileged_headers,
15761560 };
15771561 errdefer req.deinit();
15781562
......@@ -1618,25 +1602,34 @@ pub const FetchOptions = struct {
16181602
16191603 location: Location,
16201604 method: http.Method = .GET,
1621 headers: http.Headers = .{ .allocator = std.heap.page_allocator, .owned = false },
16221605 payload: Payload = .none,
16231606 raw_uri: bool = false,
1607
1608 /// Standard headers that have default, but overridable, behavior.
1609 headers: Request.Headers = .{},
1610 /// These headers are kept including when following a redirect to a
1611 /// different domain.
1612 /// Externally-owned; must outlive the Request.
1613 extra_headers: []const http.Header = &.{},
1614 /// These headers are stripped when following a redirect to a different
1615 /// domain.
1616 /// Externally-owned; must outlive the Request.
1617 privileged_headers: []const http.Header = &.{},
16241618};
16251619
16261620pub const FetchResult = struct {
16271621 status: http.Status,
16281622 body: ?[]const u8 = null,
1629 headers: http.Headers,
16301623
16311624 allocator: Allocator,
16321625 options: FetchOptions,
16331626
16341627 pub fn deinit(res: *FetchResult) void {
1635 if (res.options.response_strategy == .storage and res.options.response_strategy.storage == .dynamic) {
1628 if (res.options.response_strategy == .storage and
1629 res.options.response_strategy.storage == .dynamic)
1630 {
16361631 if (res.body) |body| res.allocator.free(body);
16371632 }
1638
1639 res.headers.deinit();
16401633 }
16411634};
16421635
......@@ -1644,21 +1637,19 @@ pub const FetchResult = struct {
16441637///
16451638/// This function is threadsafe.
16461639pub fn fetch(client: *Client, allocator: Allocator, options: FetchOptions) !FetchResult {
1647 const has_transfer_encoding = options.headers.contains("transfer-encoding");
1648 const has_content_length = options.headers.contains("content-length");
1649
1650 if (has_content_length or has_transfer_encoding) return error.UnsupportedHeader;
1651
16521640 const uri = switch (options.location) {
16531641 .url => |u| try Uri.parse(u),
16541642 .uri => |u| u,
16551643 };
16561644 var server_header_buffer: [16 * 1024]u8 = undefined;
16571645
1658 var req = try open(client, options.method, uri, options.headers, .{
1646 var req = try open(client, options.method, uri, .{
16591647 .server_header_buffer = options.server_header_buffer orelse &server_header_buffer,
16601648 .redirect_behavior = options.redirect_behavior orelse
16611649 if (options.payload == .none) @enumFromInt(3) else .unhandled,
1650 .headers = options.headers,
1651 .extra_headers = options.extra_headers,
1652 .privileged_headers = options.privileged_headers,
16621653 });
16631654 defer req.deinit();
16641655
......@@ -1690,10 +1681,8 @@ pub fn fetch(client: *Client, allocator: Allocator, options: FetchOptions) !Fetc
16901681
16911682 try req.wait();
16921683
1693 var res = FetchResult{
1684 var res: FetchResult = .{
16941685 .status = req.response.status,
1695 .headers = try req.response.headers.clone(allocator),
1696
16971686 .allocator = allocator,
16981687 .options = options,
16991688 };
lib/std/http/Headers.zig deleted-527
......@@ -1,527 +0,0 @@
1const std = @import("../std.zig");
2
3const Allocator = std.mem.Allocator;
4
5const testing = std.testing;
6const ascii = std.ascii;
7const assert = std.debug.assert;
8
9pub const HeaderList = std.ArrayListUnmanaged(Field);
10pub const HeaderIndexList = std.ArrayListUnmanaged(usize);
11pub const HeaderIndex = std.HashMapUnmanaged([]const u8, HeaderIndexList, CaseInsensitiveStringContext, std.hash_map.default_max_load_percentage);
12
13pub const CaseInsensitiveStringContext = struct {
14 pub fn hash(self: @This(), s: []const u8) u64 {
15 _ = self;
16 var buf: [64]u8 = undefined;
17 var i: usize = 0;
18
19 var h = std.hash.Wyhash.init(0);
20 while (i + 64 < s.len) : (i += 64) {
21 const ret = ascii.lowerString(buf[0..], s[i..][0..64]);
22 h.update(ret);
23 }
24
25 const left = @min(64, s.len - i);
26 const ret = ascii.lowerString(buf[0..], s[i..][0..left]);
27 h.update(ret);
28
29 return h.final();
30 }
31
32 pub fn eql(self: @This(), a: []const u8, b: []const u8) bool {
33 _ = self;
34 return ascii.eqlIgnoreCase(a, b);
35 }
36};
37
38/// A single HTTP header field.
39pub const Field = struct {
40 name: []const u8,
41 value: []const u8,
42
43 fn lessThan(ctx: void, a: Field, b: Field) bool {
44 _ = ctx;
45 if (a.name.ptr == b.name.ptr) return false;
46
47 return ascii.lessThanIgnoreCase(a.name, b.name);
48 }
49};
50
51/// A list of HTTP header fields.
52pub const Headers = struct {
53 allocator: Allocator,
54 list: HeaderList = .{},
55 index: HeaderIndex = .{},
56
57 /// When this is false, names and values will not be duplicated.
58 /// Use with caution.
59 owned: bool = true,
60
61 /// Initialize an empty list of headers.
62 pub fn init(allocator: Allocator) Headers {
63 return .{ .allocator = allocator };
64 }
65
66 /// Initialize a pre-populated list of headers from a list of fields.
67 pub fn initList(allocator: Allocator, list: []const Field) !Headers {
68 var new = Headers.init(allocator);
69
70 try new.list.ensureTotalCapacity(allocator, list.len);
71 try new.index.ensureTotalCapacity(allocator, @intCast(list.len));
72 for (list) |field| {
73 try new.append(field.name, field.value);
74 }
75
76 return new;
77 }
78
79 /// Deallocate all memory associated with the headers.
80 ///
81 /// If the `owned` field is false, this will not free the names and values of the headers.
82 pub fn deinit(headers: *Headers) void {
83 headers.deallocateIndexListsAndFields();
84 headers.index.deinit(headers.allocator);
85 headers.list.deinit(headers.allocator);
86
87 headers.* = undefined;
88 }
89
90 /// Appends a header to the list.
91 ///
92 /// If the `owned` field is true, both name and value will be copied.
93 pub fn append(headers: *Headers, name: []const u8, value: []const u8) !void {
94 try headers.appendOwned(.{ .unowned = name }, .{ .unowned = value });
95 }
96
97 pub const OwnedString = union(enum) {
98 /// A string allocated by the `allocator` field.
99 owned: []u8,
100 /// A string to be copied by the `allocator` field.
101 unowned: []const u8,
102 };
103
104 /// Appends a header to the list.
105 ///
106 /// If the `owned` field is true, `name` and `value` will be copied if unowned.
107 pub fn appendOwned(headers: *Headers, name: OwnedString, value: OwnedString) !void {
108 const n = headers.list.items.len;
109 try headers.list.ensureUnusedCapacity(headers.allocator, 1);
110
111 const owned_value = switch (value) {
112 .owned => |owned| owned,
113 .unowned => |unowned| if (headers.owned)
114 try headers.allocator.dupe(u8, unowned)
115 else
116 unowned,
117 };
118 errdefer if (value == .unowned and headers.owned) headers.allocator.free(owned_value);
119
120 var entry = Field{ .name = undefined, .value = owned_value };
121
122 if (headers.index.getEntry(switch (name) {
123 inline else => |string| string,
124 })) |kv| {
125 defer switch (name) {
126 .owned => |owned| headers.allocator.free(owned),
127 .unowned => {},
128 };
129
130 entry.name = kv.key_ptr.*;
131 try kv.value_ptr.append(headers.allocator, n);
132 } else {
133 const owned_name = switch (name) {
134 .owned => |owned| owned,
135 .unowned => |unowned| if (headers.owned)
136 try std.ascii.allocLowerString(headers.allocator, unowned)
137 else
138 unowned,
139 };
140 errdefer if (name == .unowned and headers.owned) headers.allocator.free(owned_name);
141
142 entry.name = owned_name;
143
144 var new_index = try HeaderIndexList.initCapacity(headers.allocator, 1);
145 errdefer new_index.deinit(headers.allocator);
146
147 new_index.appendAssumeCapacity(n);
148 try headers.index.put(headers.allocator, owned_name, new_index);
149 }
150
151 headers.list.appendAssumeCapacity(entry);
152 }
153
154 /// Returns true if this list of headers contains the given name.
155 pub fn contains(headers: Headers, name: []const u8) bool {
156 return headers.index.contains(name);
157 }
158
159 /// Removes all headers with the given name.
160 pub fn delete(headers: *Headers, name: []const u8) bool {
161 if (headers.index.fetchRemove(name)) |kv| {
162 var index = kv.value;
163
164 // iterate backwards
165 var i = index.items.len;
166 while (i > 0) {
167 i -= 1;
168 const data_index = index.items[i];
169 const removed = headers.list.orderedRemove(data_index);
170
171 assert(ascii.eqlIgnoreCase(removed.name, name)); // ensure the index hasn't been corrupted
172 if (headers.owned) headers.allocator.free(removed.value);
173 }
174
175 if (headers.owned) headers.allocator.free(kv.key);
176 index.deinit(headers.allocator);
177 headers.rebuildIndex();
178
179 return true;
180 } else {
181 return false;
182 }
183 }
184
185 /// Returns the index of the first occurrence of a header with the given name.
186 pub fn firstIndexOf(headers: Headers, name: []const u8) ?usize {
187 const index = headers.index.get(name) orelse return null;
188
189 return index.items[0];
190 }
191
192 /// Returns a list of indices containing headers with the given name.
193 pub fn getIndices(headers: Headers, name: []const u8) ?[]const usize {
194 const index = headers.index.get(name) orelse return null;
195
196 return index.items;
197 }
198
199 /// Returns the entry of the first occurrence of a header with the given name.
200 pub fn getFirstEntry(headers: Headers, name: []const u8) ?Field {
201 const first_index = headers.firstIndexOf(name) orelse return null;
202
203 return headers.list.items[first_index];
204 }
205
206 /// Returns a slice containing each header with the given name.
207 /// The caller owns the returned slice, but NOT the values in the slice.
208 pub fn getEntries(headers: Headers, allocator: Allocator, name: []const u8) !?[]const Field {
209 const indices = headers.getIndices(name) orelse return null;
210
211 const buf = try allocator.alloc(Field, indices.len);
212 for (indices, 0..) |idx, n| {
213 buf[n] = headers.list.items[idx];
214 }
215
216 return buf;
217 }
218
219 /// Returns the value in the entry of the first occurrence of a header with the given name.
220 pub fn getFirstValue(headers: Headers, name: []const u8) ?[]const u8 {
221 const first_index = headers.firstIndexOf(name) orelse return null;
222
223 return headers.list.items[first_index].value;
224 }
225
226 /// Returns a slice containing the value of each header with the given name.
227 /// The caller owns the returned slice, but NOT the values in the slice.
228 pub fn getValues(headers: Headers, allocator: Allocator, name: []const u8) !?[]const []const u8 {
229 const indices = headers.getIndices(name) orelse return null;
230
231 const buf = try allocator.alloc([]const u8, indices.len);
232 for (indices, 0..) |idx, n| {
233 buf[n] = headers.list.items[idx].value;
234 }
235
236 return buf;
237 }
238
239 fn rebuildIndex(headers: *Headers) void {
240 // clear out the indexes
241 var it = headers.index.iterator();
242 while (it.next()) |entry| {
243 entry.value_ptr.shrinkRetainingCapacity(0);
244 }
245
246 // fill up indexes again; we know capacity is fine from before
247 for (headers.list.items, 0..) |entry, i| {
248 headers.index.getEntry(entry.name).?.value_ptr.appendAssumeCapacity(i);
249 }
250 }
251
252 /// Sorts the headers in lexicographical order.
253 pub fn sort(headers: *Headers) void {
254 std.mem.sort(Field, headers.list.items, {}, Field.lessThan);
255 headers.rebuildIndex();
256 }
257
258 /// Writes the headers to the given stream.
259 pub fn format(
260 headers: Headers,
261 comptime fmt: []const u8,
262 options: std.fmt.FormatOptions,
263 out_stream: anytype,
264 ) !void {
265 _ = fmt;
266 _ = options;
267
268 for (headers.list.items) |entry| {
269 if (entry.value.len == 0) continue;
270
271 try out_stream.writeAll(entry.name);
272 try out_stream.writeAll(": ");
273 try out_stream.writeAll(entry.value);
274 try out_stream.writeAll("\r\n");
275 }
276 }
277
278 /// Writes all of the headers with the given name to the given stream, separated by commas.
279 ///
280 /// This is useful for headers like `Set-Cookie` which can have multiple values. RFC 9110, Section 5.2
281 pub fn formatCommaSeparated(
282 headers: Headers,
283 name: []const u8,
284 out_stream: anytype,
285 ) !void {
286 const indices = headers.getIndices(name) orelse return;
287
288 try out_stream.writeAll(name);
289 try out_stream.writeAll(": ");
290
291 for (indices, 0..) |idx, n| {
292 if (n != 0) try out_stream.writeAll(", ");
293 try out_stream.writeAll(headers.list.items[idx].value);
294 }
295
296 try out_stream.writeAll("\r\n");
297 }
298
299 /// Frees all `HeaderIndexList`s within `index`.
300 /// Frees names and values of all fields if they are owned.
301 fn deallocateIndexListsAndFields(headers: *Headers) void {
302 var it = headers.index.iterator();
303 while (it.next()) |entry| {
304 entry.value_ptr.deinit(headers.allocator);
305
306 if (headers.owned) headers.allocator.free(entry.key_ptr.*);
307 }
308
309 if (headers.owned) {
310 for (headers.list.items) |entry| {
311 headers.allocator.free(entry.value);
312 }
313 }
314 }
315
316 /// Clears and frees the underlying data structures.
317 /// Frees names and values if they are owned.
318 pub fn clearAndFree(headers: *Headers) void {
319 headers.deallocateIndexListsAndFields();
320 headers.index.clearAndFree(headers.allocator);
321 headers.list.clearAndFree(headers.allocator);
322 }
323
324 /// Clears the underlying data structures while retaining their capacities.
325 /// Frees names and values if they are owned.
326 pub fn clearRetainingCapacity(headers: *Headers) void {
327 headers.deallocateIndexListsAndFields();
328 headers.index.clearRetainingCapacity();
329 headers.list.clearRetainingCapacity();
330 }
331
332 /// Creates a copy of the headers using the provided allocator.
333 pub fn clone(headers: Headers, allocator: Allocator) !Headers {
334 var new = Headers.init(allocator);
335
336 try new.list.ensureTotalCapacity(allocator, headers.list.capacity);
337 try new.index.ensureTotalCapacity(allocator, headers.index.capacity());
338 for (headers.list.items) |field| {
339 try new.append(field.name, field.value);
340 }
341
342 return new;
343 }
344};
345
346test "Headers.append" {
347 var h = Headers{ .allocator = std.testing.allocator };
348 defer h.deinit();
349
350 try h.append("foo", "bar");
351 try h.append("hello", "world");
352
353 try testing.expect(h.contains("Foo"));
354 try testing.expect(!h.contains("Bar"));
355}
356
357test "Headers.delete" {
358 var h = Headers{ .allocator = std.testing.allocator };
359 defer h.deinit();
360
361 try h.append("foo", "bar");
362 try h.append("hello", "world");
363
364 try testing.expect(h.contains("Foo"));
365
366 _ = h.delete("Foo");
367
368 try testing.expect(!h.contains("foo"));
369}
370
371test "Headers consistency" {
372 var h = Headers{ .allocator = std.testing.allocator };
373 defer h.deinit();
374
375 try h.append("foo", "bar");
376 try h.append("hello", "world");
377 _ = h.delete("Foo");
378
379 try h.append("foo", "bar");
380 try h.append("bar", "world");
381 try h.append("foo", "baz");
382 try h.append("baz", "hello");
383
384 try testing.expectEqual(@as(?usize, 0), h.firstIndexOf("hello"));
385 try testing.expectEqual(@as(?usize, 1), h.firstIndexOf("foo"));
386 try testing.expectEqual(@as(?usize, 2), h.firstIndexOf("bar"));
387 try testing.expectEqual(@as(?usize, 4), h.firstIndexOf("baz"));
388 try testing.expectEqual(@as(?usize, null), h.firstIndexOf("pog"));
389
390 try testing.expectEqualSlices(usize, &[_]usize{0}, h.getIndices("hello").?);
391 try testing.expectEqualSlices(usize, &[_]usize{ 1, 3 }, h.getIndices("foo").?);
392 try testing.expectEqualSlices(usize, &[_]usize{2}, h.getIndices("bar").?);
393 try testing.expectEqualSlices(usize, &[_]usize{4}, h.getIndices("baz").?);
394 try testing.expectEqual(@as(?[]const usize, null), h.getIndices("pog"));
395
396 try testing.expectEqualStrings("world", h.getFirstEntry("hello").?.value);
397 try testing.expectEqualStrings("bar", h.getFirstEntry("foo").?.value);
398 try testing.expectEqualStrings("world", h.getFirstEntry("bar").?.value);
399 try testing.expectEqualStrings("hello", h.getFirstEntry("baz").?.value);
400
401 const hello_entries = (try h.getEntries(testing.allocator, "hello")).?;
402 defer testing.allocator.free(hello_entries);
403 try testing.expectEqualDeep(@as([]const Field, &[_]Field{
404 .{ .name = "hello", .value = "world" },
405 }), hello_entries);
406
407 const foo_entries = (try h.getEntries(testing.allocator, "foo")).?;
408 defer testing.allocator.free(foo_entries);
409 try testing.expectEqualDeep(@as([]const Field, &[_]Field{
410 .{ .name = "foo", .value = "bar" },
411 .{ .name = "foo", .value = "baz" },
412 }), foo_entries);
413
414 const bar_entries = (try h.getEntries(testing.allocator, "bar")).?;
415 defer testing.allocator.free(bar_entries);
416 try testing.expectEqualDeep(@as([]const Field, &[_]Field{
417 .{ .name = "bar", .value = "world" },
418 }), bar_entries);
419
420 const baz_entries = (try h.getEntries(testing.allocator, "baz")).?;
421 defer testing.allocator.free(baz_entries);
422 try testing.expectEqualDeep(@as([]const Field, &[_]Field{
423 .{ .name = "baz", .value = "hello" },
424 }), baz_entries);
425
426 const pog_entries = (try h.getEntries(testing.allocator, "pog"));
427 try testing.expectEqual(@as(?[]const Field, null), pog_entries);
428
429 try testing.expectEqualStrings("world", h.getFirstValue("hello").?);
430 try testing.expectEqualStrings("bar", h.getFirstValue("foo").?);
431 try testing.expectEqualStrings("world", h.getFirstValue("bar").?);
432 try testing.expectEqualStrings("hello", h.getFirstValue("baz").?);
433 try testing.expectEqual(@as(?[]const u8, null), h.getFirstValue("pog"));
434
435 const hello_values = (try h.getValues(testing.allocator, "hello")).?;
436 defer testing.allocator.free(hello_values);
437 try testing.expectEqualDeep(@as([]const []const u8, &[_][]const u8{"world"}), hello_values);
438
439 const foo_values = (try h.getValues(testing.allocator, "foo")).?;
440 defer testing.allocator.free(foo_values);
441 try testing.expectEqualDeep(@as([]const []const u8, &[_][]const u8{ "bar", "baz" }), foo_values);
442
443 const bar_values = (try h.getValues(testing.allocator, "bar")).?;
444 defer testing.allocator.free(bar_values);
445 try testing.expectEqualDeep(@as([]const []const u8, &[_][]const u8{"world"}), bar_values);
446
447 const baz_values = (try h.getValues(testing.allocator, "baz")).?;
448 defer testing.allocator.free(baz_values);
449 try testing.expectEqualDeep(@as([]const []const u8, &[_][]const u8{"hello"}), baz_values);
450
451 const pog_values = (try h.getValues(testing.allocator, "pog"));
452 try testing.expectEqual(@as(?[]const []const u8, null), pog_values);
453
454 h.sort();
455
456 try testing.expectEqualSlices(usize, &[_]usize{0}, h.getIndices("bar").?);
457 try testing.expectEqualSlices(usize, &[_]usize{1}, h.getIndices("baz").?);
458 try testing.expectEqualSlices(usize, &[_]usize{ 2, 3 }, h.getIndices("foo").?);
459 try testing.expectEqualSlices(usize, &[_]usize{4}, h.getIndices("hello").?);
460
461 const formatted_values = try std.fmt.allocPrint(testing.allocator, "{}", .{h});
462 defer testing.allocator.free(formatted_values);
463
464 try testing.expectEqualStrings("bar: world\r\nbaz: hello\r\nfoo: bar\r\nfoo: baz\r\nhello: world\r\n", formatted_values);
465
466 var buf: [128]u8 = undefined;
467 var fbs = std.io.fixedBufferStream(&buf);
468 const writer = fbs.writer();
469
470 try h.formatCommaSeparated("foo", writer);
471 try testing.expectEqualStrings("foo: bar, baz\r\n", fbs.getWritten());
472}
473
474test "Headers.clearRetainingCapacity and clearAndFree" {
475 var h = Headers.init(std.testing.allocator);
476 defer h.deinit();
477
478 h.clearRetainingCapacity();
479
480 try h.append("foo", "bar");
481 try h.append("bar", "world");
482 try h.append("foo", "baz");
483 try h.append("baz", "hello");
484 try testing.expectEqual(@as(usize, 4), h.list.items.len);
485 try testing.expectEqual(@as(usize, 3), h.index.count());
486 const list_capacity = h.list.capacity;
487 const index_capacity = h.index.capacity();
488
489 h.clearRetainingCapacity();
490 try testing.expectEqual(@as(usize, 0), h.list.items.len);
491 try testing.expectEqual(@as(usize, 0), h.index.count());
492 try testing.expectEqual(list_capacity, h.list.capacity);
493 try testing.expectEqual(index_capacity, h.index.capacity());
494
495 try h.append("foo", "bar");
496 try h.append("bar", "world");
497 try h.append("foo", "baz");
498 try h.append("baz", "hello");
499 try testing.expectEqual(@as(usize, 4), h.list.items.len);
500 try testing.expectEqual(@as(usize, 3), h.index.count());
501 // Capacity should still be the same since we shouldn't have needed to grow
502 // when adding back the same fields
503 try testing.expectEqual(list_capacity, h.list.capacity);
504 try testing.expectEqual(index_capacity, h.index.capacity());
505
506 h.clearAndFree();
507 try testing.expectEqual(@as(usize, 0), h.list.items.len);
508 try testing.expectEqual(@as(usize, 0), h.index.count());
509 try testing.expectEqual(@as(usize, 0), h.list.capacity);
510 try testing.expectEqual(@as(usize, 0), h.index.capacity());
511}
512
513test "Headers.initList" {
514 var h = try Headers.initList(std.testing.allocator, &.{
515 .{ .name = "Accept-Encoding", .value = "gzip" },
516 .{ .name = "Authorization", .value = "it's over 9000!" },
517 });
518 defer h.deinit();
519
520 const encoding_values = (try h.getValues(testing.allocator, "Accept-Encoding")).?;
521 defer testing.allocator.free(encoding_values);
522 try testing.expectEqualDeep(@as([]const []const u8, &[_][]const u8{"gzip"}), encoding_values);
523
524 const authorization_values = (try h.getValues(testing.allocator, "Authorization")).?;
525 defer testing.allocator.free(authorization_values);
526 try testing.expectEqualDeep(@as([]const []const u8, &[_][]const u8{"it's over 9000!"}), authorization_values);
527}
lib/std/http/Server.zig+89-139
......@@ -162,11 +162,13 @@ pub const ResponseTransfer = union(enum) {
162162pub const Compression = union(enum) {
163163 pub const DeflateDecompressor = std.compress.zlib.Decompressor(Response.TransferReader);
164164 pub const GzipDecompressor = std.compress.gzip.Decompressor(Response.TransferReader);
165 pub const ZstdDecompressor = std.compress.zstd.DecompressStream(Response.TransferReader, .{});
165 // https://github.com/ziglang/zig/issues/18937
166 //pub const ZstdDecompressor = std.compress.zstd.DecompressStream(Response.TransferReader, .{});
166167
167168 deflate: DeflateDecompressor,
168169 gzip: GzipDecompressor,
169 zstd: ZstdDecompressor,
170 // https://github.com/ziglang/zig/issues/18937
171 //zstd: ZstdDecompressor,
170172 none: void,
171173};
172174
......@@ -179,7 +181,7 @@ pub const Request = struct {
179181 HttpTransferEncodingUnsupported,
180182 HttpConnectionHeaderUnsupported,
181183 InvalidContentLength,
182 CompressionNotSupported,
184 CompressionUnsupported,
183185 };
184186
185187 pub fn parse(req: *Request, bytes: []const u8) ParseError!void {
......@@ -189,13 +191,15 @@ pub const Request = struct {
189191 if (first_line.len < 10)
190192 return error.HttpHeadersInvalid;
191193
192 const method_end = mem.indexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid;
194 const method_end = mem.indexOfScalar(u8, first_line, ' ') orelse
195 return error.HttpHeadersInvalid;
193196 if (method_end > 24) return error.HttpHeadersInvalid;
194197
195198 const method_str = first_line[0..method_end];
196199 const method: http.Method = @enumFromInt(http.Method.parse(method_str));
197200
198 const version_start = mem.lastIndexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid;
201 const version_start = mem.lastIndexOfScalar(u8, first_line, ' ') orelse
202 return error.HttpHeadersInvalid;
199203 if (version_start == method_end) return error.HttpHeadersInvalid;
200204
201205 const version_str = first_line[version_start + 1 ..];
......@@ -223,11 +227,26 @@ pub const Request = struct {
223227 const header_name = line_it.next() orelse return error.HttpHeadersInvalid;
224228 const header_value = line_it.rest();
225229
226 try req.headers.append(header_name, header_value);
227
228 if (std.ascii.eqlIgnoreCase(header_name, "content-length")) {
230 if (std.ascii.eqlIgnoreCase(header_name, "connection")) {
231 req.keep_alive = !std.ascii.eqlIgnoreCase(header_value, "close");
232 } else if (std.ascii.eqlIgnoreCase(header_name, "expect")) {
233 req.expect = header_value;
234 } else if (std.ascii.eqlIgnoreCase(header_name, "content-type")) {
235 req.content_type = header_value;
236 } else if (std.ascii.eqlIgnoreCase(header_name, "content-length")) {
229237 if (req.content_length != null) return error.HttpHeadersInvalid;
230 req.content_length = std.fmt.parseInt(u64, header_value, 10) catch return error.InvalidContentLength;
238 req.content_length = std.fmt.parseInt(u64, header_value, 10) catch
239 return error.InvalidContentLength;
240 } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) {
241 if (req.transfer_compression != .identity) return error.HttpHeadersInvalid;
242
243 const trimmed = mem.trim(u8, header_value, " ");
244
245 if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
246 req.transfer_compression = ce;
247 } else {
248 return error.HttpTransferEncodingUnsupported;
249 }
231250 } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) {
232251 // Transfer-Encoding: second, first
233252 // Transfer-Encoding: deflate, chunked
......@@ -238,7 +257,8 @@ pub const Request = struct {
238257
239258 var next: ?[]const u8 = first;
240259 if (std.meta.stringToEnum(http.TransferEncoding, trimmed_first)) |transfer| {
241 if (req.transfer_encoding != .none) return error.HttpHeadersInvalid; // we already have a transfer encoding
260 if (req.transfer_encoding != .none)
261 return error.HttpHeadersInvalid; // we already have a transfer encoding
242262 req.transfer_encoding = transfer;
243263
244264 next = iter.next();
......@@ -248,7 +268,8 @@ pub const Request = struct {
248268 const trimmed_second = mem.trim(u8, second, " ");
249269
250270 if (std.meta.stringToEnum(http.ContentEncoding, trimmed_second)) |transfer| {
251 if (req.transfer_compression != .identity) return error.HttpHeadersInvalid; // double compression is not supported
271 if (req.transfer_compression != .identity)
272 return error.HttpHeadersInvalid; // double compression is not supported
252273 req.transfer_compression = transfer;
253274 } else {
254275 return error.HttpTransferEncodingUnsupported;
......@@ -256,45 +277,23 @@ pub const Request = struct {
256277 }
257278
258279 if (iter.next()) |_| return error.HttpTransferEncodingUnsupported;
259 } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) {
260 if (req.transfer_compression != .identity) return error.HttpHeadersInvalid;
261
262 const trimmed = mem.trim(u8, header_value, " ");
263
264 if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
265 req.transfer_compression = ce;
266 } else {
267 return error.HttpTransferEncodingUnsupported;
268 }
269280 }
270281 }
271282 }
272283
273284 inline fn int64(array: *const [8]u8) u64 {
274 return @as(u64, @bitCast(array.*));
285 return @bitCast(array.*);
275286 }
276287
277 /// The HTTP request method.
278288 method: http.Method,
279
280 /// The HTTP request target.
281289 target: []const u8,
282
283 /// The HTTP version of this request.
284290 version: http.Version,
285
286 /// The length of the request body, if known.
291 expect: ?[]const u8 = null,
292 content_type: ?[]const u8 = null,
287293 content_length: ?u64 = null,
288
289 /// The transfer encoding of the request body, or .none if not present.
290294 transfer_encoding: http.TransferEncoding = .none,
291
292 /// The compression of the request body, or .identity (no compression) if not present.
293295 transfer_compression: http.ContentEncoding = .identity,
294
295 /// The list of HTTP request headers
296 headers: http.Headers,
297
296 keep_alive: bool = false,
298297 parser: proto.HeadersParser,
299298 compression: Compression = .none,
300299};
......@@ -311,11 +310,8 @@ pub const Response = struct {
311310 version: http.Version = .@"HTTP/1.1",
312311 status: http.Status = .ok,
313312 reason: ?[]const u8 = null,
314
315 transfer_encoding: ResponseTransfer = .none,
316
317 /// The allocator responsible for allocating memory for this response.
318 allocator: Allocator,
313 transfer_encoding: ResponseTransfer,
314 keep_alive: bool,
319315
320316 /// The peer's address
321317 address: net.Address,
......@@ -323,8 +319,8 @@ pub const Response = struct {
323319 /// The underlying connection for this response.
324320 connection: Connection,
325321
326 /// The HTTP response headers
327 headers: http.Headers,
322 /// Externally-owned; must outlive the Response.
323 extra_headers: []const http.Header = &.{},
328324
329325 /// The HTTP request that this response is responding to.
330326 ///
......@@ -333,7 +329,7 @@ pub const Response = struct {
333329
334330 state: State = .first,
335331
336 const State = enum {
332 pub const State = enum {
337333 first,
338334 start,
339335 waited,
......@@ -344,14 +340,12 @@ pub const Response = struct {
344340 /// Free all resources associated with this response.
345341 pub fn deinit(res: *Response) void {
346342 res.connection.close();
347
348 res.headers.deinit();
349 res.request.headers.deinit();
350343 }
351344
352345 pub const ResetState = enum { reset, closing };
353346
354 /// Reset this response to its initial state. This must be called before handling a second request on the same connection.
347 /// Reset this response to its initial state. This must be called before
348 /// handling a second request on the same connection.
355349 pub fn reset(res: *Response) ResetState {
356350 if (res.state == .first) {
357351 res.state = .start;
......@@ -364,27 +358,11 @@ pub const Response = struct {
364358 return .closing;
365359 }
366360
367 // A connection is only keep-alive if the Connection header is present and it's value is not "close".
368 // The server and client must both agree
361 // A connection is only keep-alive if the Connection header is present
362 // and its value is not "close". The server and client must both agree.
369363 //
370364 // send() defaults to using keep-alive if the client requests it.
371 const res_connection = res.headers.getFirstValue("connection");
372 const res_keepalive = res_connection != null and !std.ascii.eqlIgnoreCase("close", res_connection.?);
373
374 const req_connection = res.request.headers.getFirstValue("connection");
375 const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?);
376 if (req_keepalive and (res_keepalive or res_connection == null)) {
377 res.connection.closing = false;
378 } else {
379 res.connection.closing = true;
380 }
381
382 switch (res.request.compression) {
383 .none => {},
384 .deflate => {},
385 .gzip => {},
386 .zstd => |*zstd| zstd.deinit(),
387 }
365 res.connection.closing = !res.keep_alive or !res.request.keep_alive;
388366
389367 res.state = .start;
390368 res.version = .@"HTTP/1.1";
......@@ -393,27 +371,22 @@ pub const Response = struct {
393371
394372 res.transfer_encoding = .none;
395373
396 res.headers.clearRetainingCapacity();
397
398 res.request.headers.clearAndFree(); // FIXME: figure out why `clearRetainingCapacity` causes a leak in hash_map here
399374 res.request.parser.reset();
400375
401 res.request = Request{
376 res.request = .{
402377 .version = undefined,
403378 .method = undefined,
404379 .target = undefined,
405 .headers = res.request.headers,
406380 .parser = res.request.parser,
407381 };
408382
409 if (res.connection.closing) {
410 return .closing;
411 } else {
412 return .reset;
413 }
383 return if (res.connection.closing) .closing else .reset;
414384 }
415385
416 pub const SendError = Connection.WriteError || error{ UnsupportedTransferEncoding, InvalidContentLength };
386 pub const SendError = Connection.WriteError || error{
387 UnsupportedTransferEncoding,
388 InvalidContentLength,
389 };
417390
418391 /// Send the HTTP response headers to the client.
419392 pub fn send(res: *Response) SendError!void {
......@@ -439,44 +412,21 @@ pub const Response = struct {
439412 if (res.status == .@"continue") {
440413 res.state = .waited; // we still need to send another request after this
441414 } else {
442 if (!res.headers.contains("connection")) {
443 const req_connection = res.request.headers.getFirstValue("connection");
444 const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?);
445
446 if (req_keepalive) {
447 try w.writeAll("Connection: keep-alive\r\n");
448 } else {
449 try w.writeAll("Connection: close\r\n");
450 }
415 if (res.keep_alive and res.request.keep_alive) {
416 try w.writeAll("connection: keep-alive\r\n");
417 } else {
418 try w.writeAll("connection: close\r\n");
451419 }
452420
453 const has_transfer_encoding = res.headers.contains("transfer-encoding");
454 const has_content_length = res.headers.contains("content-length");
455
456 if (!has_transfer_encoding and !has_content_length) {
457 switch (res.transfer_encoding) {
458 .chunked => try w.writeAll("Transfer-Encoding: chunked\r\n"),
459 .content_length => |content_length| try w.print("Content-Length: {d}\r\n", .{content_length}),
460 .none => {},
461 }
462 } else {
463 if (has_content_length) {
464 const content_length = std.fmt.parseInt(u64, res.headers.getFirstValue("content-length").?, 10) catch return error.InvalidContentLength;
465
466 res.transfer_encoding = .{ .content_length = content_length };
467 } else if (has_transfer_encoding) {
468 const transfer_encoding = res.headers.getFirstValue("transfer-encoding").?;
469 if (std.mem.eql(u8, transfer_encoding, "chunked")) {
470 res.transfer_encoding = .chunked;
471 } else {
472 return error.UnsupportedTransferEncoding;
473 }
474 } else {
475 res.transfer_encoding = .none;
476 }
421 switch (res.transfer_encoding) {
422 .chunked => try w.writeAll("transfer-encoding: chunked\r\n"),
423 .content_length => |content_length| try w.print("content-length: {d}\r\n", .{content_length}),
424 .none => {},
477425 }
478426
479 try w.print("{}", .{res.headers});
427 for (res.extra_headers) |header| {
428 try w.print("{s}: {s}\r\n", .{ header.name, header.value });
429 }
480430 }
481431
482432 if (res.request.method == .HEAD) {
......@@ -511,7 +461,7 @@ pub const Response = struct {
511461
512462 pub const WaitError = Connection.ReadError ||
513463 proto.HeadersParser.CheckCompleteHeadError || Request.ParseError ||
514 error{ CompressionInitializationFailed, CompressionNotSupported };
464 error{CompressionUnsupported};
515465
516466 /// Wait for the client to send a complete request head.
517467 ///
......@@ -545,37 +495,37 @@ pub const Response = struct {
545495 if (res.request.parser.state.isContent()) break;
546496 }
547497
548 res.request.headers = .{ .allocator = res.allocator, .owned = true };
549498 try res.request.parse(res.request.parser.get());
550499
551 if (res.request.transfer_encoding != .none) {
552 switch (res.request.transfer_encoding) {
553 .none => unreachable,
554 .chunked => {
555 res.request.parser.next_chunk_length = 0;
556 res.request.parser.state = .chunk_head_size;
557 },
558 }
559 } else if (res.request.content_length) |cl| {
560 res.request.parser.next_chunk_length = cl;
500 switch (res.request.transfer_encoding) {
501 .none => {
502 if (res.request.content_length) |len| {
503 res.request.parser.next_chunk_length = len;
561504
562 if (cl == 0) res.request.parser.state = .complete;
563 } else {
564 res.request.parser.state = .complete;
505 if (len == 0) res.request.parser.state = .complete;
506 } else {
507 res.request.parser.state = .complete;
508 }
509 },
510 .chunked => {
511 res.request.parser.next_chunk_length = 0;
512 res.request.parser.state = .chunk_head_size;
513 },
565514 }
566515
567516 if (res.request.parser.state != .complete) {
568517 switch (res.request.transfer_compression) {
569518 .identity => res.request.compression = .none,
570 .compress, .@"x-compress" => return error.CompressionNotSupported,
519 .compress, .@"x-compress" => return error.CompressionUnsupported,
571520 .deflate => res.request.compression = .{
572521 .deflate = std.compress.zlib.decompressor(res.transferReader()),
573522 },
574523 .gzip, .@"x-gzip" => res.request.compression = .{
575524 .gzip = std.compress.gzip.decompressor(res.transferReader()),
576525 },
577 .zstd => res.request.compression = .{
578 .zstd = std.compress.zstd.decompressStream(res.allocator, res.transferReader()),
526 .zstd => {
527 // https://github.com/ziglang/zig/issues/18937
528 return error.CompressionUnsupported;
579529 },
580530 }
581531 }
......@@ -599,7 +549,8 @@ pub const Response = struct {
599549 const out_index = switch (res.request.compression) {
600550 .deflate => |*deflate| deflate.read(buffer) catch return error.DecompressionFailure,
601551 .gzip => |*gzip| gzip.read(buffer) catch return error.DecompressionFailure,
602 .zstd => |*zstd| zstd.read(buffer) catch return error.DecompressionFailure,
552 // https://github.com/ziglang/zig/issues/18937
553 //.zstd => |*zstd| zstd.read(buffer) catch return error.DecompressionFailure,
603554 else => try res.transferRead(buffer),
604555 };
605556
......@@ -614,8 +565,6 @@ pub const Response = struct {
614565 }
615566
616567 if (has_trail) {
617 res.request.headers = http.Headers{ .allocator = res.allocator, .owned = false };
618
619568 // The response headers before the trailers are already
620569 // guaranteed to be valid, so they will always be parsed again
621570 // and cannot return an error.
......@@ -736,18 +685,17 @@ pub fn accept(server: *Server, options: AcceptOptions) AcceptError!Response {
736685 const in = try server.socket.accept();
737686
738687 return .{
739 .allocator = options.allocator,
688 .transfer_encoding = .none,
689 .keep_alive = true,
740690 .address = in.address,
741691 .connection = .{
742692 .stream = in.stream,
743693 .protocol = .plain,
744694 },
745 .headers = .{ .allocator = options.allocator },
746695 .request = .{
747696 .version = undefined,
748697 .method = undefined,
749698 .target = undefined,
750 .headers = .{ .allocator = options.allocator, .owned = false },
751699 .parser = proto.HeadersParser.init(options.client_header_buffer),
752700 },
753701 };
......@@ -793,8 +741,10 @@ test "HTTP server handles a chunked transfer coding request" {
793741
794742 const server_body: []const u8 = "message from server!\n";
795743 res.transfer_encoding = .{ .content_length = server_body.len };
796 try res.headers.append("content-type", "text/plain");
797 try res.headers.append("connection", "close");
744 res.extra_headers = &.{
745 .{ .name = "content-type", .value = "text/plain" },
746 };
747 res.keep_alive = false;
798748 try res.send();
799749
800750 var buf: [128]u8 = undefined;
src/Package/Fetch.zig+3-6
......@@ -898,10 +898,7 @@ fn initResource(f: *Fetch, uri: std.Uri, server_header_buffer: []u8) RunError!Re
898898 if (ascii.eqlIgnoreCase(uri.scheme, "http") or
899899 ascii.eqlIgnoreCase(uri.scheme, "https"))
900900 {
901 var h: std.http.Headers = .{ .allocator = gpa };
902 defer h.deinit();
903
904 var req = http_client.open(.GET, uri, h, .{
901 var req = http_client.open(.GET, uri, .{
905902 .server_header_buffer = server_header_buffer,
906903 }) catch |err| {
907904 return f.fail(f.location_tok, try eb.printString(
......@@ -1043,7 +1040,7 @@ fn unpackResource(
10431040
10441041 .http_request => |req| ft: {
10451042 // Content-Type takes first precedence.
1046 const content_type = req.response.headers.getFirstValue("Content-Type") orelse
1043 const content_type = req.response.content_type orelse
10471044 return f.fail(f.location_tok, try eb.addString("missing 'Content-Type' header"));
10481045
10491046 // Extract the MIME type, ignoring charset and boundary directives
......@@ -1076,7 +1073,7 @@ fn unpackResource(
10761073 }
10771074
10781075 // Next, the filename from 'content-disposition: attachment' takes precedence.
1079 if (req.response.headers.getFirstValue("Content-Disposition")) |cd_header| {
1076 if (req.response.content_disposition) |cd_header| {
10801077 break :ft FileType.fromContentDisposition(cd_header) orelse {
10811078 return f.fail(f.location_tok, try eb.printString(
10821079 "unsupported Content-Disposition header value: '{s}' for Content-Type=application/octet-stream",
src/Package/Fetch/git.zig+23-21
......@@ -530,13 +530,12 @@ pub const Session = struct {
530530 info_refs_uri.query = "service=git-upload-pack";
531531 info_refs_uri.fragment = null;
532532
533 var headers = std.http.Headers.init(allocator);
534 defer headers.deinit();
535 try headers.append("Git-Protocol", "version=2");
536
537 var request = try session.transport.open(.GET, info_refs_uri, headers, .{
538 .max_redirects = 3,
533 var request = try session.transport.open(.GET, info_refs_uri, .{
534 .redirect_behavior = @enumFromInt(3),
539535 .server_header_buffer = http_headers_buffer,
536 .extra_headers = &.{
537 .{ .name = "Git-Protocol", .value = "version=2" },
538 },
540539 });
541540 errdefer request.deinit();
542541 try request.send(.{});
......@@ -544,7 +543,12 @@ pub const Session = struct {
544543
545544 try request.wait();
546545 if (request.response.status != .ok) return error.ProtocolError;
547 if (request.redirects_left < 3) {
546 // Pretty sure this is dead code - in order for a redirect to occur, the status
547 // code would need to be in the 300s and then it would not be "OK" which is checked
548 // on the line above.
549 var runtime_false = false;
550 _ = &runtime_false;
551 if (runtime_false) {
548552 if (!mem.endsWith(u8, request.uri.path, "/info/refs")) return error.UnparseableRedirect;
549553 var new_uri = request.uri;
550554 new_uri.path = new_uri.path[0 .. new_uri.path.len - "/info/refs".len];
......@@ -634,11 +638,6 @@ pub const Session = struct {
634638 upload_pack_uri.query = null;
635639 upload_pack_uri.fragment = null;
636640
637 var headers = std.http.Headers.init(allocator);
638 defer headers.deinit();
639 try headers.append("Content-Type", "application/x-git-upload-pack-request");
640 try headers.append("Git-Protocol", "version=2");
641
642641 var body = std.ArrayListUnmanaged(u8){};
643642 defer body.deinit(allocator);
644643 const body_writer = body.writer(allocator);
......@@ -660,9 +659,13 @@ pub const Session = struct {
660659 }
661660 try Packet.write(.flush, body_writer);
662661
663 var request = try session.transport.open(.POST, upload_pack_uri, headers, .{
664 .handle_redirects = false,
662 var request = try session.transport.open(.POST, upload_pack_uri, .{
663 .redirect_behavior = .unhandled,
665664 .server_header_buffer = options.server_header_buffer,
665 .extra_headers = &.{
666 .{ .name = "Content-Type", .value = "application/x-git-upload-pack-request" },
667 .{ .name = "Git-Protocol", .value = "version=2" },
668 },
666669 });
667670 errdefer request.deinit();
668671 request.transfer_encoding = .{ .content_length = body.items.len };
......@@ -738,11 +741,6 @@ pub const Session = struct {
738741 upload_pack_uri.query = null;
739742 upload_pack_uri.fragment = null;
740743
741 var headers = std.http.Headers.init(allocator);
742 defer headers.deinit();
743 try headers.append("Content-Type", "application/x-git-upload-pack-request");
744 try headers.append("Git-Protocol", "version=2");
745
746744 var body = std.ArrayListUnmanaged(u8){};
747745 defer body.deinit(allocator);
748746 const body_writer = body.writer(allocator);
......@@ -766,9 +764,13 @@ pub const Session = struct {
766764 try Packet.write(.{ .data = "done\n" }, body_writer);
767765 try Packet.write(.flush, body_writer);
768766
769 var request = try session.transport.open(.POST, upload_pack_uri, headers, .{
770 .handle_redirects = false,
767 var request = try session.transport.open(.POST, upload_pack_uri, .{
768 .redirect_behavior = .not_allowed,
771769 .server_header_buffer = http_headers_buffer,
770 .extra_headers = &.{
771 .{ .name = "Content-Type", .value = "application/x-git-upload-pack-request" },
772 .{ .name = "Git-Protocol", .value = "version=2" },
773 },
772774 });
773775 errdefer request.deinit();
774776 request.transfer_encoding = .{ .content_length = body.items.len };
src/main.zig+2-2
......@@ -5486,7 +5486,7 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
54865486 job_queue.read_only = true;
54875487 cleanup_build_dir = job_queue.global_cache.handle;
54885488 } else {
5489 try http_client.loadDefaultProxies();
5489 try http_client.initDefaultProxies(arena);
54905490 }
54915491
54925492 try job_queue.all_fetches.ensureUnusedCapacity(gpa, 1);
......@@ -7442,7 +7442,7 @@ fn cmdFetch(
74427442 var http_client: std.http.Client = .{ .allocator = gpa };
74437443 defer http_client.deinit();
74447444
7445 try http_client.loadDefaultProxies();
7445 try http_client.initDefaultProxies(arena);
74467446
74477447 var progress: std.Progress = .{ .dont_print_on_dumb = true };
74487448 const root_prog_node = progress.start("Fetch", 0);
test/standalone/http.zig+79-109
......@@ -26,8 +26,8 @@ fn handleRequest(res: *Server.Response) !void {
2626
2727 log.info("{} {s} {s}", .{ res.request.method, @tagName(res.request.version), res.request.target });
2828
29 if (res.request.headers.contains("expect")) {
30 if (mem.eql(u8, res.request.headers.getFirstValue("expect").?, "100-continue")) {
29 if (res.request.expect) |expect| {
30 if (mem.eql(u8, expect, "100-continue")) {
3131 res.status = .@"continue";
3232 try res.send();
3333 res.status = .ok;
......@@ -41,8 +41,8 @@ fn handleRequest(res: *Server.Response) !void {
4141 const body = try res.reader().readAllAlloc(salloc, 8192);
4242 defer salloc.free(body);
4343
44 if (res.request.headers.contains("connection")) {
45 try res.headers.append("connection", "keep-alive");
44 if (res.request.keep_alive) {
45 res.keep_alive = true;
4646 }
4747
4848 if (mem.startsWith(u8, res.request.target, "/get")) {
......@@ -52,7 +52,9 @@ fn handleRequest(res: *Server.Response) !void {
5252 res.transfer_encoding = .{ .content_length = 14 };
5353 }
5454
55 try res.headers.append("content-type", "text/plain");
55 res.extra_headers = &.{
56 .{ .name = "content-type", .value = "text/plain" },
57 };
5658
5759 try res.send();
5860 if (res.request.method != .HEAD) {
......@@ -82,14 +84,14 @@ fn handleRequest(res: *Server.Response) !void {
8284 try res.finish();
8385 } else if (mem.startsWith(u8, res.request.target, "/echo-content")) {
8486 try testing.expectEqualStrings("Hello, World!\n", body);
85 try testing.expectEqualStrings("text/plain", res.request.headers.getFirstValue("content-type").?);
86
87 if (res.request.headers.contains("transfer-encoding")) {
88 try testing.expectEqualStrings("chunked", res.request.headers.getFirstValue("transfer-encoding").?);
89 res.transfer_encoding = .chunked;
90 } else {
91 res.transfer_encoding = .{ .content_length = 14 };
92 try testing.expectEqualStrings("14", res.request.headers.getFirstValue("content-length").?);
87 try testing.expectEqualStrings("text/plain", res.request.content_type.?);
88
89 switch (res.request.transfer_encoding) {
90 .chunked => res.transfer_encoding = .chunked,
91 .none => {
92 res.transfer_encoding = .{ .content_length = 14 };
93 try testing.expectEqual(14, res.request.content_length.?);
94 },
9395 }
9496
9597 try res.send();
......@@ -108,7 +110,9 @@ fn handleRequest(res: *Server.Response) !void {
108110 res.transfer_encoding = .chunked;
109111
110112 res.status = .found;
111 try res.headers.append("location", "../../get");
113 res.extra_headers = &.{
114 .{ .name = "location", .value = "../../get" },
115 };
112116
113117 try res.send();
114118 try res.writeAll("Hello, ");
......@@ -118,7 +122,9 @@ fn handleRequest(res: *Server.Response) !void {
118122 res.transfer_encoding = .chunked;
119123
120124 res.status = .found;
121 try res.headers.append("location", "/redirect/1");
125 res.extra_headers = &.{
126 .{ .name = "location", .value = "/redirect/1" },
127 };
122128
123129 try res.send();
124130 try res.writeAll("Hello, ");
......@@ -131,7 +137,9 @@ fn handleRequest(res: *Server.Response) !void {
131137 defer salloc.free(location);
132138
133139 res.status = .found;
134 try res.headers.append("location", location);
140 res.extra_headers = &.{
141 .{ .name = "location", .value = location },
142 };
135143
136144 try res.send();
137145 try res.writeAll("Hello, ");
......@@ -141,7 +149,9 @@ fn handleRequest(res: *Server.Response) !void {
141149 res.transfer_encoding = .chunked;
142150
143151 res.status = .found;
144 try res.headers.append("location", "/redirect/3");
152 res.extra_headers = &.{
153 .{ .name = "location", .value = "/redirect/3" },
154 };
145155
146156 try res.send();
147157 try res.writeAll("Hello, ");
......@@ -153,7 +163,9 @@ fn handleRequest(res: *Server.Response) !void {
153163 defer salloc.free(location);
154164
155165 res.status = .found;
156 try res.headers.append("location", location);
166 res.extra_headers = &.{
167 .{ .name = "location", .value = location },
168 };
157169 try res.send();
158170 try res.finish();
159171 } else {
......@@ -234,19 +246,20 @@ pub fn main() !void {
234246 errdefer client.deinit();
235247 // defer client.deinit(); handled below
236248
237 try client.loadDefaultProxies();
249 var arena_instance = std.heap.ArenaAllocator.init(calloc);
250 defer arena_instance.deinit();
251 const arena = arena_instance.allocator();
238252
239 { // read content-length response
240 var h = http.Headers{ .allocator = calloc };
241 defer h.deinit();
253 try client.initDefaultProxies(arena);
242254
255 { // read content-length response
243256 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get", .{port});
244257 defer calloc.free(location);
245258 const uri = try std.Uri.parse(location);
246259
247260 log.info("{s}", .{location});
248261 var server_header_buffer: [1024]u8 = undefined;
249 var req = try client.open(.GET, uri, h, .{
262 var req = try client.open(.GET, uri, .{
250263 .server_header_buffer = &server_header_buffer,
251264 });
252265 defer req.deinit();
......@@ -258,23 +271,20 @@ pub fn main() !void {
258271 defer calloc.free(body);
259272
260273 try testing.expectEqualStrings("Hello, World!\n", body);
261 try testing.expectEqualStrings("text/plain", req.response.headers.getFirstValue("content-type").?);
274 try testing.expectEqualStrings("text/plain", req.response.content_type.?);
262275 }
263276
264277 // connection has been kept alive
265278 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
266279
267280 { // read large content-length response
268 var h = http.Headers{ .allocator = calloc };
269 defer h.deinit();
270
271281 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/large", .{port});
272282 defer calloc.free(location);
273283 const uri = try std.Uri.parse(location);
274284
275285 log.info("{s}", .{location});
276286 var server_header_buffer: [1024]u8 = undefined;
277 var req = try client.open(.GET, uri, h, .{
287 var req = try client.open(.GET, uri, .{
278288 .server_header_buffer = &server_header_buffer,
279289 });
280290 defer req.deinit();
......@@ -292,16 +302,13 @@ pub fn main() !void {
292302 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
293303
294304 { // send head request and not read chunked
295 var h = http.Headers{ .allocator = calloc };
296 defer h.deinit();
297
298305 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get", .{port});
299306 defer calloc.free(location);
300307 const uri = try std.Uri.parse(location);
301308
302309 log.info("{s}", .{location});
303310 var server_header_buffer: [1024]u8 = undefined;
304 var req = try client.open(.HEAD, uri, h, .{
311 var req = try client.open(.HEAD, uri, .{
305312 .server_header_buffer = &server_header_buffer,
306313 });
307314 defer req.deinit();
......@@ -313,24 +320,21 @@ pub fn main() !void {
313320 defer calloc.free(body);
314321
315322 try testing.expectEqualStrings("", body);
316 try testing.expectEqualStrings("text/plain", req.response.headers.getFirstValue("content-type").?);
317 try testing.expectEqualStrings("14", req.response.headers.getFirstValue("content-length").?);
323 try testing.expectEqualStrings("text/plain", req.response.content_type.?);
324 try testing.expectEqual(14, req.response.content_length.?);
318325 }
319326
320327 // connection has been kept alive
321328 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
322329
323330 { // read chunked response
324 var h = http.Headers{ .allocator = calloc };
325 defer h.deinit();
326
327331 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get?chunked", .{port});
328332 defer calloc.free(location);
329333 const uri = try std.Uri.parse(location);
330334
331335 log.info("{s}", .{location});
332336 var server_header_buffer: [1024]u8 = undefined;
333 var req = try client.open(.GET, uri, h, .{
337 var req = try client.open(.GET, uri, .{
334338 .server_header_buffer = &server_header_buffer,
335339 });
336340 defer req.deinit();
......@@ -342,23 +346,20 @@ pub fn main() !void {
342346 defer calloc.free(body);
343347
344348 try testing.expectEqualStrings("Hello, World!\n", body);
345 try testing.expectEqualStrings("text/plain", req.response.headers.getFirstValue("content-type").?);
349 try testing.expectEqualStrings("text/plain", req.response.content_type.?);
346350 }
347351
348352 // connection has been kept alive
349353 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
350354
351355 { // send head request and not read chunked
352 var h = http.Headers{ .allocator = calloc };
353 defer h.deinit();
354
355356 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get?chunked", .{port});
356357 defer calloc.free(location);
357358 const uri = try std.Uri.parse(location);
358359
359360 log.info("{s}", .{location});
360361 var server_header_buffer: [1024]u8 = undefined;
361 var req = try client.open(.HEAD, uri, h, .{
362 var req = try client.open(.HEAD, uri, .{
362363 .server_header_buffer = &server_header_buffer,
363364 });
364365 defer req.deinit();
......@@ -370,24 +371,21 @@ pub fn main() !void {
370371 defer calloc.free(body);
371372
372373 try testing.expectEqualStrings("", body);
373 try testing.expectEqualStrings("text/plain", req.response.headers.getFirstValue("content-type").?);
374 try testing.expectEqualStrings("chunked", req.response.headers.getFirstValue("transfer-encoding").?);
374 try testing.expectEqualStrings("text/plain", req.response.content_type.?);
375 try testing.expect(req.response.transfer_encoding == .chunked);
375376 }
376377
377378 // connection has been kept alive
378379 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
379380
380381 { // check trailing headers
381 var h = http.Headers{ .allocator = calloc };
382 defer h.deinit();
383
384382 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/trailer", .{port});
385383 defer calloc.free(location);
386384 const uri = try std.Uri.parse(location);
387385
388386 log.info("{s}", .{location});
389387 var server_header_buffer: [1024]u8 = undefined;
390 var req = try client.open(.GET, uri, h, .{
388 var req = try client.open(.GET, uri, .{
391389 .server_header_buffer = &server_header_buffer,
392390 });
393391 defer req.deinit();
......@@ -399,26 +397,25 @@ pub fn main() !void {
399397 defer calloc.free(body);
400398
401399 try testing.expectEqualStrings("Hello, World!\n", body);
402 try testing.expectEqualStrings("aaaa", req.response.headers.getFirstValue("x-checksum").?);
400 @panic("TODO implement inspecting custom headers in responses");
401 //try testing.expectEqualStrings("aaaa", req.response.headers.getFirstValue("x-checksum").?);
403402 }
404403
405404 // connection has been kept alive
406405 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
407406
408407 { // send content-length request
409 var h = http.Headers{ .allocator = calloc };
410 defer h.deinit();
411
412 try h.append("content-type", "text/plain");
413
414408 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/echo-content", .{port});
415409 defer calloc.free(location);
416410 const uri = try std.Uri.parse(location);
417411
418412 log.info("{s}", .{location});
419413 var server_header_buffer: [1024]u8 = undefined;
420 var req = try client.open(.POST, uri, h, .{
414 var req = try client.open(.POST, uri, .{
421415 .server_header_buffer = &server_header_buffer,
416 .extra_headers = &.{
417 .{ .name = "content-type", .value = "text/plain" },
418 },
422419 });
423420 defer req.deinit();
424421
......@@ -441,19 +438,15 @@ pub fn main() !void {
441438 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
442439
443440 { // read content-length response with connection close
444 var h = http.Headers{ .allocator = calloc };
445 defer h.deinit();
446
447 try h.append("connection", "close");
448
449441 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get", .{port});
450442 defer calloc.free(location);
451443 const uri = try std.Uri.parse(location);
452444
453445 log.info("{s}", .{location});
454446 var server_header_buffer: [1024]u8 = undefined;
455 var req = try client.open(.GET, uri, h, .{
447 var req = try client.open(.GET, uri, .{
456448 .server_header_buffer = &server_header_buffer,
449 .keep_alive = false,
457450 });
458451 defer req.deinit();
459452
......@@ -464,26 +457,24 @@ pub fn main() !void {
464457 defer calloc.free(body);
465458
466459 try testing.expectEqualStrings("Hello, World!\n", body);
467 try testing.expectEqualStrings("text/plain", req.response.headers.getFirstValue("content-type").?);
460 try testing.expectEqualStrings("text/plain", req.response.content_type.?);
468461 }
469462
470463 // connection has been closed
471464 try testing.expect(client.connection_pool.free_len == 0);
472465
473466 { // send chunked request
474 var h = http.Headers{ .allocator = calloc };
475 defer h.deinit();
476
477 try h.append("content-type", "text/plain");
478
479467 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/echo-content", .{port});
480468 defer calloc.free(location);
481469 const uri = try std.Uri.parse(location);
482470
483471 log.info("{s}", .{location});
484472 var server_header_buffer: [1024]u8 = undefined;
485 var req = try client.open(.POST, uri, h, .{
473 var req = try client.open(.POST, uri, .{
486474 .server_header_buffer = &server_header_buffer,
475 .extra_headers = &.{
476 .{ .name = "content-type", .value = "text/plain" },
477 },
487478 });
488479 defer req.deinit();
489480
......@@ -506,16 +497,13 @@ pub fn main() !void {
506497 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
507498
508499 { // relative redirect
509 var h = http.Headers{ .allocator = calloc };
510 defer h.deinit();
511
512500 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/1", .{port});
513501 defer calloc.free(location);
514502 const uri = try std.Uri.parse(location);
515503
516504 log.info("{s}", .{location});
517505 var server_header_buffer: [1024]u8 = undefined;
518 var req = try client.open(.GET, uri, h, .{
506 var req = try client.open(.GET, uri, .{
519507 .server_header_buffer = &server_header_buffer,
520508 });
521509 defer req.deinit();
......@@ -533,16 +521,13 @@ pub fn main() !void {
533521 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
534522
535523 { // redirect from root
536 var h = http.Headers{ .allocator = calloc };
537 defer h.deinit();
538
539524 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/2", .{port});
540525 defer calloc.free(location);
541526 const uri = try std.Uri.parse(location);
542527
543528 log.info("{s}", .{location});
544529 var server_header_buffer: [1024]u8 = undefined;
545 var req = try client.open(.GET, uri, h, .{
530 var req = try client.open(.GET, uri, .{
546531 .server_header_buffer = &server_header_buffer,
547532 });
548533 defer req.deinit();
......@@ -560,16 +545,13 @@ pub fn main() !void {
560545 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
561546
562547 { // absolute redirect
563 var h = http.Headers{ .allocator = calloc };
564 defer h.deinit();
565
566548 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/3", .{port});
567549 defer calloc.free(location);
568550 const uri = try std.Uri.parse(location);
569551
570552 log.info("{s}", .{location});
571553 var server_header_buffer: [1024]u8 = undefined;
572 var req = try client.open(.GET, uri, h, .{
554 var req = try client.open(.GET, uri, .{
573555 .server_header_buffer = &server_header_buffer,
574556 });
575557 defer req.deinit();
......@@ -587,16 +569,13 @@ pub fn main() !void {
587569 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
588570
589571 { // too many redirects
590 var h = http.Headers{ .allocator = calloc };
591 defer h.deinit();
592
593572 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/4", .{port});
594573 defer calloc.free(location);
595574 const uri = try std.Uri.parse(location);
596575
597576 log.info("{s}", .{location});
598577 var server_header_buffer: [1024]u8 = undefined;
599 var req = try client.open(.GET, uri, h, .{
578 var req = try client.open(.GET, uri, .{
600579 .server_header_buffer = &server_header_buffer,
601580 });
602581 defer req.deinit();
......@@ -612,16 +591,13 @@ pub fn main() !void {
612591 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
613592
614593 { // check client without segfault by connection error after redirection
615 var h = http.Headers{ .allocator = calloc };
616 defer h.deinit();
617
618594 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/invalid", .{port});
619595 defer calloc.free(location);
620596 const uri = try std.Uri.parse(location);
621597
622598 log.info("{s}", .{location});
623599 var server_header_buffer: [1024]u8 = undefined;
624 var req = try client.open(.GET, uri, h, .{
600 var req = try client.open(.GET, uri, .{
625601 .server_header_buffer = &server_header_buffer,
626602 });
627603 defer req.deinit();
......@@ -639,10 +615,6 @@ pub fn main() !void {
639615 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
640616
641617 { // Client.fetch()
642 var h = http.Headers{ .allocator = calloc };
643 defer h.deinit();
644
645 try h.append("content-type", "text/plain");
646618
647619 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/echo-content#fetch", .{port});
648620 defer calloc.free(location);
......@@ -651,8 +623,10 @@ pub fn main() !void {
651623 var res = try client.fetch(calloc, .{
652624 .location = .{ .url = location },
653625 .method = .POST,
654 .headers = h,
655626 .payload = .{ .string = "Hello, World!\n" },
627 .extra_headers = &.{
628 .{ .name = "content-type", .value = "text/plain" },
629 },
656630 });
657631 defer res.deinit();
658632
......@@ -660,20 +634,18 @@ pub fn main() !void {
660634 }
661635
662636 { // expect: 100-continue
663 var h = http.Headers{ .allocator = calloc };
664 defer h.deinit();
665
666 try h.append("expect", "100-continue");
667 try h.append("content-type", "text/plain");
668
669637 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/echo-content#expect-100", .{port});
670638 defer calloc.free(location);
671639 const uri = try std.Uri.parse(location);
672640
673641 log.info("{s}", .{location});
674642 var server_header_buffer: [1024]u8 = undefined;
675 var req = try client.open(.POST, uri, h, .{
643 var req = try client.open(.POST, uri, .{
676644 .server_header_buffer = &server_header_buffer,
645 .extra_headers = &.{
646 .{ .name = "expect", .value = "100-continue" },
647 .{ .name = "content-type", .value = "text/plain" },
648 },
677649 });
678650 defer req.deinit();
679651
......@@ -694,20 +666,18 @@ pub fn main() !void {
694666 }
695667
696668 { // expect: garbage
697 var h = http.Headers{ .allocator = calloc };
698 defer h.deinit();
699
700 try h.append("content-type", "text/plain");
701 try h.append("expect", "garbage");
702
703669 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/echo-content#expect-garbage", .{port});
704670 defer calloc.free(location);
705671 const uri = try std.Uri.parse(location);
706672
707673 log.info("{s}", .{location});
708674 var server_header_buffer: [1024]u8 = undefined;
709 var req = try client.open(.POST, uri, h, .{
675 var req = try client.open(.POST, uri, .{
710676 .server_header_buffer = &server_header_buffer,
677 .extra_headers = &.{
678 .{ .name = "content-type", .value = "text/plain" },
679 .{ .name = "expect", .value = "garbage" },
680 },
711681 });
712682 defer req.deinit();
713683
......@@ -734,7 +704,7 @@ pub fn main() !void {
734704 for (0..total_connections) |i| {
735705 const headers_buf = try calloc.alloc(u8, 1024);
736706 try header_bufs.append(headers_buf);
737 var req = try client.open(.GET, uri, .{ .allocator = calloc }, .{
707 var req = try client.open(.GET, uri, .{
738708 .server_header_buffer = headers_buf,
739709 });
740710 req.response.parser.state = .complete;