zig

fork of https://codeberg.org/ziglang/zig
Log | Files | Refs | README | LICENSE

blob 81bfbdcc (70579B) - Raw


      1 //! HTTP(S) Client implementation.
      2 //!
      3 //! Connections are opened in a thread-safe manner, but individual Requests are not.
      4 //!
      5 //! TLS support may be disabled via `std.options.http_disable_tls`.
      6 
      7 const std = @import("../std.zig");
      8 const builtin = @import("builtin");
      9 const testing = std.testing;
     10 const http = std.http;
     11 const mem = std.mem;
     12 const Uri = std.Uri;
     13 const Allocator = mem.Allocator;
     14 const assert = std.debug.assert;
     15 const Io = std.Io;
     16 const Writer = std.Io.Writer;
     17 const Reader = std.Io.Reader;
     18 
     19 const Client = @This();
     20 
     21 pub const disable_tls = std.options.http_disable_tls;
     22 
     23 /// Used for all client allocations. Must be thread-safe.
     24 allocator: Allocator,
     25 /// Used for opening TCP connections.
     26 io: Io,
     27 
     28 ca_bundle: if (disable_tls) void else std.crypto.Certificate.Bundle = if (disable_tls) {} else .{},
     29 ca_bundle_mutex: std.Thread.Mutex = .{},
     30 /// Used both for the reader and writer buffers.
     31 tls_buffer_size: if (disable_tls) u0 else usize = if (disable_tls) 0 else std.crypto.tls.Client.min_buffer_len,
     32 /// If non-null, ssl secrets are logged to a stream. Creating such a stream
     33 /// allows other processes with access to that stream to decrypt all
     34 /// traffic over connections created with this `Client`.
     35 ssl_key_log: ?*std.crypto.tls.Client.SslKeyLog = null,
     36 
     37 /// When this is `true`, the next time this client performs an HTTPS request,
     38 /// it will first rescan the system for root certificates.
     39 next_https_rescan_certs: bool = true,
     40 
     41 /// The pool of connections that can be reused (and currently in use).
     42 connection_pool: ConnectionPool = .{},
     43 /// Each `Connection` allocates this amount for the reader buffer.
     44 ///
     45 /// If the entire HTTP header cannot fit in this amount of bytes,
     46 /// `error.HttpHeadersOversize` will be returned from `Request.wait`.
     47 read_buffer_size: usize = 8192,
     48 /// Each `Connection` allocates this amount for the writer buffer.
     49 write_buffer_size: usize = 1024,
     50 
     51 /// If populated, all http traffic travels through this third party.
     52 /// This field cannot be modified while the client has active connections.
     53 /// Pointer to externally-owned memory.
     54 http_proxy: ?*Proxy = null,
     55 /// If populated, all https traffic travels through this third party.
     56 /// This field cannot be modified while the client has active connections.
     57 /// Pointer to externally-owned memory.
     58 https_proxy: ?*Proxy = null,
     59 
     60 /// A Least-Recently-Used cache of open connections to be reused.
     61 pub const ConnectionPool = struct {
     62     mutex: std.Thread.Mutex = .{},
     63     /// Open connections that are currently in use.
     64     used: std.DoublyLinkedList = .{},
     65     /// Open connections that are not currently in use.
     66     free: std.DoublyLinkedList = .{},
     67     free_len: usize = 0,
     68     free_size: usize = 32,
     69 
     70     /// The criteria for a connection to be considered a match.
     71     pub const Criteria = struct {
     72         host: []const u8,
     73         port: u16,
     74         protocol: Protocol,
     75     };
     76 
     77     /// Finds and acquires a connection from the connection pool matching the criteria.
     78     /// If no connection is found, null is returned.
     79     ///
     80     /// Threadsafe.
     81     pub fn findConnection(pool: *ConnectionPool, criteria: Criteria) ?*Connection {
     82         pool.mutex.lock();
     83         defer pool.mutex.unlock();
     84 
     85         var next = pool.free.last;
     86         while (next) |node| : (next = node.prev) {
     87             const connection: *Connection = @alignCast(@fieldParentPtr("pool_node", node));
     88             if (connection.protocol != criteria.protocol) continue;
     89             if (connection.port != criteria.port) continue;
     90 
     91             // Domain names are case-insensitive (RFC 5890, Section 2.3.2.4)
     92             if (!std.ascii.eqlIgnoreCase(connection.host(), criteria.host)) continue;
     93 
     94             pool.acquireUnsafe(connection);
     95             return connection;
     96         }
     97 
     98         return null;
     99     }
    100 
    101     /// Acquires an existing connection from the connection pool. This function is not threadsafe.
    102     pub fn acquireUnsafe(pool: *ConnectionPool, connection: *Connection) void {
    103         pool.free.remove(&connection.pool_node);
    104         pool.free_len -= 1;
    105 
    106         pool.used.append(&connection.pool_node);
    107     }
    108 
    109     /// Acquires an existing connection from the connection pool. This function is threadsafe.
    110     pub fn acquire(pool: *ConnectionPool, connection: *Connection) void {
    111         pool.mutex.lock();
    112         defer pool.mutex.unlock();
    113 
    114         return pool.acquireUnsafe(connection);
    115     }
    116 
    117     /// Tries to release a connection back to the connection pool.
    118     /// If the connection is marked as closing, it will be closed instead.
    119     ///
    120     /// Threadsafe.
    121     pub fn release(pool: *ConnectionPool, connection: *Connection) void {
    122         pool.mutex.lock();
    123         defer pool.mutex.unlock();
    124 
    125         pool.used.remove(&connection.pool_node);
    126 
    127         if (connection.closing or pool.free_size == 0) return connection.destroy();
    128 
    129         if (pool.free_len >= pool.free_size) {
    130             const popped: *Connection = @alignCast(@fieldParentPtr("pool_node", pool.free.popFirst().?));
    131             pool.free_len -= 1;
    132 
    133             popped.destroy();
    134         }
    135 
    136         if (connection.proxied) {
    137             // proxied connections go to the end of the queue, always try direct connections first
    138             pool.free.prepend(&connection.pool_node);
    139         } else {
    140             pool.free.append(&connection.pool_node);
    141         }
    142 
    143         pool.free_len += 1;
    144     }
    145 
    146     /// Adds a newly created node to the pool of used connections. This function is threadsafe.
    147     pub fn addUsed(pool: *ConnectionPool, connection: *Connection) void {
    148         pool.mutex.lock();
    149         defer pool.mutex.unlock();
    150 
    151         pool.used.append(&connection.pool_node);
    152     }
    153 
    154     /// Resizes the connection pool.
    155     ///
    156     /// If the new size is smaller than the current size, then idle connections will be closed until the pool is the new size.
    157     ///
    158     /// Threadsafe.
    159     pub fn resize(pool: *ConnectionPool, allocator: Allocator, new_size: usize) void {
    160         pool.mutex.lock();
    161         defer pool.mutex.unlock();
    162 
    163         const next = pool.free.first;
    164         _ = next;
    165         while (pool.free_len > new_size) {
    166             const popped = pool.free.popFirst() orelse unreachable;
    167             pool.free_len -= 1;
    168 
    169             popped.data.close(allocator);
    170             allocator.destroy(popped);
    171         }
    172 
    173         pool.free_size = new_size;
    174     }
    175 
    176     /// Frees the connection pool and closes all connections within.
    177     ///
    178     /// All future operations on the connection pool will deadlock.
    179     ///
    180     /// Threadsafe.
    181     pub fn deinit(pool: *ConnectionPool) void {
    182         pool.mutex.lock();
    183 
    184         var next = pool.free.first;
    185         while (next) |node| {
    186             const connection: *Connection = @alignCast(@fieldParentPtr("pool_node", node));
    187             next = node.next;
    188             connection.destroy();
    189         }
    190 
    191         next = pool.used.first;
    192         while (next) |node| {
    193             const connection: *Connection = @alignCast(@fieldParentPtr("pool_node", node));
    194             next = node.next;
    195             connection.destroy();
    196         }
    197 
    198         pool.* = undefined;
    199     }
    200 };
    201 
    202 pub const Protocol = enum {
    203     plain,
    204     tls,
    205 
    206     fn port(protocol: Protocol) u16 {
    207         return switch (protocol) {
    208             .plain => 80,
    209             .tls => 443,
    210         };
    211     }
    212 
    213     pub fn fromScheme(scheme: []const u8) ?Protocol {
    214         const protocol_map = std.StaticStringMap(Protocol).initComptime(.{
    215             .{ "http", .plain },
    216             .{ "ws", .plain },
    217             .{ "https", .tls },
    218             .{ "wss", .tls },
    219         });
    220         return protocol_map.get(scheme);
    221     }
    222 
    223     pub fn fromUri(uri: Uri) ?Protocol {
    224         return fromScheme(uri.scheme);
    225     }
    226 };
    227 
    228 pub const Connection = struct {
    229     client: *Client,
    230     stream_writer: Io.net.Stream.Writer,
    231     stream_reader: Io.net.Stream.Reader,
    232     /// Entry in `ConnectionPool.used` or `ConnectionPool.free`.
    233     pool_node: std.DoublyLinkedList.Node,
    234     port: u16,
    235     host_len: u8,
    236     proxied: bool,
    237     closing: bool,
    238     protocol: Protocol,
    239 
    240     const Plain = struct {
    241         connection: Connection,
    242 
    243         fn create(
    244             client: *Client,
    245             remote_host: []const u8,
    246             port: u16,
    247             stream: Io.net.Stream,
    248         ) error{OutOfMemory}!*Plain {
    249             const gpa = client.allocator;
    250             const alloc_len = allocLen(client, remote_host.len);
    251             const base = try gpa.alignedAlloc(u8, .of(Plain), alloc_len);
    252             errdefer gpa.free(base);
    253             const host_buffer = base[@sizeOf(Plain)..][0..remote_host.len];
    254             const socket_read_buffer = host_buffer.ptr[host_buffer.len..][0..client.read_buffer_size];
    255             const socket_write_buffer = socket_read_buffer.ptr[socket_read_buffer.len..][0..client.write_buffer_size];
    256             assert(base.ptr + alloc_len == socket_write_buffer.ptr + socket_write_buffer.len);
    257             @memcpy(host_buffer, remote_host);
    258             const plain: *Plain = @ptrCast(base);
    259             plain.* = .{
    260                 .connection = .{
    261                     .client = client,
    262                     .stream_writer = stream.writer(socket_write_buffer),
    263                     .stream_reader = stream.reader(socket_read_buffer),
    264                     .pool_node = .{},
    265                     .port = port,
    266                     .host_len = @intCast(remote_host.len),
    267                     .proxied = false,
    268                     .closing = false,
    269                     .protocol = .plain,
    270                 },
    271             };
    272             return plain;
    273         }
    274 
    275         fn destroy(plain: *Plain) void {
    276             const c = &plain.connection;
    277             const gpa = c.client.allocator;
    278             const base: [*]align(@alignOf(Plain)) u8 = @ptrCast(plain);
    279             gpa.free(base[0..allocLen(c.client, c.host_len)]);
    280         }
    281 
    282         fn allocLen(client: *Client, host_len: usize) usize {
    283             return @sizeOf(Plain) + host_len + client.read_buffer_size + client.write_buffer_size;
    284         }
    285 
    286         fn host(plain: *Plain) []u8 {
    287             const base: [*]u8 = @ptrCast(plain);
    288             return base[@sizeOf(Plain)..][0..plain.connection.host_len];
    289         }
    290     };
    291 
    292     const Tls = struct {
    293         client: std.crypto.tls.Client,
    294         connection: Connection,
    295 
    296         fn create(
    297             client: *Client,
    298             remote_host: []const u8,
    299             port: u16,
    300             stream: Io.net.Stream,
    301         ) error{ OutOfMemory, TlsInitializationFailed }!*Tls {
    302             const gpa = client.allocator;
    303             const alloc_len = allocLen(client, remote_host.len);
    304             const base = try gpa.alignedAlloc(u8, .of(Tls), alloc_len);
    305             errdefer gpa.free(base);
    306             const host_buffer = base[@sizeOf(Tls)..][0..remote_host.len];
    307             // The TLS client wants enough buffer for the max encrypted frame
    308             // size, and the HTTP body reader wants enough buffer for the
    309             // entire HTTP header. This means we need a combined upper bound.
    310             const tls_read_buffer_len = client.tls_buffer_size + client.read_buffer_size;
    311             const tls_read_buffer = host_buffer.ptr[host_buffer.len..][0..tls_read_buffer_len];
    312             const tls_write_buffer = tls_read_buffer.ptr[tls_read_buffer.len..][0..client.tls_buffer_size];
    313             const socket_write_buffer = tls_write_buffer.ptr[tls_write_buffer.len..][0..client.write_buffer_size];
    314             const socket_read_buffer = socket_write_buffer.ptr[socket_write_buffer.len..][0..client.tls_buffer_size];
    315             assert(base.ptr + alloc_len == socket_read_buffer.ptr + socket_read_buffer.len);
    316             @memcpy(host_buffer, remote_host);
    317             const tls: *Tls = @ptrCast(base);
    318             tls.* = .{
    319                 .connection = .{
    320                     .client = client,
    321                     .stream_writer = stream.writer(tls_write_buffer),
    322                     .stream_reader = stream.reader(socket_read_buffer),
    323                     .pool_node = .{},
    324                     .port = port,
    325                     .host_len = @intCast(remote_host.len),
    326                     .proxied = false,
    327                     .closing = false,
    328                     .protocol = .tls,
    329                 },
    330                 // TODO data race here on ca_bundle if the user sets next_https_rescan_certs to true
    331                 .client = std.crypto.tls.Client.init(
    332                     tls.connection.stream_reader.interface(),
    333                     &tls.connection.stream_writer.interface,
    334                     .{
    335                         .host = .{ .explicit = remote_host },
    336                         .ca = .{ .bundle = client.ca_bundle },
    337                         .ssl_key_log = client.ssl_key_log,
    338                         .read_buffer = tls_read_buffer,
    339                         .write_buffer = socket_write_buffer,
    340                         // This is appropriate for HTTPS because the HTTP headers contain
    341                         // the content length which is used to detect truncation attacks.
    342                         .allow_truncation_attacks = true,
    343                     },
    344                 ) catch return error.TlsInitializationFailed,
    345             };
    346             return tls;
    347         }
    348 
    349         fn destroy(tls: *Tls) void {
    350             const c = &tls.connection;
    351             const gpa = c.client.allocator;
    352             const base: [*]align(@alignOf(Tls)) u8 = @ptrCast(tls);
    353             gpa.free(base[0..allocLen(c.client, c.host_len)]);
    354         }
    355 
    356         fn allocLen(client: *Client, host_len: usize) usize {
    357             const tls_read_buffer_len = client.tls_buffer_size + client.read_buffer_size;
    358             return @sizeOf(Tls) + host_len + tls_read_buffer_len + client.tls_buffer_size +
    359                 client.write_buffer_size + client.tls_buffer_size;
    360         }
    361 
    362         fn host(tls: *Tls) []u8 {
    363             const base: [*]u8 = @ptrCast(tls);
    364             return base[@sizeOf(Tls)..][0..tls.connection.host_len];
    365         }
    366     };
    367 
    368     pub const ReadError = std.crypto.tls.Client.ReadError || Io.net.Stream.ReadError;
    369 
    370     pub fn getReadError(c: *const Connection) ?ReadError {
    371         return switch (c.protocol) {
    372             .tls => {
    373                 if (disable_tls) unreachable;
    374                 const tls: *const Tls = @alignCast(@fieldParentPtr("connection", c));
    375                 return tls.client.read_err orelse c.stream_reader.getError();
    376             },
    377             .plain => {
    378                 return c.stream_reader.getError();
    379             },
    380         };
    381     }
    382 
    383     fn getStream(c: *Connection) Io.net.Stream {
    384         return c.stream_reader.stream;
    385     }
    386 
    387     pub fn host(c: *Connection) []u8 {
    388         return switch (c.protocol) {
    389             .tls => {
    390                 if (disable_tls) unreachable;
    391                 const tls: *Tls = @alignCast(@fieldParentPtr("connection", c));
    392                 return tls.host();
    393             },
    394             .plain => {
    395                 const plain: *Plain = @alignCast(@fieldParentPtr("connection", c));
    396                 return plain.host();
    397             },
    398         };
    399     }
    400 
    401     /// If this is called without calling `flush` or `end`, data will be
    402     /// dropped unsent.
    403     pub fn destroy(c: *Connection) void {
    404         c.getStream().close();
    405         switch (c.protocol) {
    406             .tls => {
    407                 if (disable_tls) unreachable;
    408                 const tls: *Tls = @alignCast(@fieldParentPtr("connection", c));
    409                 tls.destroy();
    410             },
    411             .plain => {
    412                 const plain: *Plain = @alignCast(@fieldParentPtr("connection", c));
    413                 plain.destroy();
    414             },
    415         }
    416     }
    417 
    418     /// HTTP protocol from client to server.
    419     /// This either goes directly to `stream_writer`, or to a TLS client.
    420     pub fn writer(c: *Connection) *Writer {
    421         return switch (c.protocol) {
    422             .tls => {
    423                 if (disable_tls) unreachable;
    424                 const tls: *Tls = @alignCast(@fieldParentPtr("connection", c));
    425                 return &tls.client.writer;
    426             },
    427             .plain => &c.stream_writer.interface,
    428         };
    429     }
    430 
    431     /// HTTP protocol from server to client.
    432     /// This either comes directly from `stream_reader`, or from a TLS client.
    433     pub fn reader(c: *Connection) *Reader {
    434         return switch (c.protocol) {
    435             .tls => {
    436                 if (disable_tls) unreachable;
    437                 const tls: *Tls = @alignCast(@fieldParentPtr("connection", c));
    438                 return &tls.client.reader;
    439             },
    440             .plain => c.stream_reader.interface(),
    441         };
    442     }
    443 
    444     pub fn flush(c: *Connection) Writer.Error!void {
    445         if (c.protocol == .tls) {
    446             if (disable_tls) unreachable;
    447             const tls: *Tls = @alignCast(@fieldParentPtr("connection", c));
    448             try tls.client.writer.flush();
    449         }
    450         try c.stream_writer.interface.flush();
    451     }
    452 
    453     /// If the connection is a TLS connection, sends the close_notify alert.
    454     ///
    455     /// Flushes all buffers.
    456     pub fn end(c: *Connection) Writer.Error!void {
    457         if (c.protocol == .tls) {
    458             if (disable_tls) unreachable;
    459             const tls: *Tls = @alignCast(@fieldParentPtr("connection", c));
    460             try tls.client.end();
    461         }
    462         try c.stream_writer.interface.flush();
    463     }
    464 };
    465 
    466 pub const Response = struct {
    467     request: *Request,
    468     /// Pointers in this struct are invalidated when the response body stream
    469     /// is initialized.
    470     head: Head,
    471 
    472     pub const Head = struct {
    473         bytes: []const u8,
    474         version: http.Version,
    475         status: http.Status,
    476         reason: []const u8,
    477         location: ?[]const u8 = null,
    478         content_type: ?[]const u8 = null,
    479         content_disposition: ?[]const u8 = null,
    480 
    481         keep_alive: bool,
    482 
    483         /// If present, the number of bytes in the response body.
    484         content_length: ?u64 = null,
    485 
    486         transfer_encoding: http.TransferEncoding = .none,
    487         content_encoding: http.ContentEncoding = .identity,
    488 
    489         pub const ParseError = error{
    490             HttpConnectionHeaderUnsupported,
    491             HttpContentEncodingUnsupported,
    492             HttpHeaderContinuationsUnsupported,
    493             HttpHeadersInvalid,
    494             HttpTransferEncodingUnsupported,
    495             InvalidContentLength,
    496         };
    497 
    498         pub fn parse(bytes: []const u8) ParseError!Head {
    499             var res: Head = .{
    500                 .bytes = bytes,
    501                 .status = undefined,
    502                 .reason = undefined,
    503                 .version = undefined,
    504                 .keep_alive = false,
    505             };
    506             var it = mem.splitSequence(u8, bytes, "\r\n");
    507 
    508             const first_line = it.first();
    509             if (first_line.len < 12) return error.HttpHeadersInvalid;
    510 
    511             const version: http.Version = switch (int64(first_line[0..8])) {
    512                 int64("HTTP/1.0") => .@"HTTP/1.0",
    513                 int64("HTTP/1.1") => .@"HTTP/1.1",
    514                 else => return error.HttpHeadersInvalid,
    515             };
    516             if (first_line[8] != ' ') return error.HttpHeadersInvalid;
    517             const status: http.Status = @enumFromInt(parseInt3(first_line[9..12]));
    518             const reason = mem.trimLeft(u8, first_line[12..], " ");
    519 
    520             res.version = version;
    521             res.status = status;
    522             res.reason = reason;
    523             res.keep_alive = switch (version) {
    524                 .@"HTTP/1.0" => false,
    525                 .@"HTTP/1.1" => true,
    526             };
    527 
    528             while (it.next()) |line| {
    529                 if (line.len == 0) return res;
    530                 switch (line[0]) {
    531                     ' ', '\t' => return error.HttpHeaderContinuationsUnsupported,
    532                     else => {},
    533                 }
    534 
    535                 var line_it = mem.splitScalar(u8, line, ':');
    536                 const header_name = line_it.next().?;
    537                 const header_value = mem.trim(u8, line_it.rest(), " \t");
    538                 if (header_name.len == 0) return error.HttpHeadersInvalid;
    539 
    540                 if (std.ascii.eqlIgnoreCase(header_name, "connection")) {
    541                     res.keep_alive = !std.ascii.eqlIgnoreCase(header_value, "close");
    542                 } else if (std.ascii.eqlIgnoreCase(header_name, "content-type")) {
    543                     res.content_type = header_value;
    544                 } else if (std.ascii.eqlIgnoreCase(header_name, "location")) {
    545                     res.location = header_value;
    546                 } else if (std.ascii.eqlIgnoreCase(header_name, "content-disposition")) {
    547                     res.content_disposition = header_value;
    548                 } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) {
    549                     // Transfer-Encoding: second, first
    550                     // Transfer-Encoding: deflate, chunked
    551                     var iter = mem.splitBackwardsScalar(u8, header_value, ',');
    552 
    553                     const first = iter.first();
    554                     const trimmed_first = mem.trim(u8, first, " ");
    555 
    556                     var next: ?[]const u8 = first;
    557                     if (std.meta.stringToEnum(http.TransferEncoding, trimmed_first)) |transfer| {
    558                         if (res.transfer_encoding != .none) return error.HttpHeadersInvalid; // we already have a transfer encoding
    559                         res.transfer_encoding = transfer;
    560 
    561                         next = iter.next();
    562                     }
    563 
    564                     if (next) |second| {
    565                         const trimmed_second = mem.trim(u8, second, " ");
    566 
    567                         if (http.ContentEncoding.fromString(trimmed_second)) |transfer| {
    568                             if (res.content_encoding != .identity) return error.HttpHeadersInvalid; // double compression is not supported
    569                             res.content_encoding = transfer;
    570                         } else {
    571                             return error.HttpTransferEncodingUnsupported;
    572                         }
    573                     }
    574 
    575                     if (iter.next()) |_| return error.HttpTransferEncodingUnsupported;
    576                 } else if (std.ascii.eqlIgnoreCase(header_name, "content-length")) {
    577                     const content_length = std.fmt.parseInt(u64, header_value, 10) catch return error.InvalidContentLength;
    578 
    579                     if (res.content_length != null and res.content_length != content_length) return error.HttpHeadersInvalid;
    580 
    581                     res.content_length = content_length;
    582                 } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) {
    583                     if (res.content_encoding != .identity) return error.HttpHeadersInvalid;
    584 
    585                     const trimmed = mem.trim(u8, header_value, " ");
    586 
    587                     if (http.ContentEncoding.fromString(trimmed)) |ce| {
    588                         res.content_encoding = ce;
    589                     } else {
    590                         return error.HttpContentEncodingUnsupported;
    591                     }
    592                 }
    593             }
    594             return error.HttpHeadersInvalid; // missing empty line
    595         }
    596 
    597         test parse {
    598             const response_bytes = "HTTP/1.1 200 OK\r\n" ++
    599                 "LOcation:url\r\n" ++
    600                 "content-tYpe: text/plain\r\n" ++
    601                 "content-disposition:attachment; filename=example.txt \r\n" ++
    602                 "content-Length:10\r\n" ++
    603                 "TRansfer-encoding:\tdeflate, chunked \r\n" ++
    604                 "connectioN:\t keep-alive \r\n\r\n";
    605 
    606             const head = try Head.parse(response_bytes);
    607 
    608             try testing.expectEqual(.@"HTTP/1.1", head.version);
    609             try testing.expectEqualStrings("OK", head.reason);
    610             try testing.expectEqual(.ok, head.status);
    611 
    612             try testing.expectEqualStrings("url", head.location.?);
    613             try testing.expectEqualStrings("text/plain", head.content_type.?);
    614             try testing.expectEqualStrings("attachment; filename=example.txt", head.content_disposition.?);
    615 
    616             try testing.expectEqual(true, head.keep_alive);
    617             try testing.expectEqual(10, head.content_length.?);
    618             try testing.expectEqual(.chunked, head.transfer_encoding);
    619             try testing.expectEqual(.deflate, head.content_encoding);
    620         }
    621 
    622         pub fn iterateHeaders(h: Head) http.HeaderIterator {
    623             return .init(h.bytes);
    624         }
    625 
    626         test iterateHeaders {
    627             const response_bytes = "HTTP/1.1 200 OK\r\n" ++
    628                 "LOcation:url\r\n" ++
    629                 "content-tYpe: text/plain\r\n" ++
    630                 "content-disposition:attachment; filename=example.txt \r\n" ++
    631                 "content-Length:10\r\n" ++
    632                 "TRansfer-encoding:\tdeflate, chunked \r\n" ++
    633                 "connectioN:\t keep-alive \r\n\r\n";
    634 
    635             const head = try Head.parse(response_bytes);
    636             var it = head.iterateHeaders();
    637             {
    638                 const header = it.next().?;
    639                 try testing.expectEqualStrings("LOcation", header.name);
    640                 try testing.expectEqualStrings("url", header.value);
    641                 try testing.expect(!it.is_trailer);
    642             }
    643             {
    644                 const header = it.next().?;
    645                 try testing.expectEqualStrings("content-tYpe", header.name);
    646                 try testing.expectEqualStrings("text/plain", header.value);
    647                 try testing.expect(!it.is_trailer);
    648             }
    649             {
    650                 const header = it.next().?;
    651                 try testing.expectEqualStrings("content-disposition", header.name);
    652                 try testing.expectEqualStrings("attachment; filename=example.txt", header.value);
    653                 try testing.expect(!it.is_trailer);
    654             }
    655             {
    656                 const header = it.next().?;
    657                 try testing.expectEqualStrings("content-Length", header.name);
    658                 try testing.expectEqualStrings("10", header.value);
    659                 try testing.expect(!it.is_trailer);
    660             }
    661             {
    662                 const header = it.next().?;
    663                 try testing.expectEqualStrings("TRansfer-encoding", header.name);
    664                 try testing.expectEqualStrings("deflate, chunked", header.value);
    665                 try testing.expect(!it.is_trailer);
    666             }
    667             {
    668                 const header = it.next().?;
    669                 try testing.expectEqualStrings("connectioN", header.name);
    670                 try testing.expectEqualStrings("keep-alive", header.value);
    671                 try testing.expect(!it.is_trailer);
    672             }
    673             try testing.expectEqual(null, it.next());
    674         }
    675 
    676         inline fn int64(array: *const [8]u8) u64 {
    677             return @bitCast(array.*);
    678         }
    679 
    680         fn parseInt3(text: *const [3]u8) u10 {
    681             const nnn: @Vector(3, u8) = text.*;
    682             const zero: @Vector(3, u8) = .{ '0', '0', '0' };
    683             const mmm: @Vector(3, u10) = .{ 100, 10, 1 };
    684             return @reduce(.Add, (nnn -% zero) *% mmm);
    685         }
    686 
    687         test parseInt3 {
    688             const expectEqual = testing.expectEqual;
    689             try expectEqual(@as(u10, 0), parseInt3("000"));
    690             try expectEqual(@as(u10, 418), parseInt3("418"));
    691             try expectEqual(@as(u10, 999), parseInt3("999"));
    692         }
    693 
    694         /// Help the programmer avoid bugs by calling this when the string
    695         /// memory of `Head` becomes invalidated.
    696         fn invalidateStrings(h: *Head) void {
    697             h.bytes = undefined;
    698             h.reason = undefined;
    699             if (h.location) |*s| s.* = undefined;
    700             if (h.content_type) |*s| s.* = undefined;
    701             if (h.content_disposition) |*s| s.* = undefined;
    702         }
    703     };
    704 
    705     /// If compressed body has been negotiated this will return compressed bytes.
    706     ///
    707     /// If the returned `Reader` returns `error.ReadFailed` the error is
    708     /// available via `bodyErr`.
    709     ///
    710     /// Asserts that this function is only called once.
    711     ///
    712     /// See also:
    713     /// * `readerDecompressing`
    714     pub fn reader(response: *Response, transfer_buffer: []u8) *Reader {
    715         response.head.invalidateStrings();
    716         const req = response.request;
    717         if (!req.method.responseHasBody()) return .ending;
    718         const head = &response.head;
    719         return req.reader.bodyReader(transfer_buffer, head.transfer_encoding, head.content_length);
    720     }
    721 
    722     /// If compressed body has been negotiated this will return decompressed bytes.
    723     ///
    724     /// If the returned `Reader` returns `error.ReadFailed` the error is
    725     /// available via `bodyErr`.
    726     ///
    727     /// Asserts that this function is only called once.
    728     ///
    729     /// See also:
    730     /// * `reader`
    731     pub fn readerDecompressing(
    732         response: *Response,
    733         transfer_buffer: []u8,
    734         decompress: *http.Decompress,
    735         decompress_buffer: []u8,
    736     ) *Reader {
    737         response.head.invalidateStrings();
    738         const head = &response.head;
    739         return response.request.reader.bodyReaderDecompressing(
    740             transfer_buffer,
    741             head.transfer_encoding,
    742             head.content_length,
    743             head.content_encoding,
    744             decompress,
    745             decompress_buffer,
    746         );
    747     }
    748 
    749     /// After receiving `error.ReadFailed` from the `Reader` returned by
    750     /// `reader` or `readerDecompressing`, this function accesses the
    751     /// more specific error code.
    752     pub fn bodyErr(response: *const Response) ?http.Reader.BodyError {
    753         return response.request.reader.body_err;
    754     }
    755 
    756     pub fn iterateTrailers(response: *const Response) http.HeaderIterator {
    757         const r = &response.request.reader;
    758         assert(r.state == .ready);
    759         return .{
    760             .bytes = r.trailers,
    761             .index = 0,
    762             .is_trailer = true,
    763         };
    764     }
    765 };
    766 
    767 pub const Request = struct {
    768     /// This field is provided so that clients can observe redirected URIs.
    769     ///
    770     /// Its backing memory is externally provided by API users when creating a
    771     /// request, and then again provided externally via `redirect_buffer` to
    772     /// `receiveHead`.
    773     uri: Uri,
    774     client: *Client,
    775     /// This is null when the connection is released.
    776     connection: ?*Connection,
    777     reader: http.Reader,
    778     keep_alive: bool,
    779 
    780     method: http.Method,
    781     version: http.Version = .@"HTTP/1.1",
    782     transfer_encoding: TransferEncoding,
    783     redirect_behavior: RedirectBehavior,
    784     accept_encoding: @TypeOf(default_accept_encoding) = default_accept_encoding,
    785 
    786     /// Whether the request should handle a 100-continue response before sending the request body.
    787     handle_continue: bool,
    788 
    789     /// Standard headers that have default, but overridable, behavior.
    790     headers: Headers,
    791 
    792     /// Populated in `receiveHead`; used in `deinit` to determine whether to
    793     /// discard the body to reuse the connection.
    794     response_content_length: ?u64 = null,
    795     /// Populated in `receiveHead`; used in `deinit` to determine whether to
    796     /// discard the body to reuse the connection.
    797     response_transfer_encoding: http.TransferEncoding = .none,
    798 
    799     /// These headers are kept including when following a redirect to a
    800     /// different domain.
    801     /// Externally-owned; must outlive the Request.
    802     extra_headers: []const http.Header,
    803 
    804     /// These headers are stripped when following a redirect to a different
    805     /// domain.
    806     /// Externally-owned; must outlive the Request.
    807     privileged_headers: []const http.Header,
    808 
    809     pub const default_accept_encoding: [@typeInfo(http.ContentEncoding).@"enum".fields.len]bool = b: {
    810         var result: [@typeInfo(http.ContentEncoding).@"enum".fields.len]bool = @splat(false);
    811         result[@intFromEnum(http.ContentEncoding.gzip)] = true;
    812         result[@intFromEnum(http.ContentEncoding.deflate)] = true;
    813         result[@intFromEnum(http.ContentEncoding.identity)] = true;
    814         break :b result;
    815     };
    816 
    817     pub const TransferEncoding = union(enum) {
    818         content_length: u64,
    819         chunked: void,
    820         none: void,
    821     };
    822 
    823     pub const Headers = struct {
    824         host: Value = .default,
    825         authorization: Value = .default,
    826         user_agent: Value = .default,
    827         connection: Value = .default,
    828         accept_encoding: Value = .default,
    829         content_type: Value = .default,
    830 
    831         pub const Value = union(enum) {
    832             default,
    833             omit,
    834             override: []const u8,
    835         };
    836     };
    837 
    838     /// Any value other than `not_allowed` or `unhandled` means that integer represents
    839     /// how many remaining redirects are allowed.
    840     pub const RedirectBehavior = enum(u16) {
    841         /// The next redirect will cause an error.
    842         not_allowed = 0,
    843         /// Redirects are passed to the client to analyze the redirect response
    844         /// directly.
    845         unhandled = std.math.maxInt(u16),
    846         _,
    847 
    848         pub fn init(n: u16) RedirectBehavior {
    849             assert(n != std.math.maxInt(u16));
    850             return @enumFromInt(n);
    851         }
    852 
    853         pub fn subtractOne(rb: *RedirectBehavior) void {
    854             switch (rb.*) {
    855                 .not_allowed => unreachable,
    856                 .unhandled => unreachable,
    857                 _ => rb.* = @enumFromInt(@intFromEnum(rb.*) - 1),
    858             }
    859         }
    860 
    861         pub fn remaining(rb: RedirectBehavior) u16 {
    862             assert(rb != .unhandled);
    863             return @intFromEnum(rb);
    864         }
    865     };
    866 
    867     /// Returns the request's `Connection` back to the pool of the `Client`.
    868     pub fn deinit(r: *Request) void {
    869         if (r.connection) |connection| {
    870             connection.closing = connection.closing or switch (r.reader.state) {
    871                 .ready => false,
    872                 .received_head => c: {
    873                     if (r.method.requestHasBody()) break :c true;
    874                     if (!r.method.responseHasBody()) break :c false;
    875                     const reader = r.reader.bodyReader(&.{}, r.response_transfer_encoding, r.response_content_length);
    876                     _ = reader.discardRemaining() catch |err| switch (err) {
    877                         error.ReadFailed => break :c true,
    878                     };
    879                     break :c r.reader.state != .ready;
    880                 },
    881                 else => true,
    882             };
    883             r.client.connection_pool.release(connection);
    884         }
    885         r.* = undefined;
    886     }
    887 
    888     /// Sends and flushes a complete request as only HTTP head, no body.
    889     pub fn sendBodiless(r: *Request) Writer.Error!void {
    890         try sendBodilessUnflushed(r);
    891         try r.connection.?.flush();
    892     }
    893 
    894     /// Sends but does not flush a complete request as only HTTP head, no body.
    895     pub fn sendBodilessUnflushed(r: *Request) Writer.Error!void {
    896         assert(r.transfer_encoding == .none);
    897         assert(!r.method.requestHasBody());
    898         try sendHead(r);
    899     }
    900 
    901     /// Transfers the HTTP head over the connection and flushes.
    902     ///
    903     /// See also:
    904     /// * `sendBodyUnflushed`
    905     pub fn sendBody(r: *Request, buffer: []u8) Writer.Error!http.BodyWriter {
    906         const result = try sendBodyUnflushed(r, buffer);
    907         try r.connection.?.flush();
    908         return result;
    909     }
    910 
    911     /// Transfers the HTTP head and body over the connection and flushes.
    912     pub fn sendBodyComplete(r: *Request, body: []u8) Writer.Error!void {
    913         r.transfer_encoding = .{ .content_length = body.len };
    914         var bw = try sendBodyUnflushed(r, body);
    915         bw.writer.end = body.len;
    916         try bw.end();
    917         try r.connection.?.flush();
    918     }
    919 
    920     /// Transfers the HTTP head over the connection, which is not flushed until
    921     /// `BodyWriter.flush` or `BodyWriter.end` is called.
    922     ///
    923     /// See also:
    924     /// * `sendBody`
    925     pub fn sendBodyUnflushed(r: *Request, buffer: []u8) Writer.Error!http.BodyWriter {
    926         assert(r.method.requestHasBody());
    927         try sendHead(r);
    928         const http_protocol_output = r.connection.?.writer();
    929         return switch (r.transfer_encoding) {
    930             .chunked => .{
    931                 .http_protocol_output = http_protocol_output,
    932                 .state = .init_chunked,
    933                 .writer = .{
    934                     .buffer = buffer,
    935                     .vtable = &.{
    936                         .drain = http.BodyWriter.chunkedDrain,
    937                         .sendFile = http.BodyWriter.chunkedSendFile,
    938                     },
    939                 },
    940             },
    941             .content_length => |len| .{
    942                 .http_protocol_output = http_protocol_output,
    943                 .state = .{ .content_length = len },
    944                 .writer = .{
    945                     .buffer = buffer,
    946                     .vtable = &.{
    947                         .drain = http.BodyWriter.contentLengthDrain,
    948                         .sendFile = http.BodyWriter.contentLengthSendFile,
    949                     },
    950                 },
    951             },
    952             .none => .{
    953                 .http_protocol_output = http_protocol_output,
    954                 .state = .none,
    955                 .writer = .{
    956                     .buffer = buffer,
    957                     .vtable = &.{
    958                         .drain = http.BodyWriter.noneDrain,
    959                         .sendFile = http.BodyWriter.noneSendFile,
    960                     },
    961                 },
    962             },
    963         };
    964     }
    965 
    966     /// Sends HTTP headers without flushing.
    967     fn sendHead(r: *Request) Writer.Error!void {
    968         const uri = r.uri;
    969         const connection = r.connection.?;
    970         const w = connection.writer();
    971 
    972         try w.writeAll(@tagName(r.method));
    973         try w.writeByte(' ');
    974 
    975         if (r.method == .CONNECT) {
    976             try uri.writeToStream(w, .{ .authority = true });
    977         } else {
    978             try uri.writeToStream(w, .{
    979                 .scheme = connection.proxied,
    980                 .authentication = connection.proxied,
    981                 .authority = connection.proxied,
    982                 .path = true,
    983                 .query = true,
    984             });
    985         }
    986         try w.writeByte(' ');
    987         try w.writeAll(@tagName(r.version));
    988         try w.writeAll("\r\n");
    989 
    990         if (try emitOverridableHeader("host: ", r.headers.host, w)) {
    991             try w.writeAll("host: ");
    992             try uri.writeToStream(w, .{ .authority = true });
    993             try w.writeAll("\r\n");
    994         }
    995 
    996         if (try emitOverridableHeader("authorization: ", r.headers.authorization, w)) {
    997             if (uri.user != null or uri.password != null) {
    998                 try w.writeAll("authorization: ");
    999                 try basic_authorization.write(uri, w);
   1000                 try w.writeAll("\r\n");
   1001             }
   1002         }
   1003 
   1004         if (try emitOverridableHeader("user-agent: ", r.headers.user_agent, w)) {
   1005             try w.writeAll("user-agent: zig/");
   1006             try w.writeAll(builtin.zig_version_string);
   1007             try w.writeAll(" (std.http)\r\n");
   1008         }
   1009 
   1010         if (try emitOverridableHeader("connection: ", r.headers.connection, w)) {
   1011             if (r.keep_alive) {
   1012                 try w.writeAll("connection: keep-alive\r\n");
   1013             } else {
   1014                 try w.writeAll("connection: close\r\n");
   1015             }
   1016         }
   1017 
   1018         if (try emitOverridableHeader("accept-encoding: ", r.headers.accept_encoding, w)) {
   1019             try w.writeAll("accept-encoding: ");
   1020             for (r.accept_encoding, 0..) |enabled, i| {
   1021                 if (!enabled) continue;
   1022                 const tag: http.ContentEncoding = @enumFromInt(i);
   1023                 if (tag == .identity) continue;
   1024                 const tag_name = @tagName(tag);
   1025                 try w.ensureUnusedCapacity(tag_name.len + 2);
   1026                 try w.writeAll(tag_name);
   1027                 try w.writeAll(", ");
   1028             }
   1029             w.undo(2);
   1030             try w.writeAll("\r\n");
   1031         }
   1032 
   1033         switch (r.transfer_encoding) {
   1034             .chunked => try w.writeAll("transfer-encoding: chunked\r\n"),
   1035             .content_length => |len| try w.print("content-length: {d}\r\n", .{len}),
   1036             .none => {},
   1037         }
   1038 
   1039         if (try emitOverridableHeader("content-type: ", r.headers.content_type, w)) {
   1040             // The default is to omit content-type if not provided because
   1041             // "application/octet-stream" is redundant.
   1042         }
   1043 
   1044         for (r.extra_headers) |header| {
   1045             assert(header.name.len != 0);
   1046 
   1047             try w.writeAll(header.name);
   1048             try w.writeAll(": ");
   1049             try w.writeAll(header.value);
   1050             try w.writeAll("\r\n");
   1051         }
   1052 
   1053         if (connection.proxied) proxy: {
   1054             const proxy = switch (connection.protocol) {
   1055                 .plain => r.client.http_proxy,
   1056                 .tls => r.client.https_proxy,
   1057             } orelse break :proxy;
   1058 
   1059             const authorization = proxy.authorization orelse break :proxy;
   1060             try w.writeAll("proxy-authorization: ");
   1061             try w.writeAll(authorization);
   1062             try w.writeAll("\r\n");
   1063         }
   1064 
   1065         try w.writeAll("\r\n");
   1066     }
   1067 
   1068     pub const ReceiveHeadError = http.Reader.HeadError || ConnectError || error{
   1069         /// Server sent headers that did not conform to the HTTP protocol.
   1070         ///
   1071         /// To find out more detailed diagnostics, `http.Reader.head_buffer` can be
   1072         /// passed directly to `Request.Head.parse`.
   1073         HttpHeadersInvalid,
   1074         TooManyHttpRedirects,
   1075         /// This can be avoided by calling `receiveHead` before sending the
   1076         /// request body.
   1077         RedirectRequiresResend,
   1078         HttpRedirectLocationMissing,
   1079         HttpRedirectLocationOversize,
   1080         HttpRedirectLocationInvalid,
   1081         HttpContentEncodingUnsupported,
   1082         HttpChunkInvalid,
   1083         HttpChunkTruncated,
   1084         HttpHeadersOversize,
   1085         UnsupportedUriScheme,
   1086 
   1087         /// Sending the request failed. Error code can be found on the
   1088         /// `Connection` object.
   1089         WriteFailed,
   1090     };
   1091 
   1092     /// If handling redirects and the request has no payload, then this
   1093     /// function will automatically follow redirects.
   1094     ///
   1095     /// If a request payload is present, then this function will error with
   1096     /// `error.RedirectRequiresResend`.
   1097     ///
   1098     /// This function takes an auxiliary buffer to store the arbitrarily large
   1099     /// URI which may need to be merged with the previous URI, and that data
   1100     /// needs to survive across different connections, which is where the input
   1101     /// buffer lives.
   1102     ///
   1103     /// `redirect_buffer` must outlive accesses to `Request.uri`. If this
   1104     /// buffer capacity would be exceeded, `error.HttpRedirectLocationOversize`
   1105     /// is returned instead. This buffer may be empty if no redirects are to be
   1106     /// handled.
   1107     ///
   1108     /// If this fails with `error.ReadFailed` then the `Connection.getReadError`
   1109     /// method of `r.connection` can be used to get more detailed information.
   1110     pub fn receiveHead(r: *Request, redirect_buffer: []u8) ReceiveHeadError!Response {
   1111         var aux_buf = redirect_buffer;
   1112         while (true) {
   1113             const head_buffer = try r.reader.receiveHead();
   1114             const response: Response = .{
   1115                 .request = r,
   1116                 .head = Response.Head.parse(head_buffer) catch return error.HttpHeadersInvalid,
   1117             };
   1118             const head = &response.head;
   1119 
   1120             if (head.status == .@"continue") {
   1121                 if (r.handle_continue) continue;
   1122                 r.response_transfer_encoding = head.transfer_encoding;
   1123                 r.response_content_length = head.content_length;
   1124                 return response; // we're not handling the 100-continue
   1125             }
   1126 
   1127             // This while loop is for handling redirects, which means the request's
   1128             // connection may be different than the previous iteration. However, it
   1129             // is still guaranteed to be non-null with each iteration of this loop.
   1130             const connection = r.connection.?;
   1131 
   1132             if (r.method == .CONNECT and head.status.class() == .success) {
   1133                 // This connection is no longer doing HTTP.
   1134                 connection.closing = false;
   1135                 r.response_transfer_encoding = head.transfer_encoding;
   1136                 r.response_content_length = head.content_length;
   1137                 return response;
   1138             }
   1139 
   1140             connection.closing = !head.keep_alive or !r.keep_alive;
   1141 
   1142             // Any response to a HEAD request and any response with a 1xx
   1143             // (Informational), 204 (No Content), or 304 (Not Modified) status
   1144             // code is always terminated by the first empty line after the
   1145             // header fields, regardless of the header fields present in the
   1146             // message.
   1147             if (r.method == .HEAD or head.status.class() == .informational or
   1148                 head.status == .no_content or head.status == .not_modified)
   1149             {
   1150                 r.response_transfer_encoding = head.transfer_encoding;
   1151                 r.response_content_length = head.content_length;
   1152                 return response;
   1153             }
   1154 
   1155             if (head.status.class() == .redirect and r.redirect_behavior != .unhandled) {
   1156                 if (r.redirect_behavior == .not_allowed) {
   1157                     // Connection can still be reused by skipping the body.
   1158                     const reader = r.reader.bodyReader(&.{}, head.transfer_encoding, head.content_length);
   1159                     _ = reader.discardRemaining() catch |err| switch (err) {
   1160                         error.ReadFailed => connection.closing = true,
   1161                     };
   1162                     return error.TooManyHttpRedirects;
   1163                 }
   1164                 try r.redirect(head, &aux_buf);
   1165                 try r.sendBodiless();
   1166                 continue;
   1167             }
   1168 
   1169             if (!r.accept_encoding[@intFromEnum(head.content_encoding)])
   1170                 return error.HttpContentEncodingUnsupported;
   1171 
   1172             r.response_transfer_encoding = head.transfer_encoding;
   1173             r.response_content_length = head.content_length;
   1174             return response;
   1175         }
   1176     }
   1177 
   1178     /// This function takes an auxiliary buffer to store the arbitrarily large
   1179     /// URI which may need to be merged with the previous URI, and that data
   1180     /// needs to survive across different connections, which is where the input
   1181     /// buffer lives.
   1182     ///
   1183     /// `aux_buf` must outlive accesses to `Request.uri`.
   1184     fn redirect(r: *Request, head: *const Response.Head, aux_buf: *[]u8) !void {
   1185         const new_location = head.location orelse return error.HttpRedirectLocationMissing;
   1186         if (new_location.len > aux_buf.*.len) return error.HttpRedirectLocationOversize;
   1187         const location = aux_buf.*[0..new_location.len];
   1188         @memcpy(location, new_location);
   1189         {
   1190             // Skip the body of the redirect response to leave the connection in
   1191             // the correct state. This causes `new_location` to be invalidated.
   1192             const reader = r.reader.bodyReader(&.{}, head.transfer_encoding, head.content_length);
   1193             _ = reader.discardRemaining() catch |err| switch (err) {
   1194                 error.ReadFailed => return r.reader.body_err.?,
   1195             };
   1196         }
   1197         const new_uri = r.uri.resolveInPlace(location.len, aux_buf) catch |err| switch (err) {
   1198             error.UnexpectedCharacter => return error.HttpRedirectLocationInvalid,
   1199             error.InvalidFormat => return error.HttpRedirectLocationInvalid,
   1200             error.InvalidPort => return error.HttpRedirectLocationInvalid,
   1201             error.NoSpaceLeft => return error.HttpRedirectLocationOversize,
   1202         };
   1203 
   1204         const protocol = Protocol.fromUri(new_uri) orelse return error.UnsupportedUriScheme;
   1205         const old_connection = r.connection.?;
   1206         const old_host = old_connection.host();
   1207         var new_host_name_buffer: [Uri.host_name_max]u8 = undefined;
   1208         const new_host = try new_uri.getHost(&new_host_name_buffer);
   1209         const keep_privileged_headers =
   1210             std.ascii.eqlIgnoreCase(r.uri.scheme, new_uri.scheme) and
   1211             sameParentDomain(old_host, new_host);
   1212 
   1213         r.client.connection_pool.release(old_connection);
   1214         r.connection = null;
   1215 
   1216         if (!keep_privileged_headers) {
   1217             // When redirecting to a different domain, strip privileged headers.
   1218             r.privileged_headers = &.{};
   1219         }
   1220 
   1221         if (switch (head.status) {
   1222             .see_other => true,
   1223             .moved_permanently, .found => r.method == .POST,
   1224             else => false,
   1225         }) {
   1226             // A redirect to a GET must change the method and remove the body.
   1227             r.method = .GET;
   1228             r.transfer_encoding = .none;
   1229             r.headers.content_type = .omit;
   1230         }
   1231 
   1232         if (r.transfer_encoding != .none) {
   1233             // The request body has already been sent. The request is
   1234             // still in a valid state, but the redirect must be handled
   1235             // manually.
   1236             return error.RedirectRequiresResend;
   1237         }
   1238 
   1239         const new_connection = try r.client.connect(new_host, uriPort(new_uri, protocol), protocol);
   1240         r.uri = new_uri;
   1241         r.connection = new_connection;
   1242         r.reader = .{
   1243             .in = new_connection.reader(),
   1244             .state = .ready,
   1245             // Populated when `http.Reader.bodyReader` is called.
   1246             .interface = undefined,
   1247             .max_head_len = r.client.read_buffer_size,
   1248         };
   1249         r.redirect_behavior.subtractOne();
   1250     }
   1251 
   1252     /// Returns true if the default behavior is required, otherwise handles
   1253     /// writing (or not writing) the header.
   1254     fn emitOverridableHeader(prefix: []const u8, v: Headers.Value, bw: *Writer) Writer.Error!bool {
   1255         switch (v) {
   1256             .default => return true,
   1257             .omit => return false,
   1258             .override => |x| {
   1259                 var vecs: [3][]const u8 = .{ prefix, x, "\r\n" };
   1260                 try bw.writeVecAll(&vecs);
   1261                 return false;
   1262             },
   1263         }
   1264     }
   1265 };
   1266 
   1267 pub const Proxy = struct {
   1268     protocol: Protocol,
   1269     host: []const u8,
   1270     authorization: ?[]const u8,
   1271     port: u16,
   1272     supports_connect: bool,
   1273 };
   1274 
   1275 /// Release all associated resources with the client.
   1276 ///
   1277 /// All pending requests must be de-initialized and all active connections released
   1278 /// before calling this function.
   1279 pub fn deinit(client: *Client) void {
   1280     assert(client.connection_pool.used.first == null); // There are still active requests.
   1281 
   1282     client.connection_pool.deinit();
   1283     if (!disable_tls) client.ca_bundle.deinit(client.allocator);
   1284 
   1285     client.* = undefined;
   1286 }
   1287 
   1288 /// Populates `http_proxy` and `https_proxy` via standard proxy environment variables.
   1289 /// Asserts the client has no active connections.
   1290 /// Uses `arena` for a few small allocations that must outlive the client, or
   1291 /// at least until those fields are set to different values.
   1292 pub fn initDefaultProxies(client: *Client, arena: Allocator) !void {
   1293     // Prevent any new connections from being created.
   1294     client.connection_pool.mutex.lock();
   1295     defer client.connection_pool.mutex.unlock();
   1296 
   1297     assert(client.connection_pool.used.first == null); // There are active requests.
   1298 
   1299     if (client.http_proxy == null) {
   1300         client.http_proxy = try createProxyFromEnvVar(arena, &.{
   1301             "http_proxy", "HTTP_PROXY", "all_proxy", "ALL_PROXY",
   1302         });
   1303     }
   1304 
   1305     if (client.https_proxy == null) {
   1306         client.https_proxy = try createProxyFromEnvVar(arena, &.{
   1307             "https_proxy", "HTTPS_PROXY", "all_proxy", "ALL_PROXY",
   1308         });
   1309     }
   1310 }
   1311 
   1312 fn createProxyFromEnvVar(arena: Allocator, env_var_names: []const []const u8) !?*Proxy {
   1313     const content = for (env_var_names) |name| {
   1314         const content = std.process.getEnvVarOwned(arena, name) catch |err| switch (err) {
   1315             error.EnvironmentVariableNotFound => continue,
   1316             else => |e| return e,
   1317         };
   1318 
   1319         if (content.len == 0) continue;
   1320 
   1321         break content;
   1322     } else return null;
   1323 
   1324     const uri = Uri.parse(content) catch try Uri.parseAfterScheme("http", content);
   1325     const protocol = Protocol.fromUri(uri) orelse return null;
   1326     const raw_host = try uri.getHostAlloc(arena);
   1327 
   1328     const authorization: ?[]const u8 = if (uri.user != null or uri.password != null) a: {
   1329         const authorization = try arena.alloc(u8, basic_authorization.valueLengthFromUri(uri));
   1330         assert(basic_authorization.value(uri, authorization).len == authorization.len);
   1331         break :a authorization;
   1332     } else null;
   1333 
   1334     const proxy = try arena.create(Proxy);
   1335     proxy.* = .{
   1336         .protocol = protocol,
   1337         .host = raw_host,
   1338         .authorization = authorization,
   1339         .port = uriPort(uri, protocol),
   1340         .supports_connect = true,
   1341     };
   1342     return proxy;
   1343 }
   1344 
   1345 pub const basic_authorization = struct {
   1346     pub const max_user_len = 255;
   1347     pub const max_password_len = 255;
   1348     pub const max_value_len = valueLength(max_user_len, max_password_len);
   1349 
   1350     pub fn valueLength(user_len: usize, password_len: usize) usize {
   1351         return "Basic ".len + std.base64.standard.Encoder.calcSize(user_len + 1 + password_len);
   1352     }
   1353 
   1354     pub fn valueLengthFromUri(uri: Uri) usize {
   1355         const user: Uri.Component = uri.user orelse .empty;
   1356         const password: Uri.Component = uri.password orelse .empty;
   1357 
   1358         var dw: Writer.Discarding = .init(&.{});
   1359         user.formatUser(&dw.writer) catch unreachable; // discarding
   1360         const user_len = dw.count + dw.writer.end;
   1361 
   1362         dw.count = 0;
   1363         dw.writer.end = 0;
   1364         password.formatPassword(&dw.writer) catch unreachable; // discarding
   1365         const password_len = dw.count + dw.writer.end;
   1366 
   1367         return valueLength(@intCast(user_len), @intCast(password_len));
   1368     }
   1369 
   1370     pub fn value(uri: Uri, out: []u8) []u8 {
   1371         var bw: Writer = .fixed(out);
   1372         write(uri, &bw) catch unreachable;
   1373         return bw.buffered();
   1374     }
   1375 
   1376     pub fn write(uri: Uri, out: *Writer) Writer.Error!void {
   1377         var buf: [max_user_len + 1 + max_password_len]u8 = undefined;
   1378         var w: Writer = .fixed(&buf);
   1379         const user: Uri.Component = uri.user orelse .empty;
   1380         const password: Uri.Component = uri.password orelse .empty;
   1381         user.formatUser(&w) catch unreachable;
   1382         w.writeByte(':') catch unreachable;
   1383         password.formatPassword(&w) catch unreachable;
   1384         try out.print("Basic {b64}", .{w.buffered()});
   1385     }
   1386 };
   1387 
   1388 pub const ConnectTcpError = Allocator.Error || error{
   1389     ConnectionRefused,
   1390     NetworkUnreachable,
   1391     ConnectionTimedOut,
   1392     ConnectionResetByPeer,
   1393     TemporaryNameServerFailure,
   1394     NameServerFailure,
   1395     UnknownHostName,
   1396     HostLacksNetworkAddresses,
   1397     UnexpectedConnectFailure,
   1398     TlsInitializationFailed,
   1399 };
   1400 
   1401 /// Reuses a `Connection` if one matching `host` and `port` is already open.
   1402 ///
   1403 /// Threadsafe.
   1404 pub fn connectTcp(
   1405     client: *Client,
   1406     host: []const u8,
   1407     port: u16,
   1408     protocol: Protocol,
   1409 ) ConnectTcpError!*Connection {
   1410     return connectTcpOptions(client, .{ .host = host, .port = port, .protocol = protocol });
   1411 }
   1412 
   1413 pub const ConnectTcpOptions = struct {
   1414     host: Io.net.HostName,
   1415     port: u16,
   1416     protocol: Protocol,
   1417 
   1418     proxied_host: ?[]const u8 = null,
   1419     proxied_port: ?u16 = null,
   1420 };
   1421 
   1422 pub fn connectTcpOptions(client: *Client, options: ConnectTcpOptions) ConnectTcpError!*Connection {
   1423     const host = options.host_name;
   1424     const port = options.port;
   1425     const protocol = options.protocol;
   1426 
   1427     const proxied_host = options.proxied_host orelse host;
   1428     const proxied_port = options.proxied_port orelse port;
   1429 
   1430     if (client.connection_pool.findConnection(.{
   1431         .host = proxied_host,
   1432         .port = proxied_port,
   1433         .protocol = protocol,
   1434     })) |conn| return conn;
   1435 
   1436     const stream = host.connectTcp(client.io, port) catch |err| switch (err) {
   1437         error.ConnectionRefused => return error.ConnectionRefused,
   1438         error.NetworkUnreachable => return error.NetworkUnreachable,
   1439         error.ConnectionTimedOut => return error.ConnectionTimedOut,
   1440         error.ConnectionResetByPeer => return error.ConnectionResetByPeer,
   1441         error.TemporaryNameServerFailure => return error.TemporaryNameServerFailure,
   1442         error.NameServerFailure => return error.NameServerFailure,
   1443         error.UnknownHostName => return error.UnknownHostName,
   1444         error.HostLacksNetworkAddresses => return error.HostLacksNetworkAddresses,
   1445         error.Canceled => return error.Canceled,
   1446         else => return error.UnexpectedConnectFailure,
   1447     };
   1448     errdefer stream.close();
   1449 
   1450     switch (protocol) {
   1451         .tls => {
   1452             if (disable_tls) return error.TlsInitializationFailed;
   1453             const tc = try Connection.Tls.create(client, proxied_host, proxied_port, stream);
   1454             client.connection_pool.addUsed(&tc.connection);
   1455             return &tc.connection;
   1456         },
   1457         .plain => {
   1458             const pc = try Connection.Plain.create(client, proxied_host, proxied_port, stream);
   1459             client.connection_pool.addUsed(&pc.connection);
   1460             return &pc.connection;
   1461         },
   1462     }
   1463 }
   1464 
   1465 pub const ConnectUnixError = Allocator.Error || std.posix.SocketError || error{NameTooLong} || std.posix.ConnectError;
   1466 
   1467 /// Connect to `path` as a unix domain socket. This will reuse a connection if one is already open.
   1468 ///
   1469 /// This function is threadsafe.
   1470 pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*Connection {
   1471     if (client.connection_pool.findConnection(.{
   1472         .host = path,
   1473         .port = 0,
   1474         .protocol = .plain,
   1475     })) |node|
   1476         return node;
   1477 
   1478     const conn = try client.allocator.create(ConnectionPool.Node);
   1479     errdefer client.allocator.destroy(conn);
   1480     conn.* = .{ .data = undefined };
   1481 
   1482     const stream = try std.net.connectUnixSocket(path);
   1483     errdefer stream.close();
   1484 
   1485     conn.data = .{
   1486         .stream = stream,
   1487         .tls_client = undefined,
   1488         .protocol = .plain,
   1489 
   1490         .host = try client.allocator.dupe(u8, path),
   1491         .port = 0,
   1492     };
   1493     errdefer client.allocator.free(conn.data.host);
   1494 
   1495     client.connection_pool.addUsed(conn);
   1496 
   1497     return &conn.data;
   1498 }
   1499 
   1500 /// Connect to `proxied_host:proxied_port` using the specified proxy with HTTP
   1501 /// CONNECT. This will reuse a connection if one is already open.
   1502 ///
   1503 /// This function is threadsafe.
   1504 pub fn connectProxied(
   1505     client: *Client,
   1506     proxy: *Proxy,
   1507     proxied_host: []const u8,
   1508     proxied_port: u16,
   1509 ) !*Connection {
   1510     if (!proxy.supports_connect) return error.TunnelNotSupported;
   1511 
   1512     if (client.connection_pool.findConnection(.{
   1513         .host = proxied_host,
   1514         .port = proxied_port,
   1515         .protocol = proxy.protocol,
   1516     })) |node| return node;
   1517 
   1518     var maybe_valid = false;
   1519     (tunnel: {
   1520         const connection = try client.connectTcpOptions(.{
   1521             .host = proxy.host,
   1522             .port = proxy.port,
   1523             .protocol = proxy.protocol,
   1524             .proxied_host = proxied_host,
   1525             .proxied_port = proxied_port,
   1526         });
   1527         errdefer {
   1528             connection.closing = true;
   1529             client.connection_pool.release(connection);
   1530         }
   1531 
   1532         var req = client.request(.CONNECT, .{
   1533             .scheme = "http",
   1534             .host = .{ .raw = proxied_host },
   1535             .port = proxied_port,
   1536         }, .{
   1537             .redirect_behavior = .unhandled,
   1538             .connection = connection,
   1539         }) catch |err| {
   1540             break :tunnel err;
   1541         };
   1542         defer req.deinit();
   1543 
   1544         req.sendBodiless() catch |err| break :tunnel err;
   1545         const response = req.receiveHead(&.{}) catch |err| break :tunnel err;
   1546 
   1547         if (response.head.status.class() == .server_error) {
   1548             maybe_valid = true;
   1549             break :tunnel error.ServerError;
   1550         }
   1551 
   1552         if (response.head.status != .ok) break :tunnel error.ConnectionRefused;
   1553 
   1554         // this connection is now a tunnel, so we can't use it for anything
   1555         // else, it will only be released when the client is de-initialized.
   1556         req.connection = null;
   1557 
   1558         connection.closing = false;
   1559 
   1560         return connection;
   1561     }) catch {
   1562         // something went wrong with the tunnel
   1563         proxy.supports_connect = maybe_valid;
   1564         return error.TunnelNotSupported;
   1565     };
   1566 }
   1567 
   1568 pub const ConnectError = ConnectTcpError || RequestError;
   1569 
   1570 /// Connect to `host:port` using the specified protocol. This will reuse a
   1571 /// connection if one is already open.
   1572 ///
   1573 /// If a proxy is configured for the client, then the proxy will be used to
   1574 /// connect to the host.
   1575 ///
   1576 /// This function is threadsafe.
   1577 pub fn connect(
   1578     client: *Client,
   1579     host: []const u8,
   1580     port: u16,
   1581     protocol: Protocol,
   1582 ) ConnectError!*Connection {
   1583     const proxy = switch (protocol) {
   1584         .plain => client.http_proxy,
   1585         .tls => client.https_proxy,
   1586     } orelse return client.connectTcp(host, port, protocol);
   1587 
   1588     // Prevent proxying through itself.
   1589     if (std.ascii.eqlIgnoreCase(proxy.host, host) and
   1590         proxy.port == port and proxy.protocol == protocol)
   1591     {
   1592         return client.connectTcp(host, port, protocol);
   1593     }
   1594 
   1595     if (proxy.supports_connect) tunnel: {
   1596         return connectProxied(client, proxy, host, port) catch |err| switch (err) {
   1597             error.TunnelNotSupported => break :tunnel,
   1598             else => |e| return e,
   1599         };
   1600     }
   1601 
   1602     // fall back to using the proxy as a normal http proxy
   1603     const connection = try client.connectTcp(proxy.host, proxy.port, proxy.protocol);
   1604     connection.proxied = true;
   1605     return connection;
   1606 }
   1607 
   1608 pub const RequestError = ConnectTcpError || error{
   1609     UnsupportedUriScheme,
   1610     UriMissingHost,
   1611     UriHostTooLong,
   1612     CertificateBundleLoadFailure,
   1613 };
   1614 
   1615 pub const RequestOptions = struct {
   1616     version: http.Version = .@"HTTP/1.1",
   1617 
   1618     /// Automatically ignore 100 Continue responses. This assumes you don't
   1619     /// care, and will have sent the body before you wait for the response.
   1620     ///
   1621     /// If this is not the case AND you know the server will send a 100
   1622     /// Continue, set this to false and wait for a response before sending the
   1623     /// body. If you wait AND the server does not send a 100 Continue before
   1624     /// you finish the request, then the request *will* deadlock.
   1625     handle_continue: bool = true,
   1626 
   1627     /// If false, close the connection after the one request. If true,
   1628     /// participate in the client connection pool.
   1629     keep_alive: bool = true,
   1630 
   1631     /// This field specifies whether to automatically follow redirects, and if
   1632     /// so, how many redirects to follow before returning an error.
   1633     ///
   1634     /// This will only follow redirects for repeatable requests (ie. with no
   1635     /// payload or the server has acknowledged the payload).
   1636     redirect_behavior: Request.RedirectBehavior = @enumFromInt(3),
   1637 
   1638     /// Must be an already acquired connection.
   1639     connection: ?*Connection = null,
   1640 
   1641     /// Standard headers that have default, but overridable, behavior.
   1642     headers: Request.Headers = .{},
   1643     /// These headers are kept including when following a redirect to a
   1644     /// different domain.
   1645     /// Externally-owned; must outlive the Request.
   1646     extra_headers: []const http.Header = &.{},
   1647     /// These headers are stripped when following a redirect to a different
   1648     /// domain.
   1649     /// Externally-owned; must outlive the Request.
   1650     privileged_headers: []const http.Header = &.{},
   1651 };
   1652 
   1653 fn uriPort(uri: Uri, protocol: Protocol) u16 {
   1654     return uri.port orelse protocol.port();
   1655 }
   1656 
   1657 /// Open a connection to the host specified by `uri` and prepare to send a HTTP request.
   1658 ///
   1659 /// The caller is responsible for calling `deinit()` on the `Request`.
   1660 /// This function is threadsafe.
   1661 ///
   1662 /// Asserts that "\r\n" does not occur in any header name or value.
   1663 pub fn request(
   1664     client: *Client,
   1665     method: http.Method,
   1666     uri: Uri,
   1667     options: RequestOptions,
   1668 ) RequestError!Request {
   1669     if (std.debug.runtime_safety) {
   1670         for (options.extra_headers) |header| {
   1671             assert(header.name.len != 0);
   1672             assert(std.mem.indexOfScalar(u8, header.name, ':') == null);
   1673             assert(std.mem.indexOfPosLinear(u8, header.name, 0, "\r\n") == null);
   1674             assert(std.mem.indexOfPosLinear(u8, header.value, 0, "\r\n") == null);
   1675         }
   1676         for (options.privileged_headers) |header| {
   1677             assert(header.name.len != 0);
   1678             assert(std.mem.indexOfPosLinear(u8, header.name, 0, "\r\n") == null);
   1679             assert(std.mem.indexOfPosLinear(u8, header.value, 0, "\r\n") == null);
   1680         }
   1681     }
   1682 
   1683     const protocol = Protocol.fromUri(uri) orelse return error.UnsupportedUriScheme;
   1684 
   1685     if (protocol == .tls) {
   1686         if (disable_tls) unreachable;
   1687         if (@atomicLoad(bool, &client.next_https_rescan_certs, .acquire)) {
   1688             client.ca_bundle_mutex.lock();
   1689             defer client.ca_bundle_mutex.unlock();
   1690 
   1691             if (client.next_https_rescan_certs) {
   1692                 client.ca_bundle.rescan(client.allocator) catch
   1693                     return error.CertificateBundleLoadFailure;
   1694                 @atomicStore(bool, &client.next_https_rescan_certs, false, .release);
   1695             }
   1696         }
   1697     }
   1698 
   1699     const connection = options.connection orelse c: {
   1700         var host_name_buffer: [Uri.host_name_max]u8 = undefined;
   1701         const host_name = try uri.getHost(&host_name_buffer);
   1702         break :c try client.connect(host_name, uriPort(uri, protocol), protocol);
   1703     };
   1704 
   1705     return .{
   1706         .uri = uri,
   1707         .client = client,
   1708         .connection = connection,
   1709         .reader = .{
   1710             .in = connection.reader(),
   1711             .state = .ready,
   1712             // Populated when `http.Reader.bodyReader` is called.
   1713             .interface = undefined,
   1714             .max_head_len = client.read_buffer_size,
   1715         },
   1716         .keep_alive = options.keep_alive,
   1717         .method = method,
   1718         .version = options.version,
   1719         .transfer_encoding = .none,
   1720         .redirect_behavior = options.redirect_behavior,
   1721         .handle_continue = options.handle_continue,
   1722         .headers = options.headers,
   1723         .extra_headers = options.extra_headers,
   1724         .privileged_headers = options.privileged_headers,
   1725     };
   1726 }
   1727 
   1728 pub const FetchOptions = struct {
   1729     /// `null` means it will be heap-allocated.
   1730     redirect_buffer: ?[]u8 = null,
   1731     /// `null` means it will be heap-allocated.
   1732     decompress_buffer: ?[]u8 = null,
   1733     redirect_behavior: ?Request.RedirectBehavior = null,
   1734     /// If the server sends a body, it will be written here.
   1735     response_writer: ?*Writer = null,
   1736 
   1737     location: Location,
   1738     method: ?http.Method = null,
   1739     payload: ?[]const u8 = null,
   1740     raw_uri: bool = false,
   1741     keep_alive: bool = true,
   1742 
   1743     /// Standard headers that have default, but overridable, behavior.
   1744     headers: Request.Headers = .{},
   1745     /// These headers are kept including when following a redirect to a
   1746     /// different domain.
   1747     /// Externally-owned; must outlive the Request.
   1748     extra_headers: []const http.Header = &.{},
   1749     /// These headers are stripped when following a redirect to a different
   1750     /// domain.
   1751     /// Externally-owned; must outlive the Request.
   1752     privileged_headers: []const http.Header = &.{},
   1753 
   1754     pub const Location = union(enum) {
   1755         url: []const u8,
   1756         uri: Uri,
   1757     };
   1758 };
   1759 
   1760 pub const FetchResult = struct {
   1761     status: http.Status,
   1762 };
   1763 
   1764 pub const FetchError = Uri.ParseError || RequestError || Request.ReceiveHeadError || error{
   1765     StreamTooLong,
   1766     /// TODO provide optional diagnostics when this occurs or break into more error codes
   1767     WriteFailed,
   1768     UnsupportedCompressionMethod,
   1769 };
   1770 
   1771 /// Perform a one-shot HTTP request with the provided options.
   1772 ///
   1773 /// This function is threadsafe.
   1774 pub fn fetch(client: *Client, options: FetchOptions) FetchError!FetchResult {
   1775     const uri = switch (options.location) {
   1776         .url => |u| try Uri.parse(u),
   1777         .uri => |u| u,
   1778     };
   1779     const method: http.Method = options.method orelse
   1780         if (options.payload != null) .POST else .GET;
   1781 
   1782     const redirect_behavior: Request.RedirectBehavior = options.redirect_behavior orelse
   1783         if (options.payload == null) @enumFromInt(3) else .unhandled;
   1784 
   1785     var req = try request(client, method, uri, .{
   1786         .redirect_behavior = redirect_behavior,
   1787         .headers = options.headers,
   1788         .extra_headers = options.extra_headers,
   1789         .privileged_headers = options.privileged_headers,
   1790         .keep_alive = options.keep_alive,
   1791     });
   1792     defer req.deinit();
   1793 
   1794     if (options.payload) |payload| {
   1795         req.transfer_encoding = .{ .content_length = payload.len };
   1796         var body = try req.sendBodyUnflushed(&.{});
   1797         try body.writer.writeAll(payload);
   1798         try body.end();
   1799         try req.connection.?.flush();
   1800     } else {
   1801         try req.sendBodiless();
   1802     }
   1803 
   1804     const redirect_buffer: []u8 = if (redirect_behavior == .unhandled) &.{} else options.redirect_buffer orelse
   1805         try client.allocator.alloc(u8, 8 * 1024);
   1806     defer if (options.redirect_buffer == null) client.allocator.free(redirect_buffer);
   1807 
   1808     var response = try req.receiveHead(redirect_buffer);
   1809 
   1810     const response_writer = options.response_writer orelse {
   1811         const reader = response.reader(&.{});
   1812         _ = reader.discardRemaining() catch |err| switch (err) {
   1813             error.ReadFailed => return response.bodyErr().?,
   1814         };
   1815         return .{ .status = response.head.status };
   1816     };
   1817 
   1818     const decompress_buffer: []u8 = switch (response.head.content_encoding) {
   1819         .identity => &.{},
   1820         .zstd => options.decompress_buffer orelse try client.allocator.alloc(u8, std.compress.zstd.default_window_len),
   1821         .deflate, .gzip => options.decompress_buffer orelse try client.allocator.alloc(u8, std.compress.flate.max_window_len),
   1822         .compress => return error.UnsupportedCompressionMethod,
   1823     };
   1824     defer if (options.decompress_buffer == null) client.allocator.free(decompress_buffer);
   1825 
   1826     var transfer_buffer: [64]u8 = undefined;
   1827     var decompress: http.Decompress = undefined;
   1828     const reader = response.readerDecompressing(&transfer_buffer, &decompress, decompress_buffer);
   1829 
   1830     _ = reader.streamRemaining(response_writer) catch |err| switch (err) {
   1831         error.ReadFailed => return response.bodyErr().?,
   1832         else => |e| return e,
   1833     };
   1834 
   1835     return .{ .status = response.head.status };
   1836 }
   1837 
   1838 pub fn sameParentDomain(parent_host: []const u8, child_host: []const u8) bool {
   1839     if (!std.ascii.endsWithIgnoreCase(child_host, parent_host)) return false;
   1840     if (child_host.len == parent_host.len) return true;
   1841     if (parent_host.len > child_host.len) return false;
   1842     return child_host[child_host.len - parent_host.len - 1] == '.';
   1843 }
   1844 
   1845 test sameParentDomain {
   1846     try testing.expect(!sameParentDomain("foo.com", "bar.com"));
   1847     try testing.expect(sameParentDomain("foo.com", "foo.com"));
   1848     try testing.expect(sameParentDomain("foo.com", "bar.foo.com"));
   1849     try testing.expect(!sameParentDomain("bar.foo.com", "foo.com"));
   1850 }
   1851 
   1852 test {
   1853     _ = Response;
   1854 }