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");...@@ -3,10 +3,6 @@ const std = @import("std.zig");
3pub const Client = @import("http/Client.zig");3pub const Client = @import("http/Client.zig");
4pub const Server = @import("http/Server.zig");4pub const Server = @import("http/Server.zig");
5pub const protocol = @import("http/protocol.zig");5pub const protocol = @import("http/protocol.zig");
6const headers = @import("http/Headers.zig");
7
8pub const Headers = headers.Headers;
9pub const Field = headers.Field;
106
11pub const Version = enum {7pub const Version = enum {
12 @"HTTP/1.0",8 @"HTTP/1.0",
...@@ -18,7 +14,7 @@ pub const Version = enum {...@@ -18,7 +14,7 @@ pub const Version = enum {
18/// https://datatracker.ietf.org/doc/html/rfc7231#section-4 Initial definition14/// https://datatracker.ietf.org/doc/html/rfc7231#section-4 Initial definition
19///15///
20/// https://datatracker.ietf.org/doc/html/rfc5789#section-2 PATCH16/// 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 CI17pub const Method = enum(u64) {
22 GET = parse("GET"),18 GET = parse("GET"),
23 HEAD = parse("HEAD"),19 HEAD = parse("HEAD"),
24 POST = parse("POST"),20 POST = parse("POST"),
...@@ -309,6 +305,11 @@ pub const Connection = enum {...@@ -309,6 +305,11 @@ pub const Connection = enum {
309 close,305 close,
310};306};
311307
308pub const Header = struct {
309 name: []const u8,
310 value: []const u8,
311};
312
312test {313test {
313 _ = Client;314 _ = Client;
314 _ = Method;315 _ = Method;
lib/std/http/Client.zig+288-299
...@@ -33,13 +33,14 @@ next_https_rescan_certs: bool = true,...@@ -33,13 +33,14 @@ next_https_rescan_certs: bool = true,
33/// The pool of connections that can be reused (and currently in use).33/// The pool of connections that can be reused (and currently in use).
34connection_pool: ConnectionPool = .{},34connection_pool: ConnectionPool = .{},
3535
36/// This is the proxy that will handle http:// connections. It *must not* be36/// If populated, all http traffic travels through this third party.
37/// modified when the client has any active connections.37/// This field cannot be modified while the client has active connections.
38http_proxy: ?Proxy = null,38/// Pointer to externally-owned memory.
3939http_proxy: ?*Proxy = null,
40/// This is the proxy that will handle https:// connections. It *must not* be40/// If populated, all https traffic travels through this third party.
41/// modified when the client has any active connections.41/// This field cannot be modified while the client has active connections.
42https_proxy: ?Proxy = null,42/// Pointer to externally-owned memory.
43https_proxy: ?*Proxy = null,
4344
44/// A set of linked lists of connections that can be reused.45/// A set of linked lists of connections that can be reused.
45pub const ConnectionPool = struct {46pub const ConnectionPool = struct {
...@@ -422,7 +423,7 @@ pub const Response = struct {...@@ -422,7 +423,7 @@ pub const Response = struct {
422 HttpTransferEncodingUnsupported,423 HttpTransferEncodingUnsupported,
423 HttpConnectionHeaderUnsupported,424 HttpConnectionHeaderUnsupported,
424 InvalidContentLength,425 InvalidContentLength,
425 CompressionNotSupported,426 CompressionUnsupported,
426 };427 };
427428
428 pub fn parse(res: *Response, bytes: []const u8, trailing: bool) ParseError!void {429 pub fn parse(res: *Response, bytes: []const u8, trailing: bool) ParseError!void {
...@@ -445,8 +446,6 @@ pub const Response = struct {...@@ -445,8 +446,6 @@ pub const Response = struct {
445 res.status = status;446 res.status = status;
446 res.reason = reason;447 res.reason = reason;
447448
448 res.headers.clearRetainingCapacity();
449
450 while (it.next()) |line| {449 while (it.next()) |line| {
451 if (line.len == 0) return error.HttpHeadersInvalid;450 if (line.len == 0) return error.HttpHeadersInvalid;
452 switch (line[0]) {451 switch (line[0]) {
...@@ -458,11 +457,17 @@ pub const Response = struct {...@@ -458,11 +457,17 @@ pub const Response = struct {
458 const header_name = line_it.next() orelse return error.HttpHeadersInvalid;457 const header_name = line_it.next() orelse return error.HttpHeadersInvalid;
459 const header_value = line_it.rest();458 const header_value = line_it.rest();
460459
461 try res.headers.append(header_name, header_value);
462
463 if (trailing) continue;460 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")) {
466 // Transfer-Encoding: second, first471 // Transfer-Encoding: second, first
467 // Transfer-Encoding: deflate, chunked472 // Transfer-Encoding: deflate, chunked
468 var iter = mem.splitBackwardsScalar(u8, header_value, ',');473 var iter = mem.splitBackwardsScalar(u8, header_value, ',');
...@@ -531,15 +536,19 @@ pub const Response = struct {...@@ -531,15 +536,19 @@ pub const Response = struct {
531 try expectEqual(@as(u10, 999), parseInt3("999"));536 try expectEqual(@as(u10, 999), parseInt3("999"));
532 }537 }
533538
534 /// The HTTP version this response is using.
535 version: http.Version,539 version: http.Version,
536
537 /// The status code of the response.
538 status: http.Status,540 status: http.Status,
539
540 /// The reason phrase of the response.
541 reason: []const u8,541 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
543 /// If present, the number of bytes in the response body.552 /// If present, the number of bytes in the response body.
544 content_length: ?u64 = null,553 content_length: ?u64 = null,
545554
...@@ -549,12 +558,11 @@ pub const Response = struct {...@@ -549,12 +558,11 @@ pub const Response = struct {
549 /// If present, the compression of the response body, otherwise identity (no compression).558 /// If present, the compression of the response body, otherwise identity (no compression).
550 transfer_compression: http.ContentEncoding = .identity,559 transfer_compression: http.ContentEncoding = .identity,
551560
552 /// The headers received from the server.
553 headers: http.Headers,
554 parser: proto.HeadersParser,561 parser: proto.HeadersParser,
555 compression: Compression = .none,562 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.
558 skip: bool = false,566 skip: bool = false,
559};567};
560568
...@@ -562,24 +570,15 @@ pub const Response = struct {...@@ -562,24 +570,15 @@ pub const Response = struct {
562///570///
563/// Order of operations: open -> send[ -> write -> finish] -> wait -> read571/// Order of operations: open -> send[ -> write -> finish] -> wait -> read
564pub const Request = struct {572pub const Request = struct {
565 /// The uri that this request is being sent to.
566 uri: Uri,573 uri: Uri,
567
568 /// The client that this request was created from.
569 client: *Client,574 client: *Client,
570575 /// This is null when the connection is released.
571 /// Underlying connection to the server. This is null when the connection is released.
572 connection: ?*Connection,576 connection: ?*Connection,
577 keep_alive: bool,
573578
574 method: http.Method,579 method: http.Method,
575 version: http.Version = .@"HTTP/1.1",580 version: http.Version = .@"HTTP/1.1",
576581 transfer_encoding: RequestTransfer,
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
583 redirect_behavior: RedirectBehavior,582 redirect_behavior: RedirectBehavior,
584583
585 /// Whether the request should handle a 100-continue response before sending the request body.584 /// Whether the request should handle a 100-continue response before sending the request body.
...@@ -593,6 +592,34 @@ pub const Request = struct {...@@ -593,6 +592,34 @@ pub const Request = struct {
593 /// Used as a allocator for resolving redirects locations.592 /// Used as a allocator for resolving redirects locations.
594 arena: std.heap.ArenaAllocator,593 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
596 /// Any value other than `not_allowed` or `unhandled` means that integer represents623 /// Any value other than `not_allowed` or `unhandled` means that integer represents
597 /// how many remaining redirects are allowed.624 /// how many remaining redirects are allowed.
598 pub const RedirectBehavior = enum(u16) {625 pub const RedirectBehavior = enum(u16) {
...@@ -621,9 +648,6 @@ pub const Request = struct {...@@ -621,9 +648,6 @@ pub const Request = struct {
621 .zstd => |*zstd| zstd.deinit(),648 .zstd => |*zstd| zstd.deinit(),
622 }649 }
623650
624 req.headers.deinit();
625 req.response.headers.deinit();
626
627 if (req.connection) |connection| {651 if (req.connection) |connection| {
628 if (req.response.parser.state != .complete) {652 if (req.response.parser.state != .complete) {
629 // If the response wasn't fully read, then we need to close the connection.653 // If the response wasn't fully read, then we need to close the connection.
...@@ -664,14 +688,12 @@ pub const Request = struct {...@@ -664,14 +688,12 @@ pub const Request = struct {
664 req.uri = uri;688 req.uri = uri;
665 req.connection = try req.client.connect(host, port, protocol);689 req.connection = try req.client.connect(host, port, protocol);
666 req.redirect_behavior.subtractOne();690 req.redirect_behavior.subtractOne();
667 req.response.headers.clearRetainingCapacity();
668 req.response.parser.reset();691 req.response.parser.reset();
669692
670 req.response = .{693 req.response = .{
671 .status = undefined,694 .status = undefined,
672 .reason = undefined,695 .reason = undefined,
673 .version = undefined,696 .version = undefined,
674 .headers = req.response.headers,
675 .parser = req.response.parser,697 .parser = req.response.parser,
676 };698 };
677 }699 }
...@@ -685,9 +707,11 @@ pub const Request = struct {...@@ -685,9 +707,11 @@ pub const Request = struct {
685707
686 /// Send the HTTP request headers to the server.708 /// Send the HTTP request headers to the server.
687 pub fn send(req: *Request, options: SendOptions) SendError!void {709 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
692 try req.method.write(w);716 try req.method.write(w);
693 try w.writeByte(' ');717 try w.writeByte(' ');
...@@ -696,9 +720,9 @@ pub const Request = struct {...@@ -696,9 +720,9 @@ pub const Request = struct {
696 try req.uri.writeToStream(.{ .authority = true }, w);720 try req.uri.writeToStream(.{ .authority = true }, w);
697 } else {721 } else {
698 try req.uri.writeToStream(.{722 try req.uri.writeToStream(.{
699 .scheme = req.connection.?.proxied,723 .scheme = connection.proxied,
700 .authentication = req.connection.?.proxied,724 .authentication = connection.proxied,
701 .authority = req.connection.?.proxied,725 .authority = connection.proxied,
702 .path = true,726 .path = true,
703 .query = true,727 .query = true,
704 .raw = options.raw_uri,728 .raw = options.raw_uri,
...@@ -708,97 +732,91 @@ pub const Request = struct {...@@ -708,97 +732,91 @@ pub const Request = struct {
708 try w.writeAll(@tagName(req.version));732 try w.writeAll(@tagName(req.version));
709 try w.writeAll("\r\n");733 try w.writeAll("\r\n");
710734
711 if (!req.headers.contains("host")) {735 if (try emitOverridableHeader("host: ", req.headers.host, w)) {
712 try w.writeAll("Host: ");736 try w.writeAll("host: ");
713 try req.uri.writeToStream(.{ .authority = true }, w);737 try req.uri.writeToStream(.{ .authority = true }, w);
714 try w.writeAll("\r\n");738 try w.writeAll("\r\n");
715 }739 }
716740
717 if ((req.uri.user != null or req.uri.password != null) and741 if (try emitOverridableHeader("authorization: ", req.headers.authorization, w)) {
718 !req.headers.contains("authorization"))742 if (req.uri.user != null or req.uri.password != null) {
719 {743 try w.writeAll("authorization: ");
720 try w.writeAll("Authorization: ");744 const authorization = try connection.allocWriteBuffer(
721 const authorization = try req.connection.?.allocWriteBuffer(745 @intCast(basic_authorization.valueLengthFromUri(req.uri)),
722 @intCast(basic_authorization.valueLengthFromUri(req.uri)),746 );
723 );747 assert(basic_authorization.value(req.uri, authorization).len == authorization.len);
724 std.debug.assert(basic_authorization.value(req.uri, authorization).len == authorization.len);748 try w.writeAll("\r\n");
725 try w.writeAll("\r\n");749 }
726 }750 }
727751
728 if (!req.headers.contains("user-agent")) {752 if (try emitOverridableHeader("user-agent: ", req.headers.user_agent, w)) {
729 try w.writeAll("User-Agent: zig/");753 try w.writeAll("user-agent: zig/");
730 try w.writeAll(builtin.zig_version_string);754 try w.writeAll(builtin.zig_version_string);
731 try w.writeAll(" (std.http)\r\n");755 try w.writeAll(" (std.http)\r\n");
732 }756 }
733757
734 if (!req.headers.contains("connection")) {758 if (try emitOverridableHeader("connection: ", req.headers.connection, w)) {
735 try w.writeAll("Connection: keep-alive\r\n");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 }
736 }764 }
737765
738 if (!req.headers.contains("accept-encoding")) {766 if (try emitOverridableHeader("accept-encoding: ", req.headers.accept_encoding, w)) {
739 try w.writeAll("Accept-Encoding: gzip, deflate, zstd\r\n");767 try w.writeAll("accept-encoding: gzip, deflate, zstd\r\n");
740 }768 }
741769
742 if (!req.headers.contains("te")) {770 switch (req.transfer_encoding) {
743 try w.writeAll("TE: gzip, deflate, trailers\r\n");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 => {},
744 }774 }
745775
746 const has_transfer_encoding = req.headers.contains("transfer-encoding");776 if (try emitOverridableHeader("content-type: ", req.headers.content_type, w)) {
747 const has_content_length = req.headers.contains("content-length");777 // The default is to omit content-type if not provided because
748778 // "application/octet-stream" is redundant.
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 }
770 }779 }
771780
772 for (req.headers.list.items) |entry| {781 for (req.extra_headers) |header| {
773 if (entry.value.len == 0) continue;782 assert(header.value.len != 0);
774783
775 try w.writeAll(entry.name);784 try w.writeAll(header.name);
776 try w.writeAll(": ");785 try w.writeAll(": ");
777 try w.writeAll(entry.value);786 try w.writeAll(header.value);
778 try w.writeAll("\r\n");787 try w.writeAll("\r\n");
779 }788 }
780789
781 if (req.connection.?.proxied) {790 if (connection.proxied) proxy: {
782 const proxy_headers: ?http.Headers = switch (req.connection.?.protocol) {791 const proxy = switch (connection.protocol) {
783 .plain => if (req.client.http_proxy) |proxy| proxy.headers else null,792 .plain => req.client.http_proxy,
784 .tls => if (req.client.https_proxy) |proxy| proxy.headers else null,793 .tls => req.client.https_proxy,
785 };794 } orelse break :proxy;
786
787 if (proxy_headers) |headers| {
788 for (headers.list.items) |entry| {
789 if (entry.value.len == 0) continue;
790795
791 try w.writeAll(entry.name);796 const authorization = proxy.authorization orelse break :proxy;
792 try w.writeAll(": ");797 try w.writeAll("proxy-authorization: ");
793 try w.writeAll(entry.value);798 try w.writeAll(authorization);
794 try w.writeAll("\r\n");799 try w.writeAll("\r\n");
795 }
796 }
797 }800 }
798801
799 try w.writeAll("\r\n");802 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 }
802 }820 }
803821
804 const TransferReadError = Connection.ReadError || proto.HeadersParser.ReadError;822 const TransferReadError = Connection.ReadError || proto.HeadersParser.ReadError;
...@@ -829,7 +847,7 @@ pub const Request = struct {...@@ -829,7 +847,7 @@ pub const Request = struct {
829 RedirectRequiresResend,847 RedirectRequiresResend,
830 HttpRedirectMissingLocation,848 HttpRedirectMissingLocation,
831 CompressionInitializationFailed,849 CompressionInitializationFailed,
832 CompressionNotSupported,850 CompressionUnsupported,
833 };851 };
834852
835 /// Waits for a response from the server and parses any headers that are sent.853 /// Waits for a response from the server and parses any headers that are sent.
...@@ -843,12 +861,14 @@ pub const Request = struct {...@@ -843,12 +861,14 @@ pub const Request = struct {
843 /// Must be called after `send` and, if any data was written to the request861 /// Must be called after `send` and, if any data was written to the request
844 /// body, then also after `finish`.862 /// body, then also after `finish`.
845 pub fn wait(req: *Request) WaitError!void {863 pub fn wait(req: *Request) WaitError!void {
864 const connection = req.connection.?;
865
846 while (true) { // handle redirects866 while (true) { // handle redirects
847 while (true) { // read headers867 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());870 const nchecked = try req.response.parser.checkCompleteHead(connection.peek());
851 req.connection.?.drop(@intCast(nchecked));871 connection.drop(@intCast(nchecked));
852872
853 if (req.response.parser.state.isContent()) break;873 if (req.response.parser.state.isContent()) break;
854 }874 }
...@@ -856,44 +876,36 @@ pub const Request = struct {...@@ -856,44 +876,36 @@ pub const Request = struct {
856 try req.response.parse(req.response.parser.get(), false);876 try req.response.parse(req.response.parser.get(), false);
857877
858 if (req.response.status == .@"continue") {878 if (req.response.status == .@"continue") {
859 req.response.parser.state = .complete; // we're done parsing the continue response, reset to prepare for the real response879 // We're done parsing the continue response; reset to prepare
880 // for the real response.
881 req.response.parser.state = .complete;
860 req.response.parser.reset();882 req.response.parser.reset();
861883
862 if (req.handle_continue)884 if (req.handle_continue)
863 continue;885 continue;
864886
865 return; // we're not handling the 100-continue, return to the caller887 return; // we're not handling the 100-continue
866 }888 }
867889
868 // we're switching protocols, so this connection is no longer doing http890 // we're switching protocols, so this connection is no longer doing http
869 if (req.method == .CONNECT and req.response.status.class() == .success) {891 if (req.method == .CONNECT and req.response.status.class() == .success) {
870 req.connection.?.closing = false;892 connection.closing = false;
871 req.response.parser.state = .complete;893 req.response.parser.state = .complete;
872894 return; // the connection is not HTTP past this point
873 return; // the connection is not HTTP past this point, return to the caller
874 }895 }
875896
876 // we default to using keep-alive if not provided in the client if the server asks for it897 connection.closing = !req.response.keep_alive or !req.keep_alive;
877 const req_connection = req.headers.getFirstValue("connection");
878 const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?);
879898
880 const res_connection = req.response.headers.getFirstValue("connection");899 // Any response to a HEAD request and any response with a 1xx
881 const res_keepalive = res_connection != null and !std.ascii.eqlIgnoreCase("close", res_connection.?);900 // (Informational), 204 (No Content), or 304 (Not Modified) status
882 if (res_keepalive and (req_keepalive or req_connection == null)) {901 // code is always terminated by the first empty line after the
883 req.connection.?.closing = false;902 // header fields, regardless of the header fields present in the
884 } else {903 // message.
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
891 if (req.method == .HEAD or req.response.status.class() == .informational or904 if (req.method == .HEAD or req.response.status.class() == .informational or
892 req.response.status == .no_content or req.response.status == .not_modified)905 req.response.status == .no_content or req.response.status == .not_modified)
893 {906 {
894 req.response.parser.state = .complete;907 req.response.parser.state = .complete;
895908 return; // The response is empty; no further setup or redirection is necessary.
896 return; // the response is empty, no further setup or redirection is necessary
897 }909 }
898910
899 if (req.response.transfer_encoding != .none) {911 if (req.response.transfer_encoding != .none) {
...@@ -922,7 +934,7 @@ pub const Request = struct {...@@ -922,7 +934,7 @@ pub const Request = struct {
922934
923 if (req.redirect_behavior == .not_allowed) return error.TooManyHttpRedirects;935 if (req.redirect_behavior == .not_allowed) return error.TooManyHttpRedirects;
924936
925 const location = req.response.headers.getFirstValue("location") orelse937 const location = req.response.location orelse
926 return error.HttpRedirectMissingLocation;938 return error.HttpRedirectMissingLocation;
927939
928 const arena = req.arena.allocator();940 const arena = req.arena.allocator();
...@@ -932,42 +944,44 @@ pub const Request = struct {...@@ -932,42 +944,44 @@ pub const Request = struct {
932 const new_url = Uri.parse(location_duped) catch try Uri.parseWithoutScheme(location_duped);944 const new_url = Uri.parse(location_duped) catch try Uri.parseWithoutScheme(location_duped);
933 const resolved_url = try req.uri.resolve(new_url, false, arena);945 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?
936 const is_same_domain_or_subdomain =947 const is_same_domain_or_subdomain =
937 std.ascii.endsWithIgnoreCase(resolved_url.host.?, req.uri.host.?) and948 std.ascii.endsWithIgnoreCase(resolved_url.host.?, req.uri.host.?) and
938 (resolved_url.host.?.len == req.uri.host.?.len or949 (resolved_url.host.?.len == req.uri.host.?.len or
939 resolved_url.host.?[resolved_url.host.?.len - req.uri.host.?.len - 1] == '.');950 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)) {952 if (resolved_url.host == null or !is_same_domain_or_subdomain or
942 // we're redirecting to a different domain, strip privileged headers like cookies953 !std.ascii.eqlIgnoreCase(resolved_url.scheme, req.uri.scheme))
943 _ = req.headers.delete("authorization");954 {
944 _ = req.headers.delete("www-authenticate");955 // When redirecting to a different domain, strip privileged headers.
945 _ = req.headers.delete("cookie");956 req.privileged_headers = &.{};
946 _ = req.headers.delete("cookie2");
947 }957 }
948958
949 if (req.response.status == .see_other or ((req.response.status == .moved_permanently or req.response.status == .found) and req.method == .POST)) {959 if (switch (req.response.status) {
950 // we're redirecting to a GET, so we need to change the method and remove the body960 .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.
951 req.method = .GET;965 req.method = .GET;
952 req.transfer_encoding = .none;966 req.transfer_encoding = .none;
953 _ = req.headers.delete("transfer-encoding");967 req.headers.content_type = .omit;
954 _ = req.headers.delete("content-length");
955 _ = req.headers.delete("content-type");
956 }968 }
957969
958 if (req.transfer_encoding != .none) {970 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;
960 }975 }
961976
962 try req.redirect(resolved_url);977 try req.redirect(resolved_url);
963
964 try req.send(.{});978 try req.send(.{});
965 } else {979 } else {
966 req.response.skip = false;980 req.response.skip = false;
967 if (req.response.parser.state != .complete) {981 if (req.response.parser.state != .complete) {
968 switch (req.response.transfer_compression) {982 switch (req.response.transfer_compression) {
969 .identity => req.response.compression = .none,983 .identity => req.response.compression = .none,
970 .compress, .@"x-compress" => return error.CompressionNotSupported,984 .compress, .@"x-compress" => return error.CompressionUnsupported,
971 .deflate => req.response.compression = .{985 .deflate => req.response.compression = .{
972 .deflate = std.compress.zlib.decompressor(req.transferReader()),986 .deflate = std.compress.zlib.decompressor(req.transferReader()),
973 },987 },
...@@ -1092,16 +1106,12 @@ pub const Request = struct {...@@ -1092,16 +1106,12 @@ pub const Request = struct {
1092 }1106 }
1093};1107};
10941108
1095/// A HTTP proxy server.
1096pub const Proxy = struct {1109pub const Proxy = struct {
1097 allocator: Allocator,
1098 headers: http.Headers,
1099
1100 protocol: Connection.Protocol,1110 protocol: Connection.Protocol,
1101 host: []const u8,1111 host: []const u8,
1112 authorization: ?[]const u8,
1102 port: u16,1113 port: u16,
11031114 supports_connect: bool,
1104 supports_connect: bool = true,
1105};1115};
11061116
1107/// Release all associated resources with the client.1117/// Release all associated resources with the client.
...@@ -1113,116 +1123,71 @@ pub fn deinit(client: *Client) void {...@@ -1113,116 +1123,71 @@ pub fn deinit(client: *Client) void {
11131123
1114 client.connection_pool.deinit(client.allocator);1124 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
1126 if (!disable_tls)1126 if (!disable_tls)
1127 client.ca_bundle.deinit(client.allocator);1127 client.ca_bundle.deinit(client.allocator);
11281128
1129 client.* = undefined;1129 client.* = undefined;
1130}1130}
11311131
1132/// Uses the *_proxy environment variable to set any unset proxies for the client.1132/// Populates `http_proxy` and `http_proxy` via standard proxy environment variables.
1133/// This function *must not* be called when the client has any active connections.1133/// Asserts the client has no active connections.
1134pub fn loadDefaultProxies(client: *Client) !void {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 {
1135 // Prevent any new connections from being created.1137 // Prevent any new connections from being created.
1136 client.connection_pool.mutex.lock();1138 client.connection_pool.mutex.lock();
1137 defer client.connection_pool.mutex.unlock();1139 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: {1143 if (client.http_proxy == null) {
1142 const content: []const u8 = if (std.process.hasEnvVarConstant("http_proxy"))1144 client.http_proxy = try createProxyFromEnvVar(arena, &.{
1143 try std.process.getEnvVarOwned(client.allocator, "http_proxy")1145 "http_proxy", "HTTP_PROXY", "all_proxy", "ALL_PROXY",
1144 else if (std.process.hasEnvVarConstant("HTTP_PROXY"))1146 });
1145 try std.process.getEnvVarOwned(client.allocator, "HTTP_PROXY")1147 }
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 };
11751148
1176 if (uri.user != null or uri.password != null) {1149 if (client.https_proxy == null) {
1177 const authorization = try client.allocator.alloc(u8, basic_authorization.valueLengthFromUri(uri));1150 client.https_proxy = try createProxyFromEnvVar(arena, &.{
1178 errdefer client.allocator.free(authorization);1151 "https_proxy", "HTTPS_PROXY", "all_proxy", "ALL_PROXY",
1179 std.debug.assert(basic_authorization.value(uri, authorization).len == authorization.len);1152 });
1180 try client.http_proxy.?.headers.appendOwned(.{ .unowned = "proxy-authorization" }, .{ .owned = authorization });
1181 }
1182 }1153 }
1154}
11831155
1184 if (client.https_proxy == null) https: {1156fn createProxyFromEnvVar(arena: Allocator, env_var_names: []const []const u8) !?*Proxy {
1185 const content: []const u8 = if (std.process.hasEnvVarConstant("https_proxy"))1157 const content = for (env_var_names) |name| {
1186 try std.process.getEnvVarOwned(client.allocator, "https_proxy")1158 break std.process.getEnvVarOwned(arena, name) catch |err| switch (err) {
1187 else if (std.process.hasEnvVarConstant("HTTPS_PROXY"))1159 error.EnvironmentVariableNotFound => continue,
1188 try std.process.getEnvVarOwned(client.allocator, "HTTPS_PROXY")1160 else => |e| return e,
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 },
1217 };1161 };
1162 } else return null;
12181163
1219 if (uri.user != null or uri.password != null) {1164 const uri = Uri.parse(content) catch try Uri.parseWithoutScheme(content);
1220 const authorization = try client.allocator.alloc(u8, basic_authorization.valueLengthFromUri(uri));1165
1221 errdefer client.allocator.free(authorization);1166 const protocol = if (uri.scheme.len == 0)
1222 std.debug.assert(basic_authorization.value(uri, authorization).len == authorization.len);1167 .plain // No scheme, assume http://
1223 try client.https_proxy.?.headers.appendOwned(.{ .unowned = "proxy-authorization" }, .{ .owned = authorization });1168 else
1224 }1169 protocol_map.get(uri.scheme) orelse return null; // Unknown scheme, ignore
1225 }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;
1226}1191}
12271192
1228pub const basic_authorization = struct {1193pub const basic_authorization = struct {
...@@ -1244,8 +1209,8 @@ pub const basic_authorization = struct {...@@ -1244,8 +1209,8 @@ pub const basic_authorization = struct {
1244 }1209 }
12451210
1246 pub fn value(uri: Uri, out: []u8) []u8 {1211 pub fn value(uri: Uri, out: []u8) []u8 {
1247 std.debug.assert(uri.user == null or uri.user.?.len <= max_user_len);1212 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);1213 assert(uri.password == null or uri.password.?.len <= max_password_len);
12491214
1250 @memcpy(out[0..prefix.len], prefix);1215 @memcpy(out[0..prefix.len], prefix);
12511216
...@@ -1356,7 +1321,8 @@ pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*Connecti...@@ -1356,7 +1321,8 @@ pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*Connecti
1356 return &conn.data;1321 return &conn.data;
1357}1322}
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.
1360///1326///
1361/// This function is threadsafe.1327/// This function is threadsafe.
1362pub fn connectTunnel(1328pub fn connectTunnel(
...@@ -1394,7 +1360,7 @@ pub fn connectTunnel(...@@ -1394,7 +1360,7 @@ pub fn connectTunnel(
1394 };1360 };
13951361
1396 var buffer: [8096]u8 = undefined;1362 var buffer: [8096]u8 = undefined;
1397 var req = client.open(.CONNECT, uri, proxy.headers, .{1363 var req = client.open(.CONNECT, uri, .{
1398 .redirect_behavior = .unhandled,1364 .redirect_behavior = .unhandled,
1399 .connection = conn,1365 .connection = conn,
1400 .server_header_buffer = &buffer,1366 .server_header_buffer = &buffer,
...@@ -1436,42 +1402,44 @@ pub fn connectTunnel(...@@ -1436,42 +1402,44 @@ pub fn connectTunnel(
1436const ConnectErrorPartial = ConnectTcpError || error{ UnsupportedUrlScheme, ConnectionRefused };1402const ConnectErrorPartial = ConnectTcpError || error{ UnsupportedUrlScheme, ConnectionRefused };
1437pub const ConnectError = ConnectErrorPartial || RequestError;1403pub const ConnectError = ConnectErrorPartial || RequestError;
14381404
1439/// Connect to `host:port` using the specified protocol. This will reuse a connection if one is already open.1405/// Connect to `host:port` using the specified protocol. This will reuse a
1440/// If a proxy is configured for the client, then the proxy will be used to connect to the host.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.
1441///1409///
1442/// This function is threadsafe.1410/// This function is threadsafe.
1443pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectError!*Connection {1411pub fn connect(
1444 // pointer required so that `supports_connect` can be updated if a CONNECT fails1412 client: *Client,
1445 const potential_proxy: ?*Proxy = switch (protocol) {1413 host: []const u8,
1446 .plain => if (client.http_proxy) |*proxy_info| proxy_info else null,1414 port: u16,
1447 .tls => if (client.https_proxy) |*proxy_info| proxy_info else null,1415 protocol: Connection.Protocol,
1448 };1416) ConnectError!*Connection {
14491417 const proxy = switch (protocol) {
1450 if (potential_proxy) |proxy| {1418 .plain => client.http_proxy,
1451 // don't attempt to proxy the proxy thru itself.1419 .tls => client.https_proxy,
1452 if (std.mem.eql(u8, proxy.host, host) and proxy.port == port and proxy.protocol == protocol) {1420 } orelse return client.connectTcp(host, port, protocol);
1453 return client.connectTcp(host, port, protocol);1421
1454 }1422 // Prevent proxying through itself.
14551423 if (std.mem.eql(u8, proxy.host, host) and proxy.port == port and proxy.protocol == protocol) {
1456 if (proxy.supports_connect) tunnel: {1424 return client.connectTcp(host, port, protocol);
1457 return connectTunnel(client, proxy, host, port) catch |err| switch (err) {1425 }
1458 error.TunnelNotSupported => break :tunnel,
1459 else => |e| return e,
1460 };
1461 }
14621426
1463 // fall back to using the proxy as a normal http proxy1427 if (proxy.supports_connect) tunnel: {
1464 const conn = try client.connectTcp(proxy.host, proxy.port, proxy.protocol);1428 return connectTunnel(client, proxy, host, port) catch |err| switch (err) {
1465 errdefer {1429 error.TunnelNotSupported => break :tunnel,
1466 conn.closing = true;1430 else => |e| return e,
1467 client.connection_pool.release(conn);1431 };
1468 }1432 }
14691433
1470 conn.proxied = true;1434 // fall back to using the proxy as a normal http proxy
1471 return conn;1435 const conn = try client.connectTcp(proxy.host, proxy.port, proxy.protocol);
1436 errdefer {
1437 conn.closing = true;
1438 client.connection_pool.release(conn);
1472 }1439 }
14731440
1474 return client.connectTcp(host, port, protocol);1441 conn.proxied = true;
1442 return conn;
1475}1443}
14761444
1477pub const RequestError = ConnectTcpError || ConnectErrorPartial || Request.SendError ||1445pub const RequestError = ConnectTcpError || ConnectErrorPartial || Request.SendError ||
...@@ -1496,6 +1464,10 @@ pub const RequestOptions = struct {...@@ -1496,6 +1464,10 @@ pub const RequestOptions = struct {
1496 /// you finish the request, then the request *will* deadlock.1464 /// you finish the request, then the request *will* deadlock.
1497 handle_continue: bool = true,1465 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
1499 /// This field specifies whether to automatically follow redirects, and if1471 /// This field specifies whether to automatically follow redirects, and if
1500 /// so, how many redirects to follow before returning an error.1472 /// so, how many redirects to follow before returning an error.
1501 ///1473 ///
...@@ -1510,6 +1482,17 @@ pub const RequestOptions = struct {...@@ -1510,6 +1482,17 @@ pub const RequestOptions = struct {
15101482
1511 /// Must be an already acquired connection.1483 /// Must be an already acquired connection.
1512 connection: ?*Connection = null,1484 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 = &.{},
1513};1496};
15141497
1515pub const protocol_map = std.ComptimeStringMap(Connection.Protocol, .{1498pub const protocol_map = std.ComptimeStringMap(Connection.Protocol, .{
...@@ -1522,7 +1505,6 @@ pub const protocol_map = std.ComptimeStringMap(Connection.Protocol, .{...@@ -1522,7 +1505,6 @@ pub const protocol_map = std.ComptimeStringMap(Connection.Protocol, .{
1522/// Open a connection to the host specified by `uri` and prepare to send a HTTP request.1505/// Open a connection to the host specified by `uri` and prepare to send a HTTP request.
1523///1506///
1524/// `uri` must remain alive during the entire request.1507/// `uri` must remain alive during the entire request.
1525/// `headers` is cloned and may be freed after this function returns.
1526///1508///
1527/// The caller is responsible for calling `deinit()` on the `Request`.1509/// The caller is responsible for calling `deinit()` on the `Request`.
1528/// This function is threadsafe.1510/// This function is threadsafe.
...@@ -1530,7 +1512,6 @@ pub fn open(...@@ -1530,7 +1512,6 @@ pub fn open(
1530 client: *Client,1512 client: *Client,
1531 method: http.Method,1513 method: http.Method,
1532 uri: Uri,1514 uri: Uri,
1533 headers: http.Headers,
1534 options: RequestOptions,1515 options: RequestOptions,
1535) RequestError!Request {1516) RequestError!Request {
1536 const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme;1517 const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme;
...@@ -1560,19 +1541,22 @@ pub fn open(...@@ -1560,19 +1541,22 @@ pub fn open(
1560 .uri = uri,1541 .uri = uri,
1561 .client = client,1542 .client = client,
1562 .connection = conn,1543 .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,
1564 .method = method,1545 .method = method,
1565 .version = options.version,1546 .version = options.version,
1547 .transfer_encoding = .none,
1566 .redirect_behavior = options.redirect_behavior,1548 .redirect_behavior = options.redirect_behavior,
1567 .handle_continue = options.handle_continue,1549 .handle_continue = options.handle_continue,
1568 .response = .{1550 .response = .{
1569 .status = undefined,1551 .status = undefined,
1570 .reason = undefined,1552 .reason = undefined,
1571 .version = undefined,1553 .version = undefined,
1572 .headers = http.Headers{ .allocator = client.allocator, .owned = false },
1573 .parser = proto.HeadersParser.init(options.server_header_buffer),1554 .parser = proto.HeadersParser.init(options.server_header_buffer),
1574 },1555 },
1575 .arena = undefined,1556 .arena = undefined,
1557 .headers = options.headers,
1558 .extra_headers = options.extra_headers,
1559 .privileged_headers = options.privileged_headers,
1576 };1560 };
1577 errdefer req.deinit();1561 errdefer req.deinit();
15781562
...@@ -1618,25 +1602,34 @@ pub const FetchOptions = struct {...@@ -1618,25 +1602,34 @@ pub const FetchOptions = struct {
16181602
1619 location: Location,1603 location: Location,
1620 method: http.Method = .GET,1604 method: http.Method = .GET,
1621 headers: http.Headers = .{ .allocator = std.heap.page_allocator, .owned = false },
1622 payload: Payload = .none,1605 payload: Payload = .none,
1623 raw_uri: bool = false,1606 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 = &.{},
1624};1618};
16251619
1626pub const FetchResult = struct {1620pub const FetchResult = struct {
1627 status: http.Status,1621 status: http.Status,
1628 body: ?[]const u8 = null,1622 body: ?[]const u8 = null,
1629 headers: http.Headers,
16301623
1631 allocator: Allocator,1624 allocator: Allocator,
1632 options: FetchOptions,1625 options: FetchOptions,
16331626
1634 pub fn deinit(res: *FetchResult) void {1627 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 {
1636 if (res.body) |body| res.allocator.free(body);1631 if (res.body) |body| res.allocator.free(body);
1637 }1632 }
1638
1639 res.headers.deinit();
1640 }1633 }
1641};1634};
16421635
...@@ -1644,21 +1637,19 @@ pub const FetchResult = struct {...@@ -1644,21 +1637,19 @@ pub const FetchResult = struct {
1644///1637///
1645/// This function is threadsafe.1638/// This function is threadsafe.
1646pub fn fetch(client: *Client, allocator: Allocator, options: FetchOptions) !FetchResult {1639pub 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
1652 const uri = switch (options.location) {1640 const uri = switch (options.location) {
1653 .url => |u| try Uri.parse(u),1641 .url => |u| try Uri.parse(u),
1654 .uri => |u| u,1642 .uri => |u| u,
1655 };1643 };
1656 var server_header_buffer: [16 * 1024]u8 = undefined;1644 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, .{
1659 .server_header_buffer = options.server_header_buffer orelse &server_header_buffer,1647 .server_header_buffer = options.server_header_buffer orelse &server_header_buffer,
1660 .redirect_behavior = options.redirect_behavior orelse1648 .redirect_behavior = options.redirect_behavior orelse
1661 if (options.payload == .none) @enumFromInt(3) else .unhandled,1649 if (options.payload == .none) @enumFromInt(3) else .unhandled,
1650 .headers = options.headers,
1651 .extra_headers = options.extra_headers,
1652 .privileged_headers = options.privileged_headers,
1662 });1653 });
1663 defer req.deinit();1654 defer req.deinit();
16641655
...@@ -1690,10 +1681,8 @@ pub fn fetch(client: *Client, allocator: Allocator, options: FetchOptions) !Fetc...@@ -1690,10 +1681,8 @@ pub fn fetch(client: *Client, allocator: Allocator, options: FetchOptions) !Fetc
16901681
1691 try req.wait();1682 try req.wait();
16921683
1693 var res = FetchResult{1684 var res: FetchResult = .{
1694 .status = req.response.status,1685 .status = req.response.status,
1695 .headers = try req.response.headers.clone(allocator),
1696
1697 .allocator = allocator,1686 .allocator = allocator,
1698 .options = options,1687 .options = options,
1699 };1688 };
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) {...@@ -162,11 +162,13 @@ pub const ResponseTransfer = union(enum) {
162pub const Compression = union(enum) {162pub const Compression = union(enum) {
163 pub const DeflateDecompressor = std.compress.zlib.Decompressor(Response.TransferReader);163 pub const DeflateDecompressor = std.compress.zlib.Decompressor(Response.TransferReader);
164 pub const GzipDecompressor = std.compress.gzip.Decompressor(Response.TransferReader);164 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
167 deflate: DeflateDecompressor,168 deflate: DeflateDecompressor,
168 gzip: GzipDecompressor,169 gzip: GzipDecompressor,
169 zstd: ZstdDecompressor,170 // https://github.com/ziglang/zig/issues/18937
171 //zstd: ZstdDecompressor,
170 none: void,172 none: void,
171};173};
172174
...@@ -179,7 +181,7 @@ pub const Request = struct {...@@ -179,7 +181,7 @@ pub const Request = struct {
179 HttpTransferEncodingUnsupported,181 HttpTransferEncodingUnsupported,
180 HttpConnectionHeaderUnsupported,182 HttpConnectionHeaderUnsupported,
181 InvalidContentLength,183 InvalidContentLength,
182 CompressionNotSupported,184 CompressionUnsupported,
183 };185 };
184186
185 pub fn parse(req: *Request, bytes: []const u8) ParseError!void {187 pub fn parse(req: *Request, bytes: []const u8) ParseError!void {
...@@ -189,13 +191,15 @@ pub const Request = struct {...@@ -189,13 +191,15 @@ pub const Request = struct {
189 if (first_line.len < 10)191 if (first_line.len < 10)
190 return error.HttpHeadersInvalid;192 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;
193 if (method_end > 24) return error.HttpHeadersInvalid;196 if (method_end > 24) return error.HttpHeadersInvalid;
194197
195 const method_str = first_line[0..method_end];198 const method_str = first_line[0..method_end];
196 const method: http.Method = @enumFromInt(http.Method.parse(method_str));199 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;
199 if (version_start == method_end) return error.HttpHeadersInvalid;203 if (version_start == method_end) return error.HttpHeadersInvalid;
200204
201 const version_str = first_line[version_start + 1 ..];205 const version_str = first_line[version_start + 1 ..];
...@@ -223,11 +227,26 @@ pub const Request = struct {...@@ -223,11 +227,26 @@ pub const Request = struct {
223 const header_name = line_it.next() orelse return error.HttpHeadersInvalid;227 const header_name = line_it.next() orelse return error.HttpHeadersInvalid;
224 const header_value = line_it.rest();228 const header_value = line_it.rest();
225229
226 try req.headers.append(header_name, header_value);230 if (std.ascii.eqlIgnoreCase(header_name, "connection")) {
227231 req.keep_alive = !std.ascii.eqlIgnoreCase(header_value, "close");
228 if (std.ascii.eqlIgnoreCase(header_name, "content-length")) {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")) {
229 if (req.content_length != null) return error.HttpHeadersInvalid;237 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 }
231 } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) {250 } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) {
232 // Transfer-Encoding: second, first251 // Transfer-Encoding: second, first
233 // Transfer-Encoding: deflate, chunked252 // Transfer-Encoding: deflate, chunked
...@@ -238,7 +257,8 @@ pub const Request = struct {...@@ -238,7 +257,8 @@ pub const Request = struct {
238257
239 var next: ?[]const u8 = first;258 var next: ?[]const u8 = first;
240 if (std.meta.stringToEnum(http.TransferEncoding, trimmed_first)) |transfer| {259 if (std.meta.stringToEnum(http.TransferEncoding, trimmed_first)) |transfer| {
241 if (req.transfer_encoding != .none) return error.HttpHeadersInvalid; // we already have a transfer encoding260 if (req.transfer_encoding != .none)
261 return error.HttpHeadersInvalid; // we already have a transfer encoding
242 req.transfer_encoding = transfer;262 req.transfer_encoding = transfer;
243263
244 next = iter.next();264 next = iter.next();
...@@ -248,7 +268,8 @@ pub const Request = struct {...@@ -248,7 +268,8 @@ pub const Request = struct {
248 const trimmed_second = mem.trim(u8, second, " ");268 const trimmed_second = mem.trim(u8, second, " ");
249269
250 if (std.meta.stringToEnum(http.ContentEncoding, trimmed_second)) |transfer| {270 if (std.meta.stringToEnum(http.ContentEncoding, trimmed_second)) |transfer| {
251 if (req.transfer_compression != .identity) return error.HttpHeadersInvalid; // double compression is not supported271 if (req.transfer_compression != .identity)
272 return error.HttpHeadersInvalid; // double compression is not supported
252 req.transfer_compression = transfer;273 req.transfer_compression = transfer;
253 } else {274 } else {
254 return error.HttpTransferEncodingUnsupported;275 return error.HttpTransferEncodingUnsupported;
...@@ -256,45 +277,23 @@ pub const Request = struct {...@@ -256,45 +277,23 @@ pub const Request = struct {
256 }277 }
257278
258 if (iter.next()) |_| return error.HttpTransferEncodingUnsupported;279 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 }
269 }280 }
270 }281 }
271 }282 }
272283
273 inline fn int64(array: *const [8]u8) u64 {284 inline fn int64(array: *const [8]u8) u64 {
274 return @as(u64, @bitCast(array.*));285 return @bitCast(array.*);
275 }286 }
276287
277 /// The HTTP request method.
278 method: http.Method,288 method: http.Method,
279
280 /// The HTTP request target.
281 target: []const u8,289 target: []const u8,
282
283 /// The HTTP version of this request.
284 version: http.Version,290 version: http.Version,
285291 expect: ?[]const u8 = null,
286 /// The length of the request body, if known.292 content_type: ?[]const u8 = null,
287 content_length: ?u64 = null,293 content_length: ?u64 = null,
288
289 /// The transfer encoding of the request body, or .none if not present.
290 transfer_encoding: http.TransferEncoding = .none,294 transfer_encoding: http.TransferEncoding = .none,
291
292 /// The compression of the request body, or .identity (no compression) if not present.
293 transfer_compression: http.ContentEncoding = .identity,295 transfer_compression: http.ContentEncoding = .identity,
294296 keep_alive: bool = false,
295 /// The list of HTTP request headers
296 headers: http.Headers,
297
298 parser: proto.HeadersParser,297 parser: proto.HeadersParser,
299 compression: Compression = .none,298 compression: Compression = .none,
300};299};
...@@ -311,11 +310,8 @@ pub const Response = struct {...@@ -311,11 +310,8 @@ pub const Response = struct {
311 version: http.Version = .@"HTTP/1.1",310 version: http.Version = .@"HTTP/1.1",
312 status: http.Status = .ok,311 status: http.Status = .ok,
313 reason: ?[]const u8 = null,312 reason: ?[]const u8 = null,
314313 transfer_encoding: ResponseTransfer,
315 transfer_encoding: ResponseTransfer = .none,314 keep_alive: bool,
316
317 /// The allocator responsible for allocating memory for this response.
318 allocator: Allocator,
319315
320 /// The peer's address316 /// The peer's address
321 address: net.Address,317 address: net.Address,
...@@ -323,8 +319,8 @@ pub const Response = struct {...@@ -323,8 +319,8 @@ pub const Response = struct {
323 /// The underlying connection for this response.319 /// The underlying connection for this response.
324 connection: Connection,320 connection: Connection,
325321
326 /// The HTTP response headers322 /// Externally-owned; must outlive the Response.
327 headers: http.Headers,323 extra_headers: []const http.Header = &.{},
328324
329 /// The HTTP request that this response is responding to.325 /// The HTTP request that this response is responding to.
330 ///326 ///
...@@ -333,7 +329,7 @@ pub const Response = struct {...@@ -333,7 +329,7 @@ pub const Response = struct {
333329
334 state: State = .first,330 state: State = .first,
335331
336 const State = enum {332 pub const State = enum {
337 first,333 first,
338 start,334 start,
339 waited,335 waited,
...@@ -344,14 +340,12 @@ pub const Response = struct {...@@ -344,14 +340,12 @@ pub const Response = struct {
344 /// Free all resources associated with this response.340 /// Free all resources associated with this response.
345 pub fn deinit(res: *Response) void {341 pub fn deinit(res: *Response) void {
346 res.connection.close();342 res.connection.close();
347
348 res.headers.deinit();
349 res.request.headers.deinit();
350 }343 }
351344
352 pub const ResetState = enum { reset, closing };345 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.
355 pub fn reset(res: *Response) ResetState {349 pub fn reset(res: *Response) ResetState {
356 if (res.state == .first) {350 if (res.state == .first) {
357 res.state = .start;351 res.state = .start;
...@@ -364,27 +358,11 @@ pub const Response = struct {...@@ -364,27 +358,11 @@ pub const Response = struct {
364 return .closing;358 return .closing;
365 }359 }
366360
367 // A connection is only keep-alive if the Connection header is present and it's value is not "close".361 // A connection is only keep-alive if the Connection header is present
368 // The server and client must both agree362 // and its value is not "close". The server and client must both agree.
369 //363 //
370 // send() defaults to using keep-alive if the client requests it.364 // send() defaults to using keep-alive if the client requests it.
371 const res_connection = res.headers.getFirstValue("connection");365 res.connection.closing = !res.keep_alive or !res.request.keep_alive;
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 }
388366
389 res.state = .start;367 res.state = .start;
390 res.version = .@"HTTP/1.1";368 res.version = .@"HTTP/1.1";
...@@ -393,27 +371,22 @@ pub const Response = struct {...@@ -393,27 +371,22 @@ pub const Response = struct {
393371
394 res.transfer_encoding = .none;372 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
399 res.request.parser.reset();374 res.request.parser.reset();
400375
401 res.request = Request{376 res.request = .{
402 .version = undefined,377 .version = undefined,
403 .method = undefined,378 .method = undefined,
404 .target = undefined,379 .target = undefined,
405 .headers = res.request.headers,
406 .parser = res.request.parser,380 .parser = res.request.parser,
407 };381 };
408382
409 if (res.connection.closing) {383 return if (res.connection.closing) .closing else .reset;
410 return .closing;
411 } else {
412 return .reset;
413 }
414 }384 }
415385
416 pub const SendError = Connection.WriteError || error{ UnsupportedTransferEncoding, InvalidContentLength };386 pub const SendError = Connection.WriteError || error{
387 UnsupportedTransferEncoding,
388 InvalidContentLength,
389 };
417390
418 /// Send the HTTP response headers to the client.391 /// Send the HTTP response headers to the client.
419 pub fn send(res: *Response) SendError!void {392 pub fn send(res: *Response) SendError!void {
...@@ -439,44 +412,21 @@ pub const Response = struct {...@@ -439,44 +412,21 @@ pub const Response = struct {
439 if (res.status == .@"continue") {412 if (res.status == .@"continue") {
440 res.state = .waited; // we still need to send another request after this413 res.state = .waited; // we still need to send another request after this
441 } else {414 } else {
442 if (!res.headers.contains("connection")) {415 if (res.keep_alive and res.request.keep_alive) {
443 const req_connection = res.request.headers.getFirstValue("connection");416 try w.writeAll("connection: keep-alive\r\n");
444 const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?);417 } else {
445418 try w.writeAll("connection: close\r\n");
446 if (req_keepalive) {
447 try w.writeAll("Connection: keep-alive\r\n");
448 } else {
449 try w.writeAll("Connection: close\r\n");
450 }
451 }419 }
452420
453 const has_transfer_encoding = res.headers.contains("transfer-encoding");421 switch (res.transfer_encoding) {
454 const has_content_length = res.headers.contains("content-length");422 .chunked => try w.writeAll("transfer-encoding: chunked\r\n"),
455423 .content_length => |content_length| try w.print("content-length: {d}\r\n", .{content_length}),
456 if (!has_transfer_encoding and !has_content_length) {424 .none => {},
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 }
477 }425 }
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 }
480 }430 }
481431
482 if (res.request.method == .HEAD) {432 if (res.request.method == .HEAD) {
...@@ -511,7 +461,7 @@ pub const Response = struct {...@@ -511,7 +461,7 @@ pub const Response = struct {
511461
512 pub const WaitError = Connection.ReadError ||462 pub const WaitError = Connection.ReadError ||
513 proto.HeadersParser.CheckCompleteHeadError || Request.ParseError ||463 proto.HeadersParser.CheckCompleteHeadError || Request.ParseError ||
514 error{ CompressionInitializationFailed, CompressionNotSupported };464 error{CompressionUnsupported};
515465
516 /// Wait for the client to send a complete request head.466 /// Wait for the client to send a complete request head.
517 ///467 ///
...@@ -545,37 +495,37 @@ pub const Response = struct {...@@ -545,37 +495,37 @@ pub const Response = struct {
545 if (res.request.parser.state.isContent()) break;495 if (res.request.parser.state.isContent()) break;
546 }496 }
547497
548 res.request.headers = .{ .allocator = res.allocator, .owned = true };
549 try res.request.parse(res.request.parser.get());498 try res.request.parse(res.request.parser.get());
550499
551 if (res.request.transfer_encoding != .none) {500 switch (res.request.transfer_encoding) {
552 switch (res.request.transfer_encoding) {501 .none => {
553 .none => unreachable,502 if (res.request.content_length) |len| {
554 .chunked => {503 res.request.parser.next_chunk_length = len;
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;
561504
562 if (cl == 0) res.request.parser.state = .complete;505 if (len == 0) res.request.parser.state = .complete;
563 } else {506 } else {
564 res.request.parser.state = .complete;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 },
565 }514 }
566515
567 if (res.request.parser.state != .complete) {516 if (res.request.parser.state != .complete) {
568 switch (res.request.transfer_compression) {517 switch (res.request.transfer_compression) {
569 .identity => res.request.compression = .none,518 .identity => res.request.compression = .none,
570 .compress, .@"x-compress" => return error.CompressionNotSupported,519 .compress, .@"x-compress" => return error.CompressionUnsupported,
571 .deflate => res.request.compression = .{520 .deflate => res.request.compression = .{
572 .deflate = std.compress.zlib.decompressor(res.transferReader()),521 .deflate = std.compress.zlib.decompressor(res.transferReader()),
573 },522 },
574 .gzip, .@"x-gzip" => res.request.compression = .{523 .gzip, .@"x-gzip" => res.request.compression = .{
575 .gzip = std.compress.gzip.decompressor(res.transferReader()),524 .gzip = std.compress.gzip.decompressor(res.transferReader()),
576 },525 },
577 .zstd => res.request.compression = .{526 .zstd => {
578 .zstd = std.compress.zstd.decompressStream(res.allocator, res.transferReader()),527 // https://github.com/ziglang/zig/issues/18937
528 return error.CompressionUnsupported;
579 },529 },
580 }530 }
581 }531 }
...@@ -599,7 +549,8 @@ pub const Response = struct {...@@ -599,7 +549,8 @@ pub const Response = struct {
599 const out_index = switch (res.request.compression) {549 const out_index = switch (res.request.compression) {
600 .deflate => |*deflate| deflate.read(buffer) catch return error.DecompressionFailure,550 .deflate => |*deflate| deflate.read(buffer) catch return error.DecompressionFailure,
601 .gzip => |*gzip| gzip.read(buffer) catch return error.DecompressionFailure,551 .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,
603 else => try res.transferRead(buffer),554 else => try res.transferRead(buffer),
604 };555 };
605556
...@@ -614,8 +565,6 @@ pub const Response = struct {...@@ -614,8 +565,6 @@ pub const Response = struct {
614 }565 }
615566
616 if (has_trail) {567 if (has_trail) {
617 res.request.headers = http.Headers{ .allocator = res.allocator, .owned = false };
618
619 // The response headers before the trailers are already568 // The response headers before the trailers are already
620 // guaranteed to be valid, so they will always be parsed again569 // guaranteed to be valid, so they will always be parsed again
621 // and cannot return an error.570 // and cannot return an error.
...@@ -736,18 +685,17 @@ pub fn accept(server: *Server, options: AcceptOptions) AcceptError!Response {...@@ -736,18 +685,17 @@ pub fn accept(server: *Server, options: AcceptOptions) AcceptError!Response {
736 const in = try server.socket.accept();685 const in = try server.socket.accept();
737686
738 return .{687 return .{
739 .allocator = options.allocator,688 .transfer_encoding = .none,
689 .keep_alive = true,
740 .address = in.address,690 .address = in.address,
741 .connection = .{691 .connection = .{
742 .stream = in.stream,692 .stream = in.stream,
743 .protocol = .plain,693 .protocol = .plain,
744 },694 },
745 .headers = .{ .allocator = options.allocator },
746 .request = .{695 .request = .{
747 .version = undefined,696 .version = undefined,
748 .method = undefined,697 .method = undefined,
749 .target = undefined,698 .target = undefined,
750 .headers = .{ .allocator = options.allocator, .owned = false },
751 .parser = proto.HeadersParser.init(options.client_header_buffer),699 .parser = proto.HeadersParser.init(options.client_header_buffer),
752 },700 },
753 };701 };
...@@ -793,8 +741,10 @@ test "HTTP server handles a chunked transfer coding request" {...@@ -793,8 +741,10 @@ test "HTTP server handles a chunked transfer coding request" {
793741
794 const server_body: []const u8 = "message from server!\n";742 const server_body: []const u8 = "message from server!\n";
795 res.transfer_encoding = .{ .content_length = server_body.len };743 res.transfer_encoding = .{ .content_length = server_body.len };
796 try res.headers.append("content-type", "text/plain");744 res.extra_headers = &.{
797 try res.headers.append("connection", "close");745 .{ .name = "content-type", .value = "text/plain" },
746 };
747 res.keep_alive = false;
798 try res.send();748 try res.send();
799749
800 var buf: [128]u8 = undefined;750 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...@@ -898,10 +898,7 @@ fn initResource(f: *Fetch, uri: std.Uri, server_header_buffer: []u8) RunError!Re
898 if (ascii.eqlIgnoreCase(uri.scheme, "http") or898 if (ascii.eqlIgnoreCase(uri.scheme, "http") or
899 ascii.eqlIgnoreCase(uri.scheme, "https"))899 ascii.eqlIgnoreCase(uri.scheme, "https"))
900 {900 {
901 var h: std.http.Headers = .{ .allocator = gpa };901 var req = http_client.open(.GET, uri, .{
902 defer h.deinit();
903
904 var req = http_client.open(.GET, uri, h, .{
905 .server_header_buffer = server_header_buffer,902 .server_header_buffer = server_header_buffer,
906 }) catch |err| {903 }) catch |err| {
907 return f.fail(f.location_tok, try eb.printString(904 return f.fail(f.location_tok, try eb.printString(
...@@ -1043,7 +1040,7 @@ fn unpackResource(...@@ -1043,7 +1040,7 @@ fn unpackResource(
10431040
1044 .http_request => |req| ft: {1041 .http_request => |req| ft: {
1045 // Content-Type takes first precedence.1042 // Content-Type takes first precedence.
1046 const content_type = req.response.headers.getFirstValue("Content-Type") orelse1043 const content_type = req.response.content_type orelse
1047 return f.fail(f.location_tok, try eb.addString("missing 'Content-Type' header"));1044 return f.fail(f.location_tok, try eb.addString("missing 'Content-Type' header"));
10481045
1049 // Extract the MIME type, ignoring charset and boundary directives1046 // Extract the MIME type, ignoring charset and boundary directives
...@@ -1076,7 +1073,7 @@ fn unpackResource(...@@ -1076,7 +1073,7 @@ fn unpackResource(
1076 }1073 }
10771074
1078 // Next, the filename from 'content-disposition: attachment' takes precedence.1075 // 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| {
1080 break :ft FileType.fromContentDisposition(cd_header) orelse {1077 break :ft FileType.fromContentDisposition(cd_header) orelse {
1081 return f.fail(f.location_tok, try eb.printString(1078 return f.fail(f.location_tok, try eb.printString(
1082 "unsupported Content-Disposition header value: '{s}' for Content-Type=application/octet-stream",1079 "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 {...@@ -530,13 +530,12 @@ pub const Session = struct {
530 info_refs_uri.query = "service=git-upload-pack";530 info_refs_uri.query = "service=git-upload-pack";
531 info_refs_uri.fragment = null;531 info_refs_uri.fragment = null;
532532
533 var headers = std.http.Headers.init(allocator);533 var request = try session.transport.open(.GET, info_refs_uri, .{
534 defer headers.deinit();534 .redirect_behavior = @enumFromInt(3),
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,
539 .server_header_buffer = http_headers_buffer,535 .server_header_buffer = http_headers_buffer,
536 .extra_headers = &.{
537 .{ .name = "Git-Protocol", .value = "version=2" },
538 },
540 });539 });
541 errdefer request.deinit();540 errdefer request.deinit();
542 try request.send(.{});541 try request.send(.{});
...@@ -544,7 +543,12 @@ pub const Session = struct {...@@ -544,7 +543,12 @@ pub const Session = struct {
544543
545 try request.wait();544 try request.wait();
546 if (request.response.status != .ok) return error.ProtocolError;545 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) {
548 if (!mem.endsWith(u8, request.uri.path, "/info/refs")) return error.UnparseableRedirect;552 if (!mem.endsWith(u8, request.uri.path, "/info/refs")) return error.UnparseableRedirect;
549 var new_uri = request.uri;553 var new_uri = request.uri;
550 new_uri.path = new_uri.path[0 .. new_uri.path.len - "/info/refs".len];554 new_uri.path = new_uri.path[0 .. new_uri.path.len - "/info/refs".len];
...@@ -634,11 +638,6 @@ pub const Session = struct {...@@ -634,11 +638,6 @@ pub const Session = struct {
634 upload_pack_uri.query = null;638 upload_pack_uri.query = null;
635 upload_pack_uri.fragment = null;639 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
642 var body = std.ArrayListUnmanaged(u8){};641 var body = std.ArrayListUnmanaged(u8){};
643 defer body.deinit(allocator);642 defer body.deinit(allocator);
644 const body_writer = body.writer(allocator);643 const body_writer = body.writer(allocator);
...@@ -660,9 +659,13 @@ pub const Session = struct {...@@ -660,9 +659,13 @@ pub const Session = struct {
660 }659 }
661 try Packet.write(.flush, body_writer);660 try Packet.write(.flush, body_writer);
662661
663 var request = try session.transport.open(.POST, upload_pack_uri, headers, .{662 var request = try session.transport.open(.POST, upload_pack_uri, .{
664 .handle_redirects = false,663 .redirect_behavior = .unhandled,
665 .server_header_buffer = options.server_header_buffer,664 .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 },
666 });669 });
667 errdefer request.deinit();670 errdefer request.deinit();
668 request.transfer_encoding = .{ .content_length = body.items.len };671 request.transfer_encoding = .{ .content_length = body.items.len };
...@@ -738,11 +741,6 @@ pub const Session = struct {...@@ -738,11 +741,6 @@ pub const Session = struct {
738 upload_pack_uri.query = null;741 upload_pack_uri.query = null;
739 upload_pack_uri.fragment = null;742 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
746 var body = std.ArrayListUnmanaged(u8){};744 var body = std.ArrayListUnmanaged(u8){};
747 defer body.deinit(allocator);745 defer body.deinit(allocator);
748 const body_writer = body.writer(allocator);746 const body_writer = body.writer(allocator);
...@@ -766,9 +764,13 @@ pub const Session = struct {...@@ -766,9 +764,13 @@ pub const Session = struct {
766 try Packet.write(.{ .data = "done\n" }, body_writer);764 try Packet.write(.{ .data = "done\n" }, body_writer);
767 try Packet.write(.flush, body_writer);765 try Packet.write(.flush, body_writer);
768766
769 var request = try session.transport.open(.POST, upload_pack_uri, headers, .{767 var request = try session.transport.open(.POST, upload_pack_uri, .{
770 .handle_redirects = false,768 .redirect_behavior = .not_allowed,
771 .server_header_buffer = http_headers_buffer,769 .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 },
772 });774 });
773 errdefer request.deinit();775 errdefer request.deinit();
774 request.transfer_encoding = .{ .content_length = body.items.len };776 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 {...@@ -5486,7 +5486,7 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
5486 job_queue.read_only = true;5486 job_queue.read_only = true;
5487 cleanup_build_dir = job_queue.global_cache.handle;5487 cleanup_build_dir = job_queue.global_cache.handle;
5488 } else {5488 } else {
5489 try http_client.loadDefaultProxies();5489 try http_client.initDefaultProxies(arena);
5490 }5490 }
54915491
5492 try job_queue.all_fetches.ensureUnusedCapacity(gpa, 1);5492 try job_queue.all_fetches.ensureUnusedCapacity(gpa, 1);
...@@ -7442,7 +7442,7 @@ fn cmdFetch(...@@ -7442,7 +7442,7 @@ fn cmdFetch(
7442 var http_client: std.http.Client = .{ .allocator = gpa };7442 var http_client: std.http.Client = .{ .allocator = gpa };
7443 defer http_client.deinit();7443 defer http_client.deinit();
74447444
7445 try http_client.loadDefaultProxies();7445 try http_client.initDefaultProxies(arena);
74467446
7447 var progress: std.Progress = .{ .dont_print_on_dumb = true };7447 var progress: std.Progress = .{ .dont_print_on_dumb = true };
7448 const root_prog_node = progress.start("Fetch", 0);7448 const root_prog_node = progress.start("Fetch", 0);
test/standalone/http.zig+79-109
...@@ -26,8 +26,8 @@ fn handleRequest(res: *Server.Response) !void {...@@ -26,8 +26,8 @@ fn handleRequest(res: *Server.Response) !void {
2626
27 log.info("{} {s} {s}", .{ res.request.method, @tagName(res.request.version), res.request.target });27 log.info("{} {s} {s}", .{ res.request.method, @tagName(res.request.version), res.request.target });
2828
29 if (res.request.headers.contains("expect")) {29 if (res.request.expect) |expect| {
30 if (mem.eql(u8, res.request.headers.getFirstValue("expect").?, "100-continue")) {30 if (mem.eql(u8, expect, "100-continue")) {
31 res.status = .@"continue";31 res.status = .@"continue";
32 try res.send();32 try res.send();
33 res.status = .ok;33 res.status = .ok;
...@@ -41,8 +41,8 @@ fn handleRequest(res: *Server.Response) !void {...@@ -41,8 +41,8 @@ fn handleRequest(res: *Server.Response) !void {
41 const body = try res.reader().readAllAlloc(salloc, 8192);41 const body = try res.reader().readAllAlloc(salloc, 8192);
42 defer salloc.free(body);42 defer salloc.free(body);
4343
44 if (res.request.headers.contains("connection")) {44 if (res.request.keep_alive) {
45 try res.headers.append("connection", "keep-alive");45 res.keep_alive = true;
46 }46 }
4747
48 if (mem.startsWith(u8, res.request.target, "/get")) {48 if (mem.startsWith(u8, res.request.target, "/get")) {
...@@ -52,7 +52,9 @@ fn handleRequest(res: *Server.Response) !void {...@@ -52,7 +52,9 @@ fn handleRequest(res: *Server.Response) !void {
52 res.transfer_encoding = .{ .content_length = 14 };52 res.transfer_encoding = .{ .content_length = 14 };
53 }53 }
5454
55 try res.headers.append("content-type", "text/plain");55 res.extra_headers = &.{
56 .{ .name = "content-type", .value = "text/plain" },
57 };
5658
57 try res.send();59 try res.send();
58 if (res.request.method != .HEAD) {60 if (res.request.method != .HEAD) {
...@@ -82,14 +84,14 @@ fn handleRequest(res: *Server.Response) !void {...@@ -82,14 +84,14 @@ fn handleRequest(res: *Server.Response) !void {
82 try res.finish();84 try res.finish();
83 } else if (mem.startsWith(u8, res.request.target, "/echo-content")) {85 } else if (mem.startsWith(u8, res.request.target, "/echo-content")) {
84 try testing.expectEqualStrings("Hello, World!\n", body);86 try testing.expectEqualStrings("Hello, World!\n", body);
85 try testing.expectEqualStrings("text/plain", res.request.headers.getFirstValue("content-type").?);87 try testing.expectEqualStrings("text/plain", res.request.content_type.?);
8688
87 if (res.request.headers.contains("transfer-encoding")) {89 switch (res.request.transfer_encoding) {
88 try testing.expectEqualStrings("chunked", res.request.headers.getFirstValue("transfer-encoding").?);90 .chunked => res.transfer_encoding = .chunked,
89 res.transfer_encoding = .chunked;91 .none => {
90 } else {92 res.transfer_encoding = .{ .content_length = 14 };
91 res.transfer_encoding = .{ .content_length = 14 };93 try testing.expectEqual(14, res.request.content_length.?);
92 try testing.expectEqualStrings("14", res.request.headers.getFirstValue("content-length").?);94 },
93 }95 }
9496
95 try res.send();97 try res.send();
...@@ -108,7 +110,9 @@ fn handleRequest(res: *Server.Response) !void {...@@ -108,7 +110,9 @@ fn handleRequest(res: *Server.Response) !void {
108 res.transfer_encoding = .chunked;110 res.transfer_encoding = .chunked;
109111
110 res.status = .found;112 res.status = .found;
111 try res.headers.append("location", "../../get");113 res.extra_headers = &.{
114 .{ .name = "location", .value = "../../get" },
115 };
112116
113 try res.send();117 try res.send();
114 try res.writeAll("Hello, ");118 try res.writeAll("Hello, ");
...@@ -118,7 +122,9 @@ fn handleRequest(res: *Server.Response) !void {...@@ -118,7 +122,9 @@ fn handleRequest(res: *Server.Response) !void {
118 res.transfer_encoding = .chunked;122 res.transfer_encoding = .chunked;
119123
120 res.status = .found;124 res.status = .found;
121 try res.headers.append("location", "/redirect/1");125 res.extra_headers = &.{
126 .{ .name = "location", .value = "/redirect/1" },
127 };
122128
123 try res.send();129 try res.send();
124 try res.writeAll("Hello, ");130 try res.writeAll("Hello, ");
...@@ -131,7 +137,9 @@ fn handleRequest(res: *Server.Response) !void {...@@ -131,7 +137,9 @@ fn handleRequest(res: *Server.Response) !void {
131 defer salloc.free(location);137 defer salloc.free(location);
132138
133 res.status = .found;139 res.status = .found;
134 try res.headers.append("location", location);140 res.extra_headers = &.{
141 .{ .name = "location", .value = location },
142 };
135143
136 try res.send();144 try res.send();
137 try res.writeAll("Hello, ");145 try res.writeAll("Hello, ");
...@@ -141,7 +149,9 @@ fn handleRequest(res: *Server.Response) !void {...@@ -141,7 +149,9 @@ fn handleRequest(res: *Server.Response) !void {
141 res.transfer_encoding = .chunked;149 res.transfer_encoding = .chunked;
142150
143 res.status = .found;151 res.status = .found;
144 try res.headers.append("location", "/redirect/3");152 res.extra_headers = &.{
153 .{ .name = "location", .value = "/redirect/3" },
154 };
145155
146 try res.send();156 try res.send();
147 try res.writeAll("Hello, ");157 try res.writeAll("Hello, ");
...@@ -153,7 +163,9 @@ fn handleRequest(res: *Server.Response) !void {...@@ -153,7 +163,9 @@ fn handleRequest(res: *Server.Response) !void {
153 defer salloc.free(location);163 defer salloc.free(location);
154164
155 res.status = .found;165 res.status = .found;
156 try res.headers.append("location", location);166 res.extra_headers = &.{
167 .{ .name = "location", .value = location },
168 };
157 try res.send();169 try res.send();
158 try res.finish();170 try res.finish();
159 } else {171 } else {
...@@ -234,19 +246,20 @@ pub fn main() !void {...@@ -234,19 +246,20 @@ pub fn main() !void {
234 errdefer client.deinit();246 errdefer client.deinit();
235 // defer client.deinit(); handled below247 // 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 response253 try client.initDefaultProxies(arena);
240 var h = http.Headers{ .allocator = calloc };
241 defer h.deinit();
242254
255 { // read content-length response
243 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get", .{port});256 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get", .{port});
244 defer calloc.free(location);257 defer calloc.free(location);
245 const uri = try std.Uri.parse(location);258 const uri = try std.Uri.parse(location);
246259
247 log.info("{s}", .{location});260 log.info("{s}", .{location});
248 var server_header_buffer: [1024]u8 = undefined;261 var server_header_buffer: [1024]u8 = undefined;
249 var req = try client.open(.GET, uri, h, .{262 var req = try client.open(.GET, uri, .{
250 .server_header_buffer = &server_header_buffer,263 .server_header_buffer = &server_header_buffer,
251 });264 });
252 defer req.deinit();265 defer req.deinit();
...@@ -258,23 +271,20 @@ pub fn main() !void {...@@ -258,23 +271,20 @@ pub fn main() !void {
258 defer calloc.free(body);271 defer calloc.free(body);
259272
260 try testing.expectEqualStrings("Hello, World!\n", body);273 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.?);
262 }275 }
263276
264 // connection has been kept alive277 // connection has been kept alive
265 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);278 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
266279
267 { // read large content-length response280 { // read large content-length response
268 var h = http.Headers{ .allocator = calloc };
269 defer h.deinit();
270
271 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/large", .{port});281 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/large", .{port});
272 defer calloc.free(location);282 defer calloc.free(location);
273 const uri = try std.Uri.parse(location);283 const uri = try std.Uri.parse(location);
274284
275 log.info("{s}", .{location});285 log.info("{s}", .{location});
276 var server_header_buffer: [1024]u8 = undefined;286 var server_header_buffer: [1024]u8 = undefined;
277 var req = try client.open(.GET, uri, h, .{287 var req = try client.open(.GET, uri, .{
278 .server_header_buffer = &server_header_buffer,288 .server_header_buffer = &server_header_buffer,
279 });289 });
280 defer req.deinit();290 defer req.deinit();
...@@ -292,16 +302,13 @@ pub fn main() !void {...@@ -292,16 +302,13 @@ pub fn main() !void {
292 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);302 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
293303
294 { // send head request and not read chunked304 { // send head request and not read chunked
295 var h = http.Headers{ .allocator = calloc };
296 defer h.deinit();
297
298 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get", .{port});305 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get", .{port});
299 defer calloc.free(location);306 defer calloc.free(location);
300 const uri = try std.Uri.parse(location);307 const uri = try std.Uri.parse(location);
301308
302 log.info("{s}", .{location});309 log.info("{s}", .{location});
303 var server_header_buffer: [1024]u8 = undefined;310 var server_header_buffer: [1024]u8 = undefined;
304 var req = try client.open(.HEAD, uri, h, .{311 var req = try client.open(.HEAD, uri, .{
305 .server_header_buffer = &server_header_buffer,312 .server_header_buffer = &server_header_buffer,
306 });313 });
307 defer req.deinit();314 defer req.deinit();
...@@ -313,24 +320,21 @@ pub fn main() !void {...@@ -313,24 +320,21 @@ pub fn main() !void {
313 defer calloc.free(body);320 defer calloc.free(body);
314321
315 try testing.expectEqualStrings("", body);322 try testing.expectEqualStrings("", body);
316 try testing.expectEqualStrings("text/plain", req.response.headers.getFirstValue("content-type").?);323 try testing.expectEqualStrings("text/plain", req.response.content_type.?);
317 try testing.expectEqualStrings("14", req.response.headers.getFirstValue("content-length").?);324 try testing.expectEqual(14, req.response.content_length.?);
318 }325 }
319326
320 // connection has been kept alive327 // connection has been kept alive
321 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);328 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
322329
323 { // read chunked response330 { // read chunked response
324 var h = http.Headers{ .allocator = calloc };
325 defer h.deinit();
326
327 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get?chunked", .{port});331 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get?chunked", .{port});
328 defer calloc.free(location);332 defer calloc.free(location);
329 const uri = try std.Uri.parse(location);333 const uri = try std.Uri.parse(location);
330334
331 log.info("{s}", .{location});335 log.info("{s}", .{location});
332 var server_header_buffer: [1024]u8 = undefined;336 var server_header_buffer: [1024]u8 = undefined;
333 var req = try client.open(.GET, uri, h, .{337 var req = try client.open(.GET, uri, .{
334 .server_header_buffer = &server_header_buffer,338 .server_header_buffer = &server_header_buffer,
335 });339 });
336 defer req.deinit();340 defer req.deinit();
...@@ -342,23 +346,20 @@ pub fn main() !void {...@@ -342,23 +346,20 @@ pub fn main() !void {
342 defer calloc.free(body);346 defer calloc.free(body);
343347
344 try testing.expectEqualStrings("Hello, World!\n", body);348 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.?);
346 }350 }
347351
348 // connection has been kept alive352 // connection has been kept alive
349 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);353 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
350354
351 { // send head request and not read chunked355 { // send head request and not read chunked
352 var h = http.Headers{ .allocator = calloc };
353 defer h.deinit();
354
355 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get?chunked", .{port});356 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get?chunked", .{port});
356 defer calloc.free(location);357 defer calloc.free(location);
357 const uri = try std.Uri.parse(location);358 const uri = try std.Uri.parse(location);
358359
359 log.info("{s}", .{location});360 log.info("{s}", .{location});
360 var server_header_buffer: [1024]u8 = undefined;361 var server_header_buffer: [1024]u8 = undefined;
361 var req = try client.open(.HEAD, uri, h, .{362 var req = try client.open(.HEAD, uri, .{
362 .server_header_buffer = &server_header_buffer,363 .server_header_buffer = &server_header_buffer,
363 });364 });
364 defer req.deinit();365 defer req.deinit();
...@@ -370,24 +371,21 @@ pub fn main() !void {...@@ -370,24 +371,21 @@ pub fn main() !void {
370 defer calloc.free(body);371 defer calloc.free(body);
371372
372 try testing.expectEqualStrings("", body);373 try testing.expectEqualStrings("", body);
373 try testing.expectEqualStrings("text/plain", req.response.headers.getFirstValue("content-type").?);374 try testing.expectEqualStrings("text/plain", req.response.content_type.?);
374 try testing.expectEqualStrings("chunked", req.response.headers.getFirstValue("transfer-encoding").?);375 try testing.expect(req.response.transfer_encoding == .chunked);
375 }376 }
376377
377 // connection has been kept alive378 // connection has been kept alive
378 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);379 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
379380
380 { // check trailing headers381 { // check trailing headers
381 var h = http.Headers{ .allocator = calloc };
382 defer h.deinit();
383
384 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/trailer", .{port});382 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/trailer", .{port});
385 defer calloc.free(location);383 defer calloc.free(location);
386 const uri = try std.Uri.parse(location);384 const uri = try std.Uri.parse(location);
387385
388 log.info("{s}", .{location});386 log.info("{s}", .{location});
389 var server_header_buffer: [1024]u8 = undefined;387 var server_header_buffer: [1024]u8 = undefined;
390 var req = try client.open(.GET, uri, h, .{388 var req = try client.open(.GET, uri, .{
391 .server_header_buffer = &server_header_buffer,389 .server_header_buffer = &server_header_buffer,
392 });390 });
393 defer req.deinit();391 defer req.deinit();
...@@ -399,26 +397,25 @@ pub fn main() !void {...@@ -399,26 +397,25 @@ pub fn main() !void {
399 defer calloc.free(body);397 defer calloc.free(body);
400398
401 try testing.expectEqualStrings("Hello, World!\n", body);399 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").?);
403 }402 }
404403
405 // connection has been kept alive404 // connection has been kept alive
406 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);405 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
407406
408 { // send content-length request407 { // 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
414 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/echo-content", .{port});408 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/echo-content", .{port});
415 defer calloc.free(location);409 defer calloc.free(location);
416 const uri = try std.Uri.parse(location);410 const uri = try std.Uri.parse(location);
417411
418 log.info("{s}", .{location});412 log.info("{s}", .{location});
419 var server_header_buffer: [1024]u8 = undefined;413 var server_header_buffer: [1024]u8 = undefined;
420 var req = try client.open(.POST, uri, h, .{414 var req = try client.open(.POST, uri, .{
421 .server_header_buffer = &server_header_buffer,415 .server_header_buffer = &server_header_buffer,
416 .extra_headers = &.{
417 .{ .name = "content-type", .value = "text/plain" },
418 },
422 });419 });
423 defer req.deinit();420 defer req.deinit();
424421
...@@ -441,19 +438,15 @@ pub fn main() !void {...@@ -441,19 +438,15 @@ pub fn main() !void {
441 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);438 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
442439
443 { // read content-length response with connection close440 { // 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
449 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get", .{port});441 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get", .{port});
450 defer calloc.free(location);442 defer calloc.free(location);
451 const uri = try std.Uri.parse(location);443 const uri = try std.Uri.parse(location);
452444
453 log.info("{s}", .{location});445 log.info("{s}", .{location});
454 var server_header_buffer: [1024]u8 = undefined;446 var server_header_buffer: [1024]u8 = undefined;
455 var req = try client.open(.GET, uri, h, .{447 var req = try client.open(.GET, uri, .{
456 .server_header_buffer = &server_header_buffer,448 .server_header_buffer = &server_header_buffer,
449 .keep_alive = false,
457 });450 });
458 defer req.deinit();451 defer req.deinit();
459452
...@@ -464,26 +457,24 @@ pub fn main() !void {...@@ -464,26 +457,24 @@ pub fn main() !void {
464 defer calloc.free(body);457 defer calloc.free(body);
465458
466 try testing.expectEqualStrings("Hello, World!\n", body);459 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.?);
468 }461 }
469462
470 // connection has been closed463 // connection has been closed
471 try testing.expect(client.connection_pool.free_len == 0);464 try testing.expect(client.connection_pool.free_len == 0);
472465
473 { // send chunked request466 { // send chunked request
474 var h = http.Headers{ .allocator = calloc };
475 defer h.deinit();
476
477 try h.append("content-type", "text/plain");
478
479 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/echo-content", .{port});467 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/echo-content", .{port});
480 defer calloc.free(location);468 defer calloc.free(location);
481 const uri = try std.Uri.parse(location);469 const uri = try std.Uri.parse(location);
482470
483 log.info("{s}", .{location});471 log.info("{s}", .{location});
484 var server_header_buffer: [1024]u8 = undefined;472 var server_header_buffer: [1024]u8 = undefined;
485 var req = try client.open(.POST, uri, h, .{473 var req = try client.open(.POST, uri, .{
486 .server_header_buffer = &server_header_buffer,474 .server_header_buffer = &server_header_buffer,
475 .extra_headers = &.{
476 .{ .name = "content-type", .value = "text/plain" },
477 },
487 });478 });
488 defer req.deinit();479 defer req.deinit();
489480
...@@ -506,16 +497,13 @@ pub fn main() !void {...@@ -506,16 +497,13 @@ pub fn main() !void {
506 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);497 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
507498
508 { // relative redirect499 { // relative redirect
509 var h = http.Headers{ .allocator = calloc };
510 defer h.deinit();
511
512 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/1", .{port});500 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/1", .{port});
513 defer calloc.free(location);501 defer calloc.free(location);
514 const uri = try std.Uri.parse(location);502 const uri = try std.Uri.parse(location);
515503
516 log.info("{s}", .{location});504 log.info("{s}", .{location});
517 var server_header_buffer: [1024]u8 = undefined;505 var server_header_buffer: [1024]u8 = undefined;
518 var req = try client.open(.GET, uri, h, .{506 var req = try client.open(.GET, uri, .{
519 .server_header_buffer = &server_header_buffer,507 .server_header_buffer = &server_header_buffer,
520 });508 });
521 defer req.deinit();509 defer req.deinit();
...@@ -533,16 +521,13 @@ pub fn main() !void {...@@ -533,16 +521,13 @@ pub fn main() !void {
533 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);521 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
534522
535 { // redirect from root523 { // redirect from root
536 var h = http.Headers{ .allocator = calloc };
537 defer h.deinit();
538
539 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/2", .{port});524 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/2", .{port});
540 defer calloc.free(location);525 defer calloc.free(location);
541 const uri = try std.Uri.parse(location);526 const uri = try std.Uri.parse(location);
542527
543 log.info("{s}", .{location});528 log.info("{s}", .{location});
544 var server_header_buffer: [1024]u8 = undefined;529 var server_header_buffer: [1024]u8 = undefined;
545 var req = try client.open(.GET, uri, h, .{530 var req = try client.open(.GET, uri, .{
546 .server_header_buffer = &server_header_buffer,531 .server_header_buffer = &server_header_buffer,
547 });532 });
548 defer req.deinit();533 defer req.deinit();
...@@ -560,16 +545,13 @@ pub fn main() !void {...@@ -560,16 +545,13 @@ pub fn main() !void {
560 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);545 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
561546
562 { // absolute redirect547 { // absolute redirect
563 var h = http.Headers{ .allocator = calloc };
564 defer h.deinit();
565
566 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/3", .{port});548 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/3", .{port});
567 defer calloc.free(location);549 defer calloc.free(location);
568 const uri = try std.Uri.parse(location);550 const uri = try std.Uri.parse(location);
569551
570 log.info("{s}", .{location});552 log.info("{s}", .{location});
571 var server_header_buffer: [1024]u8 = undefined;553 var server_header_buffer: [1024]u8 = undefined;
572 var req = try client.open(.GET, uri, h, .{554 var req = try client.open(.GET, uri, .{
573 .server_header_buffer = &server_header_buffer,555 .server_header_buffer = &server_header_buffer,
574 });556 });
575 defer req.deinit();557 defer req.deinit();
...@@ -587,16 +569,13 @@ pub fn main() !void {...@@ -587,16 +569,13 @@ pub fn main() !void {
587 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);569 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
588570
589 { // too many redirects571 { // too many redirects
590 var h = http.Headers{ .allocator = calloc };
591 defer h.deinit();
592
593 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/4", .{port});572 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/4", .{port});
594 defer calloc.free(location);573 defer calloc.free(location);
595 const uri = try std.Uri.parse(location);574 const uri = try std.Uri.parse(location);
596575
597 log.info("{s}", .{location});576 log.info("{s}", .{location});
598 var server_header_buffer: [1024]u8 = undefined;577 var server_header_buffer: [1024]u8 = undefined;
599 var req = try client.open(.GET, uri, h, .{578 var req = try client.open(.GET, uri, .{
600 .server_header_buffer = &server_header_buffer,579 .server_header_buffer = &server_header_buffer,
601 });580 });
602 defer req.deinit();581 defer req.deinit();
...@@ -612,16 +591,13 @@ pub fn main() !void {...@@ -612,16 +591,13 @@ pub fn main() !void {
612 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);591 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
613592
614 { // check client without segfault by connection error after redirection593 { // check client without segfault by connection error after redirection
615 var h = http.Headers{ .allocator = calloc };
616 defer h.deinit();
617
618 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/invalid", .{port});594 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/redirect/invalid", .{port});
619 defer calloc.free(location);595 defer calloc.free(location);
620 const uri = try std.Uri.parse(location);596 const uri = try std.Uri.parse(location);
621597
622 log.info("{s}", .{location});598 log.info("{s}", .{location});
623 var server_header_buffer: [1024]u8 = undefined;599 var server_header_buffer: [1024]u8 = undefined;
624 var req = try client.open(.GET, uri, h, .{600 var req = try client.open(.GET, uri, .{
625 .server_header_buffer = &server_header_buffer,601 .server_header_buffer = &server_header_buffer,
626 });602 });
627 defer req.deinit();603 defer req.deinit();
...@@ -639,10 +615,6 @@ pub fn main() !void {...@@ -639,10 +615,6 @@ pub fn main() !void {
639 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);615 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
640616
641 { // Client.fetch()617 { // Client.fetch()
642 var h = http.Headers{ .allocator = calloc };
643 defer h.deinit();
644
645 try h.append("content-type", "text/plain");
646618
647 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/echo-content#fetch", .{port});619 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/echo-content#fetch", .{port});
648 defer calloc.free(location);620 defer calloc.free(location);
...@@ -651,8 +623,10 @@ pub fn main() !void {...@@ -651,8 +623,10 @@ pub fn main() !void {
651 var res = try client.fetch(calloc, .{623 var res = try client.fetch(calloc, .{
652 .location = .{ .url = location },624 .location = .{ .url = location },
653 .method = .POST,625 .method = .POST,
654 .headers = h,
655 .payload = .{ .string = "Hello, World!\n" },626 .payload = .{ .string = "Hello, World!\n" },
627 .extra_headers = &.{
628 .{ .name = "content-type", .value = "text/plain" },
629 },
656 });630 });
657 defer res.deinit();631 defer res.deinit();
658632
...@@ -660,20 +634,18 @@ pub fn main() !void {...@@ -660,20 +634,18 @@ pub fn main() !void {
660 }634 }
661635
662 { // expect: 100-continue636 { // 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
669 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/echo-content#expect-100", .{port});637 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/echo-content#expect-100", .{port});
670 defer calloc.free(location);638 defer calloc.free(location);
671 const uri = try std.Uri.parse(location);639 const uri = try std.Uri.parse(location);
672640
673 log.info("{s}", .{location});641 log.info("{s}", .{location});
674 var server_header_buffer: [1024]u8 = undefined;642 var server_header_buffer: [1024]u8 = undefined;
675 var req = try client.open(.POST, uri, h, .{643 var req = try client.open(.POST, uri, .{
676 .server_header_buffer = &server_header_buffer,644 .server_header_buffer = &server_header_buffer,
645 .extra_headers = &.{
646 .{ .name = "expect", .value = "100-continue" },
647 .{ .name = "content-type", .value = "text/plain" },
648 },
677 });649 });
678 defer req.deinit();650 defer req.deinit();
679651
...@@ -694,20 +666,18 @@ pub fn main() !void {...@@ -694,20 +666,18 @@ pub fn main() !void {
694 }666 }
695667
696 { // expect: garbage668 { // 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
703 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/echo-content#expect-garbage", .{port});669 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/echo-content#expect-garbage", .{port});
704 defer calloc.free(location);670 defer calloc.free(location);
705 const uri = try std.Uri.parse(location);671 const uri = try std.Uri.parse(location);
706672
707 log.info("{s}", .{location});673 log.info("{s}", .{location});
708 var server_header_buffer: [1024]u8 = undefined;674 var server_header_buffer: [1024]u8 = undefined;
709 var req = try client.open(.POST, uri, h, .{675 var req = try client.open(.POST, uri, .{
710 .server_header_buffer = &server_header_buffer,676 .server_header_buffer = &server_header_buffer,
677 .extra_headers = &.{
678 .{ .name = "content-type", .value = "text/plain" },
679 .{ .name = "expect", .value = "garbage" },
680 },
711 });681 });
712 defer req.deinit();682 defer req.deinit();
713683
...@@ -734,7 +704,7 @@ pub fn main() !void {...@@ -734,7 +704,7 @@ pub fn main() !void {
734 for (0..total_connections) |i| {704 for (0..total_connections) |i| {
735 const headers_buf = try calloc.alloc(u8, 1024);705 const headers_buf = try calloc.alloc(u8, 1024);
736 try header_bufs.append(headers_buf);706 try header_bufs.append(headers_buf);
737 var req = try client.open(.GET, uri, .{ .allocator = calloc }, .{707 var req = try client.open(.GET, uri, .{
738 .server_header_buffer = headers_buf,708 .server_header_buffer = headers_buf,
739 });709 });
740 req.response.parser.state = .complete;710 req.response.parser.state = .complete;