authorgravatar for truemedian@gmail.comNameless <truemedian@gmail.com> 2023-03-30 23:29:13-05:00
committergravatar for truemedian@gmail.comNameless <truemedian@gmail.com> 2023-04-08 09:59:36-05:00
log52c78f4974d1913b9b7af678377a4c76a41ae23f
tree257ab9285bdb187e06b32b1d67d1726b98519f7d
parentaecbfa3a1e9aa379368e6a9a999ca42fc4803f18
signaturelock-open Commit is signed but in an unrecognized format.

fix bugs, waitForCompleteHead -> do, move redirecting to do instead of read

fix for 32bit arches curate error sets for api facing functions, expose raw errors in client.last_error fix bugged dependency loop, disable protocol tests (needs mocking) add separate mutex for bundle rescan

2 files changed, 305 insertions(+), 170 deletions(-)

lib/std/http/Client.zig+285-161
......@@ -19,12 +19,55 @@ pub const connection_pool_size = std.options.http_connection_pool_size;
1919/// managed buffer is not provided.
2020allocator: Allocator,
2121ca_bundle: std.crypto.Certificate.Bundle = .{},
22ca_bundle_mutex: std.Thread.Mutex = .{},
2223/// When this is `true`, the next time this client performs an HTTPS request,
2324/// it will first rescan the system for root certificates.
2425next_https_rescan_certs: bool = true,
2526
2627connection_pool: ConnectionPool = .{},
2728
29last_error: ?ExtraError = null,
30
31pub const ExtraError = union(enum) {
32 fn impliedErrorSet(comptime f: anytype) type {
33 const set = @typeInfo(@typeInfo(@TypeOf(f)).Fn.return_type.?).ErrorUnion.error_set;
34 if (@typeName(set)[0] != '@') @compileError(@typeName(f) ++ " doesn't have an implied error set any more.");
35 return set;
36 }
37
38 // There's apparently a dependency loop with using Client.DeflateDecompressor.
39 const FakeTransferError = proto.HeadersParser.ReadError || error{ReadFailed};
40 const FakeTransferReader = std.io.Reader(void, FakeTransferError, fakeRead);
41 fn fakeRead(ctx: void, buf: []u8) FakeTransferError!usize {
42 _ = .{ buf, ctx };
43 return 0;
44 }
45
46 const FakeDeflateDecompressor = std.compress.zlib.ZlibStream(FakeTransferReader);
47 const FakeGzipDecompressor = std.compress.gzip.Decompress(FakeTransferReader);
48 const FakeZstdDecompressor = std.compress.zstd.DecompressStream(FakeTransferReader, .{});
49
50 pub const TcpConnectError = std.net.TcpConnectToHostError;
51 pub const TlsError = std.crypto.tls.Client.InitError(net.Stream);
52 pub const WriteError = BufferedConnection.WriteError;
53 pub const ReadError = BufferedConnection.ReadError || error{HttpChunkInvalid};
54 pub const CaBundleError = impliedErrorSet(std.crypto.Certificate.Bundle.rescan);
55
56 pub const ZlibInitError = error{ BadHeader, InvalidCompression, InvalidWindowSize, Unsupported, EndOfStream, OutOfMemory } || Request.TransferReadError;
57 pub const GzipInitError = error{ BadHeader, InvalidCompression, OutOfMemory, WrongChecksum, EndOfStream, StreamTooLong } || Request.TransferReadError;
58 // pub const DecompressError = Client.DeflateDecompressor.Error || Client.GzipDecompressor.Error || Client.ZstdDecompressor.Error;
59 pub const DecompressError = FakeDeflateDecompressor.Error || FakeGzipDecompressor.Error || FakeZstdDecompressor.Error;
60
61 zlib_init: ZlibInitError, // error.CompressionInitializationFailed
62 gzip_init: GzipInitError, // error.CompressionInitializationFailed
63 connect: TcpConnectError, // error.ConnectionFailed
64 ca_bundle: CaBundleError, // error.CertificateAuthorityBundleFailed
65 tls: TlsError, // error.TlsInitializationFailed
66 write: WriteError, // error.WriteFailed
67 read: ReadError, // error.ReadFailed
68 decompress: DecompressError, // error.ReadFailed
69};
70
2871pub const ConnectionPool = struct {
2972 pub const Criteria = struct {
3073 host: []const u8,
......@@ -146,10 +189,6 @@ pub const ConnectionPool = struct {
146189 }
147190};
148191
149pub const DeflateDecompressor = std.compress.zlib.ZlibStream(Request.TransferReader);
150pub const GzipDecompressor = std.compress.gzip.Decompress(Request.TransferReader);
151pub const ZstdDecompressor = std.compress.zstd.DecompressStream(Request.TransferReader, .{});
152
153192pub const Connection = struct {
154193 stream: net.Stream,
155194 /// undefined unless protocol is tls.
......@@ -312,6 +351,10 @@ pub const RequestTransfer = union(enum) {
312351};
313352
314353pub const Compression = union(enum) {
354 pub const DeflateDecompressor = std.compress.zlib.ZlibStream(Request.TransferReader);
355 pub const GzipDecompressor = std.compress.gzip.Decompress(Request.TransferReader);
356 pub const ZstdDecompressor = std.compress.zstd.DecompressStream(Request.TransferReader, .{});
357
315358 deflate: DeflateDecompressor,
316359 gzip: GzipDecompressor,
317360 zstd: ZstdDecompressor,
......@@ -336,10 +379,11 @@ pub const Response = struct {
336379 HttpHeaderContinuationsUnsupported,
337380 HttpTransferEncodingUnsupported,
338381 HttpConnectionHeaderUnsupported,
339 InvalidCharacter,
382 InvalidContentLength,
383 CompressionNotSupported,
340384 };
341385
342 pub fn parse(bytes: []const u8) !Headers {
386 pub fn parse(bytes: []const u8) ParseError!Headers {
343387 var it = mem.tokenize(u8, bytes[0 .. bytes.len - 4], "\r\n");
344388
345389 const first_line = it.next() orelse return error.HttpHeadersInvalid;
......@@ -374,7 +418,7 @@ pub const Response = struct {
374418 headers.location = header_value;
375419 } else if (std.ascii.eqlIgnoreCase(header_name, "content-length")) {
376420 if (headers.content_length != null) return error.HttpHeadersInvalid;
377 headers.content_length = try std.fmt.parseInt(u64, header_value, 10);
421 headers.content_length = std.fmt.parseInt(u64, header_value, 10) catch return error.InvalidContentLength;
378422 } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) {
379423 // Transfer-Encoding: second, first
380424 // Transfer-Encoding: deflate, chunked
......@@ -457,6 +501,14 @@ pub const Response = struct {
457501 skip: bool = false,
458502};
459503
504/// A HTTP request.
505///
506/// Order of operations:
507/// - request
508/// - write
509/// - finish
510/// - do
511/// - read
460512pub const Request = struct {
461513 pub const Headers = struct {
462514 version: http.Version = .@"HTTP/1.1",
......@@ -506,7 +558,67 @@ pub const Request = struct {
506558 req.* = undefined;
507559 }
508560
509 pub const TransferReadError = BufferedConnection.ReadError || proto.HeadersParser.ReadError;
561 pub fn start(req: *Request, uri: Uri, headers: Headers) !void {
562 var buffered = std.io.bufferedWriter(req.connection.data.buffered.writer());
563 const w = buffered.writer();
564
565 const escaped_path = try Uri.escapePath(req.client.allocator, uri.path);
566 defer req.client.allocator.free(escaped_path);
567
568 const escaped_query = if (uri.query) |q| try Uri.escapeQuery(req.client.allocator, q) else null;
569 defer if (escaped_query) |q| req.client.allocator.free(q);
570
571 const escaped_fragment = if (uri.fragment) |f| try Uri.escapeQuery(req.client.allocator, f) else null;
572 defer if (escaped_fragment) |f| req.client.allocator.free(f);
573
574 try w.writeAll(@tagName(headers.method));
575 try w.writeByte(' ');
576 if (escaped_path.len == 0) {
577 try w.writeByte('/');
578 } else {
579 try w.writeAll(escaped_path);
580 }
581 if (escaped_query) |q| {
582 try w.writeByte('?');
583 try w.writeAll(q);
584 }
585 if (escaped_fragment) |f| {
586 try w.writeByte('#');
587 try w.writeAll(f);
588 }
589 try w.writeByte(' ');
590 try w.writeAll(@tagName(headers.version));
591 try w.writeAll("\r\nHost: ");
592 try w.writeAll(uri.host.?);
593 try w.writeAll("\r\nUser-Agent: ");
594 try w.writeAll(headers.user_agent);
595 if (headers.connection == .close) {
596 try w.writeAll("\r\nConnection: close");
597 } else {
598 try w.writeAll("\r\nConnection: keep-alive");
599 }
600 try w.writeAll("\r\nAccept-Encoding: gzip, deflate, zstd");
601 try w.writeAll("\r\nTE: trailers, gzip, deflate");
602
603 switch (headers.transfer_encoding) {
604 .chunked => try w.writeAll("\r\nTransfer-Encoding: chunked"),
605 .content_length => |content_length| try w.print("\r\nContent-Length: {d}", .{content_length}),
606 .none => {},
607 }
608
609 for (headers.custom) |header| {
610 try w.writeAll("\r\n");
611 try w.writeAll(header.name);
612 try w.writeAll(": ");
613 try w.writeAll(header.value);
614 }
615
616 try w.writeAll("\r\n\r\n");
617
618 try buffered.flush();
619 }
620
621 pub const TransferReadError = proto.HeadersParser.ReadError || error{ReadFailed};
510622
511623 pub const TransferReader = std.io.Reader(*Request, TransferReadError, transferRead);
512624
......@@ -519,7 +631,10 @@ pub const Request = struct {
519631
520632 var index: usize = 0;
521633 while (index == 0) {
522 const amt = try req.response.parser.read(&req.connection.data.buffered, buf[index..], req.response.skip);
634 const amt = req.response.parser.read(&req.connection.data.buffered, buf[index..], req.response.skip) catch |err| {
635 req.client.last_error = .{ .read = err };
636 return error.ReadFailed;
637 };
523638 if (amt == 0 and req.response.parser.isComplete()) break;
524639 index += amt;
525640 }
......@@ -527,78 +642,60 @@ pub const Request = struct {
527642 return index;
528643 }
529644
530 pub const WaitForCompleteHeadError = BufferedConnection.ReadError || proto.HeadersParser.CheckCompleteHeadError || Response.Headers.ParseError || error{ BadHeader, InvalidCompression, StreamTooLong, InvalidWindowSize } || error{CompressionNotSupported};
531
532 pub fn waitForCompleteHead(req: *Request) !void {
533 while (true) {
534 try req.connection.data.buffered.fill();
535
536 const nchecked = try req.response.parser.checkCompleteHead(req.client.allocator, req.connection.data.buffered.peek());
537 req.connection.data.buffered.clear(@intCast(u16, nchecked));
538
539 if (req.response.parser.state.isContent()) break;
540 }
541
542 req.response.headers = try Response.Headers.parse(req.response.parser.header_bytes.items);
543
544 if (req.response.headers.status == .switching_protocols) {
545 req.connection.data.closing = false;
546 req.response.parser.done = true;
547 }
548
549 if (req.headers.connection == .keep_alive and req.response.headers.connection == .keep_alive) {
550 req.connection.data.closing = false;
551 } else {
552 req.connection.data.closing = true;
553 }
645 pub const DoError = RequestError || TransferReadError || proto.HeadersParser.CheckCompleteHeadError || Response.Headers.ParseError || Uri.ParseError || error{ TooManyHttpRedirects, HttpRedirectMissingLocation, CompressionInitializationFailed };
554646
555 if (req.response.headers.transfer_encoding) |te| {
556 switch (te) {
557 .chunked => {
558 req.response.parser.next_chunk_length = 0;
559 req.response.parser.state = .chunk_head_size;
560 },
647 /// Waits for a response from the server and parses any headers that are sent.
648 /// This function will block until the final response is received.
649 ///
650 /// If `handle_redirects` is true, then this function will automatically follow
651 /// redirects.
652 pub fn do(req: *Request) DoError!void {
653 while (true) { // handle redirects
654 while (true) { // read headers
655 req.connection.data.buffered.fill() catch |err| {
656 req.client.last_error = .{ .read = err };
657 return error.ReadFailed;
658 };
659
660 const nchecked = try req.response.parser.checkCompleteHead(req.client.allocator, req.connection.data.buffered.peek());
661 req.connection.data.buffered.clear(@intCast(u16, nchecked));
662
663 if (req.response.parser.state.isContent()) break;
561664 }
562 } else if (req.response.headers.content_length) |cl| {
563 req.response.parser.next_chunk_length = cl;
564
565 if (cl == 0) req.response.parser.done = true;
566 } else {
567 req.response.parser.done = true;
568 }
569665
570 if (!req.response.parser.done) {
571 if (req.response.headers.transfer_compression) |tc| switch (tc) {
572 .compress => return error.CompressionNotSupported,
573 .deflate => req.response.compression = .{
574 .deflate = try std.compress.zlib.zlibStream(req.client.allocator, req.transferReader()),
575 },
576 .gzip => req.response.compression = .{
577 .gzip = try std.compress.gzip.decompress(req.client.allocator, req.transferReader()),
578 },
579 .zstd => req.response.compression = .{
580 .zstd = std.compress.zstd.decompressStream(req.client.allocator, req.transferReader()),
581 },
582 };
583 }
666 req.response.headers = try Response.Headers.parse(req.response.parser.header_bytes.items);
584667
585 if (req.response.headers.status.class() == .redirect and req.handle_redirects) req.response.skip = true;
586 }
668 if (req.response.headers.status == .switching_protocols) {
669 req.connection.data.closing = false;
670 req.response.parser.done = true;
671 }
587672
588 pub const ReadError = RequestError || Client.DeflateDecompressor.Error || Client.GzipDecompressor.Error || Client.ZstdDecompressor.Error || WaitForCompleteHeadError || error{ TooManyHttpRedirects, HttpRedirectMissingLocation, InvalidFormat, InvalidPort, UnexpectedCharacter };
673 if (req.headers.connection == .keep_alive and req.response.headers.connection == .keep_alive) {
674 req.connection.data.closing = false;
675 } else {
676 req.connection.data.closing = true;
677 }
589678
590 pub const Reader = std.io.Reader(*Request, ReadError, read);
679 if (req.response.headers.transfer_encoding) |te| {
680 switch (te) {
681 .chunked => {
682 req.response.parser.next_chunk_length = 0;
683 req.response.parser.state = .chunk_head_size;
684 },
685 }
686 } else if (req.response.headers.content_length) |cl| {
687 req.response.parser.next_chunk_length = cl;
591688
592 pub fn reader(req: *Request) Reader {
593 return .{ .context = req };
594 }
689 if (cl == 0) req.response.parser.done = true;
690 } else {
691 req.response.parser.done = true;
692 }
595693
596 pub fn read(req: *Request, buffer: []u8) ReadError!usize {
597 while (true) {
598 if (!req.response.parser.state.isContent()) try req.waitForCompleteHead();
694 if (req.response.headers.status.class() == .redirect and req.handle_redirects) {
695 req.response.skip = true;
599696
600 if (req.handle_redirects and req.response.headers.status.class() == .redirect) {
601 assert(try req.transferRead(buffer) == 0);
697 const empty = @as([*]u8, undefined)[0..0];
698 assert(try req.transferRead(empty) == 0); // we're skipping, no buffer is necessary
602699
603700 if (req.redirects_left == 0) return error.TooManyHttpRedirects;
604701
......@@ -624,29 +721,80 @@ pub const Request = struct {
624721 req.deinit();
625722 req.* = new_req;
626723 } else {
724 req.response.skip = false;
725 if (!req.response.parser.done) {
726 if (req.response.headers.transfer_compression) |tc| switch (tc) {
727 .compress => return error.CompressionNotSupported,
728 .deflate => req.response.compression = .{
729 .deflate = std.compress.zlib.zlibStream(req.client.allocator, req.transferReader()) catch |err| {
730 req.client.last_error = .{ .zlib_init = err };
731 return error.CompressionInitializationFailed;
732 },
733 },
734 .gzip => req.response.compression = .{
735 .gzip = std.compress.gzip.decompress(req.client.allocator, req.transferReader()) catch |err| {
736 req.client.last_error = .{ .gzip_init = err };
737 return error.CompressionInitializationFailed;
738 },
739 },
740 .zstd => req.response.compression = .{
741 .zstd = std.compress.zstd.decompressStream(req.client.allocator, req.transferReader()),
742 },
743 };
744 }
745
627746 break;
628747 }
629748 }
749 }
750
751 pub const ReadError = TransferReadError;
752
753 pub const Reader = std.io.Reader(*Request, ReadError, read);
754
755 pub fn reader(req: *Request) Reader {
756 return .{ .context = req };
757 }
758
759 /// Reads data from the response body. Must be called after `do`.
760 pub fn read(req: *Request, buffer: []u8) ReadError!usize {
761 assert(req.response.parser.state.isContent());
630762
631763 return switch (req.response.compression) {
632 .deflate => |*deflate| try deflate.read(buffer),
633 .gzip => |*gzip| try gzip.read(buffer),
634 .zstd => |*zstd| try zstd.read(buffer),
764 .deflate => |*deflate| deflate.read(buffer) catch |err| {
765 req.client.last_error = .{ .decompress = err };
766 err catch {};
767 return error.ReadFailed;
768 },
769 .gzip => |*gzip| gzip.read(buffer) catch |err| {
770 req.client.last_error = .{ .decompress = err };
771 err catch {};
772 return error.ReadFailed;
773 },
774 .zstd => |*zstd| zstd.read(buffer) catch |err| {
775 req.client.last_error = .{ .decompress = err };
776 err catch {};
777 return error.ReadFailed;
778 },
635779 else => try req.transferRead(buffer),
636780 };
637781 }
638782
783 /// Reads data from the response body. Must be called after `do`.
639784 pub fn readAll(req: *Request, buffer: []u8) !usize {
640785 var index: usize = 0;
641786 while (index < buffer.len) {
642 const amt = try read(req, buffer[index..]);
787 const amt = read(req, buffer[index..]) catch |err| {
788 req.client.last_error = .{ .read = err };
789 return error.ReadFailed;
790 };
643791 if (amt == 0) break;
644792 index += amt;
645793 }
646794 return index;
647795 }
648796
649 pub const WriteError = BufferedConnection.WriteError || error{ NotWriteable, MessageTooLong };
797 pub const WriteError = error{ WriteFailed, NotWriteable, MessageTooLong };
650798
651799 pub const Writer = std.io.Writer(*Request, WriteError, write);
652800
......@@ -658,16 +806,28 @@ pub const Request = struct {
658806 pub fn write(req: *Request, bytes: []const u8) WriteError!usize {
659807 switch (req.headers.transfer_encoding) {
660808 .chunked => {
661 try req.connection.data.conn.writer().print("{x}\r\n", .{bytes.len});
662 try req.connection.data.conn.writeAll(bytes);
663 try req.connection.data.conn.writeAll("\r\n");
809 req.connection.data.conn.writer().print("{x}\r\n", .{bytes.len}) catch |err| {
810 req.client.last_error = .{ .write = err };
811 return error.WriteFailed;
812 };
813 req.connection.data.conn.writeAll(bytes) catch |err| {
814 req.client.last_error = .{ .write = err };
815 return error.WriteFailed;
816 };
817 req.connection.data.conn.writeAll("\r\n") catch |err| {
818 req.client.last_error = .{ .write = err };
819 return error.WriteFailed;
820 };
664821
665822 return bytes.len;
666823 },
667824 .content_length => |*len| {
668825 if (len.* < bytes.len) return error.MessageTooLong;
669826
670 const amt = try req.connection.data.conn.write(bytes);
827 const amt = req.connection.data.conn.write(bytes) catch |err| {
828 req.client.last_error = .{ .write = err };
829 return error.WriteFailed;
830 };
671831 len.* -= amt;
672832 return amt;
673833 },
......@@ -678,7 +838,10 @@ pub const Request = struct {
678838 /// Finish the body of a request. This notifies the server that you have no more data to send.
679839 pub fn finish(req: *Request) !void {
680840 switch (req.headers.transfer_encoding) {
681 .chunked => try req.connection.data.conn.writeAll("0\r\n"),
841 .chunked => req.connection.data.conn.writeAll("0\r\n") catch |err| {
842 req.client.last_error = .{ .write = err };
843 return error.WriteFailed;
844 },
682845 .content_length => |len| if (len != 0) return error.MessageNotCompleted,
683846 .none => {},
684847 }
......@@ -692,7 +855,7 @@ pub fn deinit(client: *Client) void {
692855 client.* = undefined;
693856}
694857
695pub const ConnectError = Allocator.Error || net.TcpConnectToHostError || std.crypto.tls.Client.InitError(net.Stream);
858pub const ConnectError = Allocator.Error || error{ ConnectionFailed, TlsInitializationFailed };
696859
697860pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectError!*ConnectionPool.Node {
698861 if (client.connection_pool.findConnection(.{
......@@ -706,7 +869,11 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio
706869 errdefer client.allocator.destroy(conn);
707870 conn.* = .{ .data = undefined };
708871
709 const stream = try net.tcpConnectToHost(client.allocator, host, port);
872 const stream = net.tcpConnectToHost(client.allocator, host, port) catch |err| {
873 client.last_error = .{ .connect = err };
874 return error.ConnectionFailed;
875 };
876 errdefer stream.close();
710877
711878 conn.data = .{
712879 .buffered = .{ .conn = .{
......@@ -717,12 +884,18 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio
717884 .host = try client.allocator.dupe(u8, host),
718885 .port = port,
719886 };
887 errdefer client.allocator.free(conn.data.host);
720888
721889 switch (protocol) {
722890 .plain => {},
723891 .tls => {
724892 conn.data.buffered.conn.tls_client = try client.allocator.create(std.crypto.tls.Client);
725 conn.data.buffered.conn.tls_client.* = try std.crypto.tls.Client.init(stream, client.ca_bundle, host);
893 errdefer client.allocator.destroy(conn.data.buffered.conn.tls_client);
894
895 conn.data.buffered.conn.tls_client.* = std.crypto.tls.Client.init(stream, client.ca_bundle, host) catch |err| {
896 client.last_error = .{ .tls = err };
897 return error.TlsInitializationFailed;
898 };
726899 // This is appropriate for HTTPS because the HTTP headers contain
727900 // the content length which is used to detect truncation attacks.
728901 conn.data.buffered.conn.tls_client.allow_truncation_attacks = true;
......@@ -734,15 +907,12 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio
734907 return conn;
735908}
736909
737pub const RequestError = ConnectError || BufferedConnection.WriteError || error{
910pub const RequestError = ConnectError || error{
738911 UnsupportedUrlScheme,
739912 UriMissingHost,
740913
741 CertificateAuthorityBundleTooBig,
742 InvalidPadding,
743 MissingEndCertificateMarker,
744 Unseekable,
745 EndOfStream,
914 CertificateAuthorityBundleFailed,
915 WriteFailed,
746916};
747917
748918pub const Options = struct {
......@@ -764,13 +934,15 @@ pub const Options = struct {
764934 };
765935};
766936
937pub const protocol_map = std.ComptimeStringMap(Connection.Protocol, .{
938 .{ "http", .plain },
939 .{ "ws", .plain },
940 .{ "https", .tls },
941 .{ "wss", .tls },
942});
943
767944pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Options) RequestError!Request {
768 const protocol: Connection.Protocol = if (mem.eql(u8, uri.scheme, "http"))
769 .plain
770 else if (mem.eql(u8, uri.scheme, "https"))
771 .tls
772 else
773 return error.UnsupportedUrlScheme;
945 const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme;
774946
775947 const port: u16 = uri.port orelse switch (protocol) {
776948 .plain => 80,
......@@ -779,13 +951,16 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Opt
779951
780952 const host = uri.host orelse return error.UriMissingHost;
781953
782 if (client.next_https_rescan_certs and protocol == .tls) {
783 client.connection_pool.mutex.lock(); // TODO: this could be so much better than reusing the connection pool mutex.
784 defer client.connection_pool.mutex.unlock();
954 if (protocol == .tls and @atomicLoad(bool, &client.next_https_rescan_certs, .Acquire)) {
955 client.ca_bundle_mutex.lock();
956 defer client.ca_bundle_mutex.unlock();
785957
786958 if (client.next_https_rescan_certs) {
787 try client.ca_bundle.rescan(client.allocator);
788 client.next_https_rescan_certs = false;
959 client.ca_bundle.rescan(client.allocator) catch |err| {
960 client.last_error = .{ .ca_bundle = err };
961 return error.CertificateAuthorityBundleFailed;
962 };
963 @atomicStore(bool, &client.next_https_rescan_certs, false, .Release);
789964 }
790965 }
791966
......@@ -804,68 +979,17 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Opt
804979 },
805980 .arena = undefined,
806981 };
982 errdefer req.deinit();
807983
808984 req.arena = std.heap.ArenaAllocator.init(client.allocator);
809985
810 {
811 var buffered = std.io.bufferedWriter(req.connection.data.buffered.writer());
812 const writer = buffered.writer();
813
814 const escaped_path = try Uri.escapePath(client.allocator, uri.path);
815 defer client.allocator.free(escaped_path);
816
817 const escaped_query = if (uri.query) |q| try Uri.escapeQuery(client.allocator, q) else null;
818 defer if (escaped_query) |q| client.allocator.free(q);
986 req.start(uri, headers) catch |err| {
987 if (err == error.OutOfMemory) return error.OutOfMemory;
988 const err_casted = @errSetCast(BufferedConnection.WriteError, err);
819989
820 const escaped_fragment = if (uri.fragment) |f| try Uri.escapeQuery(client.allocator, f) else null;
821 defer if (escaped_fragment) |f| client.allocator.free(f);
822
823 try writer.writeAll(@tagName(headers.method));
824 try writer.writeByte(' ');
825 if (escaped_path.len == 0) {
826 try writer.writeByte('/');
827 } else {
828 try writer.writeAll(escaped_path);
829 }
830 if (escaped_query) |q| {
831 try writer.writeByte('?');
832 try writer.writeAll(q);
833 }
834 if (escaped_fragment) |f| {
835 try writer.writeByte('#');
836 try writer.writeAll(f);
837 }
838 try writer.writeByte(' ');
839 try writer.writeAll(@tagName(headers.version));
840 try writer.writeAll("\r\nHost: ");
841 try writer.writeAll(host);
842 try writer.writeAll("\r\nUser-Agent: ");
843 try writer.writeAll(headers.user_agent);
844 if (headers.connection == .close) {
845 try writer.writeAll("\r\nConnection: close");
846 } else {
847 try writer.writeAll("\r\nConnection: keep-alive");
848 }
849 try writer.writeAll("\r\nAccept-Encoding: gzip, deflate, zstd");
850 try writer.writeAll("\r\nTE: trailers, gzip, deflate");
851
852 switch (headers.transfer_encoding) {
853 .chunked => try writer.writeAll("\r\nTransfer-Encoding: chunked"),
854 .content_length => |content_length| try writer.print("\r\nContent-Length: {d}", .{content_length}),
855 .none => {},
856 }
857
858 for (headers.custom) |header| {
859 try writer.writeAll("\r\n");
860 try writer.writeAll(header.name);
861 try writer.writeAll(": ");
862 try writer.writeAll(header.value);
863 }
864
865 try writer.writeAll("\r\n\r\n");
866
867 try buffered.flush();
868 }
990 client.last_error = .{ .write = err_casted };
991 return error.WriteFailed;
992 };
869993
870994 return req;
871995}
......@@ -880,5 +1004,5 @@ test {
8801004
8811005 if (builtin.os.tag == .wasi) return error.SkipZigTest;
8821006
883 _ = Request;
1007 std.testing.refAllDecls(@This());
8841008}
lib/std/http/protocol.zig+20-9
......@@ -490,8 +490,6 @@ pub const HeadersParser = struct {
490490 }
491491
492492 pub const ReadError = error{
493 UnexpectedEndOfStream,
494 HttpHeadersExceededSizeLimit,
495493 HttpChunkInvalid,
496494 };
497495
......@@ -515,16 +513,20 @@ pub const HeadersParser = struct {
515513 bconn.clear(@intCast(u16, nread));
516514 r.next_chunk_length -= nread;
517515
516 if (r.next_chunk_length == 0) r.done = true;
517
518518 return 0;
519 }
519 } else {
520 const out_avail = buffer.len;
520521
521 const out_avail = buffer.len;
522 const can_read = @intCast(usize, @min(data_avail, out_avail));
523 const nread = try bconn.read(buffer[0..can_read]);
524 r.next_chunk_length -= nread;
522525
523 const can_read = @min(data_avail, out_avail);
524 const nread = try bconn.read(buffer[0..can_read]);
525 r.next_chunk_length -= nread;
526 if (r.next_chunk_length == 0) r.done = true;
526527
527 return nread;
528 return nread;
529 }
528530 },
529531 .chunk_data_suffix, .chunk_data_suffix_r, .chunk_head_size, .chunk_head_ext, .chunk_head_r => {
530532 try bconn.fill();
......@@ -557,7 +559,7 @@ pub const HeadersParser = struct {
557559 bconn.clear(@intCast(u16, nread));
558560 r.next_chunk_length -= nread;
559561 } else {
560 const can_read = @min(data_avail, out_avail);
562 const can_read = @intCast(usize, @min(data_avail, out_avail));
561563 const nread = try bconn.read(buffer[out_index..][0..can_read]);
562564 r.next_chunk_length -= nread;
563565 out_index += nread;
......@@ -641,6 +643,9 @@ test "HeadersParser.findChunkedLen" {
641643}
642644
643645test "HeadersParser.read length" {
646 // mock BufferedConnection for read
647 if (true) return error.SkipZigTest;
648
644649 var r = HeadersParser.initDynamic(256);
645650 defer r.header_bytes.deinit(std.testing.allocator);
646651 const data = "GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 5\r\n\r\nHello";
......@@ -658,6 +663,9 @@ test "HeadersParser.read length" {
658663}
659664
660665test "HeadersParser.read chunked" {
666 // mock BufferedConnection for read
667 if (true) return error.SkipZigTest;
668
661669 var r = HeadersParser.initDynamic(256);
662670 defer r.header_bytes.deinit(std.testing.allocator);
663671 const data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n2\r\nHe\r\n2\r\nll\r\n1\r\no\r\n0\r\n\r\n";
......@@ -675,6 +683,9 @@ test "HeadersParser.read chunked" {
675683}
676684
677685test "HeadersParser.read chunked trailer" {
686 // mock BufferedConnection for read
687 if (true) return error.SkipZigTest;
688
678689 var r = HeadersParser.initDynamic(256);
679690 defer r.header_bytes.deinit(std.testing.allocator);
680691 const data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n2\r\nHe\r\n2\r\nll\r\n1\r\no\r\n0\r\nContent-Type: text/plain\r\n\r\n";