authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2023-10-22 17:48:03-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2023-10-22 17:48:03-04:00
logb82459fa435c366c6af0fee96c3d9b95c24078f9
tree771d71234a09b362ae716f53fb6482cdcdf27eb7
parent33483407a26a49db60bea039b40931cc77b10453
parent93e1f8c8e583b3140bc1985e8b346fd7aca8cf6b
signaturebadge-question-mark Signed by PGP key 4AEE18F83AFDEB23

Merge pull request #17407 from truemedian/http-ng

std.http: more proxy support, buffer writes, tls toggle

12 files changed, 769 insertions(+), 371 deletions(-)

lib/std/Uri.zig+79-33
...@@ -208,24 +208,45 @@ pub fn parseWithoutScheme(text: []const u8) ParseError!Uri {...@@ -208,24 +208,45 @@ pub fn parseWithoutScheme(text: []const u8) ParseError!Uri {
208 return uri;208 return uri;
209}209}
210210
211pub fn format(211pub const WriteToStreamOptions = struct {
212 /// When true, include the scheme part of the URI.
213 scheme: bool = false,
214
215 /// When true, include the user and password part of the URI. Ignored if `authority` is false.
216 authentication: bool = false,
217
218 /// When true, include the authority part of the URI.
219 authority: bool = false,
220
221 /// When true, include the path part of the URI.
222 path: bool = false,
223
224 /// When true, include the query part of the URI. Ignored when `path` is false.
225 query: bool = false,
226
227 /// When true, include the fragment part of the URI. Ignored when `path` is false.
228 fragment: bool = false,
229
230 /// When true, do not escape any part of the URI.
231 raw: bool = false,
232};
233
234pub fn writeToStream(
212 uri: Uri,235 uri: Uri,
213 comptime fmt: []const u8,236 options: WriteToStreamOptions,
214 options: std.fmt.FormatOptions,
215 writer: anytype,237 writer: anytype,
216) @TypeOf(writer).Error!void {238) @TypeOf(writer).Error!void {
217 _ = options;239 if (options.scheme) {
218
219 const needs_absolute = comptime std.mem.indexOf(u8, fmt, "+") != null;
220 const needs_path = comptime std.mem.indexOf(u8, fmt, "/") != null or fmt.len == 0;
221 const raw_uri = comptime std.mem.indexOf(u8, fmt, "r") != null;
222 const needs_fragment = comptime std.mem.indexOf(u8, fmt, "#") != null;
223
224 if (needs_absolute) {
225 try writer.writeAll(uri.scheme);240 try writer.writeAll(uri.scheme);
226 try writer.writeAll(":");241 try writer.writeAll(":");
227 if (uri.host) |host| {242
243 if (options.authority and uri.host != null) {
228 try writer.writeAll("//");244 try writer.writeAll("//");
245 }
246 }
247
248 if (options.authority) {
249 if (options.authentication and uri.host != null) {
229 if (uri.user) |user| {250 if (uri.user) |user| {
230 try writer.writeAll(user);251 try writer.writeAll(user);
231 if (uri.password) |password| {252 if (uri.password) |password| {
...@@ -234,7 +255,9 @@ pub fn format(...@@ -234,7 +255,9 @@ pub fn format(
234 }255 }
235 try writer.writeAll("@");256 try writer.writeAll("@");
236 }257 }
258 }
237259
260 if (uri.host) |host| {
238 try writer.writeAll(host);261 try writer.writeAll(host);
239262
240 if (uri.port) |port| {263 if (uri.port) |port| {
...@@ -244,39 +267,62 @@ pub fn format(...@@ -244,39 +267,62 @@ pub fn format(
244 }267 }
245 }268 }
246269
247 if (needs_path) {270 if (options.path) {
248 if (uri.path.len == 0) {271 if (uri.path.len == 0) {
249 try writer.writeAll("/");272 try writer.writeAll("/");
273 } else if (options.raw) {
274 try writer.writeAll(uri.path);
250 } else {275 } else {
251 if (raw_uri) {276 try writeEscapedPath(writer, uri.path);
252 try writer.writeAll(uri.path);
253 } else {
254 try Uri.writeEscapedPath(writer, uri.path);
255 }
256 }277 }
257278
258 if (uri.query) |q| {279 if (options.query) if (uri.query) |q| {
259 try writer.writeAll("?");280 try writer.writeAll("?");
260 if (raw_uri) {281 if (options.raw) {
261 try writer.writeAll(q);282 try writer.writeAll(q);
262 } else {283 } else {
263 try Uri.writeEscapedQuery(writer, q);284 try writeEscapedQuery(writer, q);
264 }285 }
265 }286 };
266287
267 if (needs_fragment) {288 if (options.fragment) if (uri.fragment) |f| {
268 if (uri.fragment) |f| {289 try writer.writeAll("#");
269 try writer.writeAll("#");290 if (options.raw) {
270 if (raw_uri) {291 try writer.writeAll(f);
271 try writer.writeAll(f);292 } else {
272 } else {293 try writeEscapedQuery(writer, f);
273 try Uri.writeEscapedQuery(writer, f);
274 }
275 }294 }
276 }295 };
277 }296 }
278}297}
279298
299pub fn format(
300 uri: Uri,
301 comptime fmt: []const u8,
302 options: std.fmt.FormatOptions,
303 writer: anytype,
304) @TypeOf(writer).Error!void {
305 _ = options;
306
307 const scheme = comptime std.mem.indexOf(u8, fmt, ":") != null or fmt.len == 0;
308 const authentication = comptime std.mem.indexOf(u8, fmt, "@") != null or fmt.len == 0;
309 const authority = comptime std.mem.indexOf(u8, fmt, "+") != null or fmt.len == 0;
310 const path = comptime std.mem.indexOf(u8, fmt, "/") != null or fmt.len == 0;
311 const query = comptime std.mem.indexOf(u8, fmt, "?") != null or fmt.len == 0;
312 const fragment = comptime std.mem.indexOf(u8, fmt, "#") != null or fmt.len == 0;
313 const raw = comptime std.mem.indexOf(u8, fmt, "r") != null or fmt.len == 0;
314
315 return writeToStream(uri, .{
316 .scheme = scheme,
317 .authentication = authentication,
318 .authority = authority,
319 .path = path,
320 .query = query,
321 .fragment = fragment,
322 .raw = raw,
323 }, writer);
324}
325
280/// Parses the URI or returns an error.326/// Parses the URI or returns an error.
281/// The return value will contain unescaped strings pointing into the327/// The return value will contain unescaped strings pointing into the
282/// original `text`. Each component that is provided, will be non-`null`.328/// original `text`. Each component that is provided, will be non-`null`.
...@@ -711,7 +757,7 @@ test "URI query escaping" {...@@ -711,7 +757,7 @@ test "URI query escaping" {
711 const parsed = try Uri.parse(address);757 const parsed = try Uri.parse(address);
712758
713 // format the URI to escape it759 // format the URI to escape it
714 const formatted_uri = try std.fmt.allocPrint(std.testing.allocator, "{}", .{parsed});760 const formatted_uri = try std.fmt.allocPrint(std.testing.allocator, "{/?}", .{parsed});
715 defer std.testing.allocator.free(formatted_uri);761 defer std.testing.allocator.free(formatted_uri);
716 try std.testing.expectEqualStrings("/?response-content-type=application%2Foctet-stream", formatted_uri);762 try std.testing.expectEqualStrings("/?response-content-type=application%2Foctet-stream", formatted_uri);
717}763}
...@@ -729,6 +775,6 @@ test "format" {...@@ -729,6 +775,6 @@ test "format" {
729 };775 };
730 var buf = std.ArrayList(u8).init(std.testing.allocator);776 var buf = std.ArrayList(u8).init(std.testing.allocator);
731 defer buf.deinit();777 defer buf.deinit();
732 try uri.format("+/", .{}, buf.writer());778 try uri.format(":/?#", .{}, buf.writer());
733 try std.testing.expectEqualSlices(u8, "file:/foo/bar/baz", buf.items);779 try std.testing.expectEqualSlices(u8, "file:/foo/bar/baz", buf.items);
734}780}
lib/std/crypto/tls/Client.zig+1-1
...@@ -881,7 +881,7 @@ pub fn readAll(c: *Client, stream: anytype, buffer: []u8) !usize {...@@ -881,7 +881,7 @@ pub fn readAll(c: *Client, stream: anytype, buffer: []u8) !usize {
881/// The `iovecs` parameter is mutable because this function needs to mutate the fields in881/// The `iovecs` parameter is mutable because this function needs to mutate the fields in
882/// order to handle partial reads from the underlying stream layer.882/// order to handle partial reads from the underlying stream layer.
883pub fn readv(c: *Client, stream: anytype, iovecs: []std.os.iovec) !usize {883pub fn readv(c: *Client, stream: anytype, iovecs: []std.os.iovec) !usize {
884 return readvAtLeast(c, stream, iovecs);884 return readvAtLeast(c, stream, iovecs, 1);
885}885}
886886
887/// Receives TLS-encrypted data from `stream`, which must conform to `StreamInterface`.887/// Receives TLS-encrypted data from `stream`, which must conform to `StreamInterface`.
lib/std/http.zig+5-1
...@@ -35,7 +35,8 @@ pub const Method = enum(u64) { // TODO: should be u192 or u256, but neither is s...@@ -35,7 +35,8 @@ pub const Method = enum(u64) { // TODO: should be u192 or u256, but neither is s
35 /// Asserts that `s` is 24 or fewer bytes.35 /// Asserts that `s` is 24 or fewer bytes.
36 pub fn parse(s: []const u8) u64 {36 pub fn parse(s: []const u8) u64 {
37 var x: u64 = 0;37 var x: u64 = 0;
38 @memcpy(std.mem.asBytes(&x)[0..s.len], s);38 const len = @min(s.len, @sizeOf(@TypeOf(x)));
39 @memcpy(std.mem.asBytes(&x)[0..len], s[0..len]);
39 return x;40 return x;
40 }41 }
4142
...@@ -289,14 +290,17 @@ pub const Status = enum(u10) {...@@ -289,14 +290,17 @@ pub const Status = enum(u10) {
289290
290pub const TransferEncoding = enum {291pub const TransferEncoding = enum {
291 chunked,292 chunked,
293 none,
292 // compression is intentionally omitted here, as std.http.Client stores it as content-encoding294 // compression is intentionally omitted here, as std.http.Client stores it as content-encoding
293};295};
294296
295pub const ContentEncoding = enum {297pub const ContentEncoding = enum {
296 identity,298 identity,
297 compress,299 compress,
300 @"x-compress",
298 deflate,301 deflate,
299 gzip,302 gzip,
303 @"x-gzip",
300 zstd,304 zstd,
301};305};
302306
lib/std/http/Client.zig+540-222
...@@ -13,12 +13,16 @@ const assert = std.debug.assert;...@@ -13,12 +13,16 @@ const assert = std.debug.assert;
13const Client = @This();13const Client = @This();
14const proto = @import("protocol.zig");14const proto = @import("protocol.zig");
1515
16pub const default_connection_pool_size = 32;16pub const disable_tls = std.options.http_disable_tls;
17pub const connection_pool_size = std.options.http_connection_pool_size;
1817
18/// Allocator used for all allocations made by the client.
19///
20/// This allocator must be thread-safe.
19allocator: Allocator,21allocator: Allocator,
20ca_bundle: std.crypto.Certificate.Bundle = .{},22
23ca_bundle: if (disable_tls) void else std.crypto.Certificate.Bundle = if (disable_tls) {} else .{},
21ca_bundle_mutex: std.Thread.Mutex = .{},24ca_bundle_mutex: std.Thread.Mutex = .{},
25
22/// When this is `true`, the next time this client performs an HTTPS request,26/// When this is `true`, the next time this client performs an HTTPS request,
23/// it will first rescan the system for root certificates.27/// it will first rescan the system for root certificates.
24next_https_rescan_certs: bool = true,28next_https_rescan_certs: bool = true,
...@@ -26,7 +30,11 @@ next_https_rescan_certs: bool = true,...@@ -26,7 +30,11 @@ next_https_rescan_certs: bool = true,
26/// The pool of connections that can be reused (and currently in use).30/// The pool of connections that can be reused (and currently in use).
27connection_pool: ConnectionPool = .{},31connection_pool: ConnectionPool = .{},
2832
29proxy: ?HttpProxy = null,33/// This is the proxy that will handle http:// connections. It *must not* be modified when the client has any active connections.
34http_proxy: ?Proxy = null,
35
36/// This is the proxy that will handle https:// connections. It *must not* be modified when the client has any active connections.
37https_proxy: ?Proxy = null,
3038
31/// A set of linked lists of connections that can be reused.39/// A set of linked lists of connections that can be reused.
32pub const ConnectionPool = struct {40pub const ConnectionPool = struct {
...@@ -34,7 +42,7 @@ pub const ConnectionPool = struct {...@@ -34,7 +42,7 @@ pub const ConnectionPool = struct {
34 pub const Criteria = struct {42 pub const Criteria = struct {
35 host: []const u8,43 host: []const u8,
36 port: u16,44 port: u16,
37 is_tls: bool,45 protocol: Connection.Protocol,
38 };46 };
3947
40 const Queue = std.DoublyLinkedList(Connection);48 const Queue = std.DoublyLinkedList(Connection);
...@@ -46,22 +54,24 @@ pub const ConnectionPool = struct {...@@ -46,22 +54,24 @@ pub const ConnectionPool = struct {
46 /// Open connections that are not currently in use.54 /// Open connections that are not currently in use.
47 free: Queue = .{},55 free: Queue = .{},
48 free_len: usize = 0,56 free_len: usize = 0,
49 free_size: usize = connection_pool_size,57 free_size: usize = 32,
5058
51 /// Finds and acquires a connection from the connection pool matching the criteria. This function is threadsafe.59 /// Finds and acquires a connection from the connection pool matching the criteria. This function is threadsafe.
52 /// If no connection is found, null is returned.60 /// If no connection is found, null is returned.
53 pub fn findConnection(pool: *ConnectionPool, criteria: Criteria) ?*Node {61 pub fn findConnection(pool: *ConnectionPool, criteria: Criteria) ?*Connection {
54 pool.mutex.lock();62 pool.mutex.lock();
55 defer pool.mutex.unlock();63 defer pool.mutex.unlock();
5664
57 var next = pool.free.last;65 var next = pool.free.last;
58 while (next) |node| : (next = node.prev) {66 while (next) |node| : (next = node.prev) {
59 if ((node.data.protocol == .tls) != criteria.is_tls) continue;67 if (node.data.protocol != criteria.protocol) continue;
60 if (node.data.port != criteria.port) continue;68 if (node.data.port != criteria.port) continue;
61 if (!mem.eql(u8, node.data.host, criteria.host)) continue;69
70 // Domain names are case-insensitive (RFC 5890, Section 2.3.2.4)
71 if (!std.ascii.eqlIgnoreCase(node.data.host, criteria.host)) continue;
6272
63 pool.acquireUnsafe(node);73 pool.acquireUnsafe(node);
64 return node;74 return &node.data;
65 }75 }
6676
67 return null;77 return null;
...@@ -85,23 +95,28 @@ pub const ConnectionPool = struct {...@@ -85,23 +95,28 @@ pub const ConnectionPool = struct {
8595
86 /// Tries to release a connection back to the connection pool. This function is threadsafe.96 /// Tries to release a connection back to the connection pool. This function is threadsafe.
87 /// If the connection is marked as closing, it will be closed instead.97 /// If the connection is marked as closing, it will be closed instead.
88 pub fn release(pool: *ConnectionPool, client: *Client, node: *Node) void {98 ///
99 /// The allocator must be the owner of all nodes in this pool.
100 /// The allocator must be the owner of all resources associated with the connection.
101 pub fn release(pool: *ConnectionPool, allocator: Allocator, connection: *Connection) void {
89 pool.mutex.lock();102 pool.mutex.lock();
90 defer pool.mutex.unlock();103 defer pool.mutex.unlock();
91104
105 const node = @fieldParentPtr(Node, "data", connection);
106
92 pool.used.remove(node);107 pool.used.remove(node);
93108
94 if (node.data.closing) {109 if (node.data.closing or pool.free_size == 0) {
95 node.data.deinit(client);110 node.data.close(allocator);
96 return client.allocator.destroy(node);111 return allocator.destroy(node);
97 }112 }
98113
99 if (pool.free_len >= pool.free_size) {114 if (pool.free_len >= pool.free_size) {
100 const popped = pool.free.popFirst() orelse unreachable;115 const popped = pool.free.popFirst() orelse unreachable;
101 pool.free_len -= 1;116 pool.free_len -= 1;
102117
103 popped.data.deinit(client);118 popped.data.close(allocator);
104 client.allocator.destroy(popped);119 allocator.destroy(popped);
105 }120 }
106121
107 if (node.data.proxied) {122 if (node.data.proxied) {
...@@ -121,23 +136,43 @@ pub const ConnectionPool = struct {...@@ -121,23 +136,43 @@ pub const ConnectionPool = struct {
121 pool.used.append(node);136 pool.used.append(node);
122 }137 }
123138
124 pub fn deinit(pool: *ConnectionPool, client: *Client) void {139 /// Resizes the connection pool. This function is threadsafe.
140 ///
141 /// If the new size is smaller than the current size, then idle connections will be closed until the pool is the new size.
142 pub fn resize(pool: *ConnectionPool, allocator: Allocator, new_size: usize) void {
143 pool.mutex.lock();
144 defer pool.mutex.unlock();
145
146 var next = pool.free.first;
147 _ = next;
148 while (pool.free_len > new_size) {
149 const popped = pool.free.popFirst() orelse unreachable;
150 pool.free_len -= 1;
151
152 popped.data.close(allocator);
153 allocator.destroy(popped);
154 }
155
156 pool.free_size = new_size;
157 }
158
159 pub fn deinit(pool: *ConnectionPool, allocator: Allocator) void {
125 pool.mutex.lock();160 pool.mutex.lock();
126161
127 var next = pool.free.first;162 var next = pool.free.first;
128 while (next) |node| {163 while (next) |node| {
129 defer client.allocator.destroy(node);164 defer allocator.destroy(node);
130 next = node.next;165 next = node.next;
131166
132 node.data.deinit(client);167 node.data.close(allocator);
133 }168 }
134169
135 next = pool.used.first;170 next = pool.used.first;
136 while (next) |node| {171 while (next) |node| {
137 defer client.allocator.destroy(node);172 defer allocator.destroy(node);
138 next = node.next;173 next = node.next;
139174
140 node.data.deinit(client);175 node.data.close(allocator);
141 }176 }
142177
143 pool.* = undefined;178 pool.* = undefined;
...@@ -147,11 +182,13 @@ pub const ConnectionPool = struct {...@@ -147,11 +182,13 @@ pub const ConnectionPool = struct {
147/// An interface to either a plain or TLS connection.182/// An interface to either a plain or TLS connection.
148pub const Connection = struct {183pub const Connection = struct {
149 pub const buffer_size = std.crypto.tls.max_ciphertext_record_len;184 pub const buffer_size = std.crypto.tls.max_ciphertext_record_len;
185 const BufferSize = std.math.IntFittingRange(0, buffer_size);
186
150 pub const Protocol = enum { plain, tls };187 pub const Protocol = enum { plain, tls };
151188
152 stream: net.Stream,189 stream: net.Stream,
153 /// undefined unless protocol is tls.190 /// undefined unless protocol is tls.
154 tls_client: *std.crypto.tls.Client,191 tls_client: if (!disable_tls) *std.crypto.tls.Client else void,
155192
156 protocol: Protocol,193 protocol: Protocol,
157 host: []u8,194 host: []u8,
...@@ -160,16 +197,15 @@ pub const Connection = struct {...@@ -160,16 +197,15 @@ pub const Connection = struct {
160 proxied: bool = false,197 proxied: bool = false,
161 closing: bool = false,198 closing: bool = false,
162199
163 read_start: u16 = 0,200 read_start: BufferSize = 0,
164 read_end: u16 = 0,201 read_end: BufferSize = 0,
202 write_end: BufferSize = 0,
165 read_buf: [buffer_size]u8 = undefined,203 read_buf: [buffer_size]u8 = undefined,
204 write_buf: [buffer_size]u8 = undefined,
166205
167 pub fn rawReadAtLeast(conn: *Connection, buffer: []u8, len: usize) ReadError!usize {206 pub fn readvDirectTls(conn: *Connection, buffers: []std.os.iovec) ReadError!usize {
168 return switch (conn.protocol) {207 return conn.tls_client.readv(conn.stream, buffers) catch |err| {
169 .plain => conn.stream.readAtLeast(buffer, len),208 // https://github.com/ziglang/zig/issues/2473
170 .tls => conn.tls_client.readAtLeast(conn.stream, buffer, len),
171 } catch |err| {
172 // TODO: https://github.com/ziglang/zig/issues/2473
173 if (mem.startsWith(u8, @errorName(err), "TlsAlert")) return error.TlsAlert;209 if (mem.startsWith(u8, @errorName(err), "TlsAlert")) return error.TlsAlert;
174210
175 switch (err) {211 switch (err) {
...@@ -181,61 +217,69 @@ pub const Connection = struct {...@@ -181,61 +217,69 @@ pub const Connection = struct {
181 };217 };
182 }218 }
183219
220 pub fn readvDirect(conn: *Connection, buffers: []std.os.iovec) ReadError!usize {
221 if (conn.protocol == .tls) {
222 if (disable_tls) unreachable;
223
224 return conn.readvDirectTls(buffers);
225 }
226
227 return conn.stream.readv(buffers) catch |err| switch (err) {
228 error.ConnectionTimedOut => return error.ConnectionTimedOut,
229 error.ConnectionResetByPeer, error.BrokenPipe => return error.ConnectionResetByPeer,
230 else => return error.UnexpectedReadFailure,
231 };
232 }
233
184 pub fn fill(conn: *Connection) ReadError!void {234 pub fn fill(conn: *Connection) ReadError!void {
185 if (conn.read_end != conn.read_start) return;235 if (conn.read_end != conn.read_start) return;
186236
187 const nread = try conn.rawReadAtLeast(conn.read_buf[0..], 1);237 var iovecs = [1]std.os.iovec{
238 .{ .iov_base = &conn.read_buf, .iov_len = conn.read_buf.len },
239 };
240 const nread = try conn.readvDirect(&iovecs);
188 if (nread == 0) return error.EndOfStream;241 if (nread == 0) return error.EndOfStream;
189 conn.read_start = 0;242 conn.read_start = 0;
190 conn.read_end = @as(u16, @intCast(nread));243 conn.read_end = @intCast(nread);
191 }244 }
192245
193 pub fn peek(conn: *Connection) []const u8 {246 pub fn peek(conn: *Connection) []const u8 {
194 return conn.read_buf[conn.read_start..conn.read_end];247 return conn.read_buf[conn.read_start..conn.read_end];
195 }248 }
196249
197 pub fn drop(conn: *Connection, num: u16) void {250 pub fn drop(conn: *Connection, num: BufferSize) void {
198 conn.read_start += num;251 conn.read_start += num;
199 }252 }
200253
201 pub fn readAtLeast(conn: *Connection, buffer: []u8, len: usize) ReadError!usize {254 pub fn read(conn: *Connection, buffer: []u8) ReadError!usize {
202 assert(len <= buffer.len);255 const available_read = conn.read_end - conn.read_start;
203256 const available_buffer = buffer.len;
204 var out_index: u16 = 0;
205 while (out_index < len) {
206 const available_read = conn.read_end - conn.read_start;
207 const available_buffer = buffer.len - out_index;
208
209 if (available_read > available_buffer) { // partially read buffered data
210 @memcpy(buffer[out_index..], conn.read_buf[conn.read_start..conn.read_end][0..available_buffer]);
211 out_index += @as(u16, @intCast(available_buffer));
212 conn.read_start += @as(u16, @intCast(available_buffer));
213257
214 break;258 if (available_read > available_buffer) { // partially read buffered data
215 } else if (available_read > 0) { // fully read buffered data259 @memcpy(buffer[0..available_buffer], conn.read_buf[conn.read_start..conn.read_end][0..available_buffer]);
216 @memcpy(buffer[out_index..][0..available_read], conn.read_buf[conn.read_start..conn.read_end]);260 conn.read_start += @intCast(available_buffer);
217 out_index += available_read;
218 conn.read_start += available_read;
219261
220 if (out_index >= len) break;262 return available_buffer;
221 }263 } else if (available_read > 0) { // fully read buffered data
264 @memcpy(buffer[0..available_read], conn.read_buf[conn.read_start..conn.read_end]);
265 conn.read_start += available_read;
222266
223 const leftover_buffer = available_buffer - available_read;267 return available_read;
224 const leftover_len = len - out_index;268 }
225269
226 if (leftover_buffer > conn.read_buf.len) {270 var iovecs = [2]std.os.iovec{
227 // skip the buffer if the output is large enough271 .{ .iov_base = buffer.ptr, .iov_len = buffer.len },
228 return conn.rawReadAtLeast(buffer[out_index..], leftover_len);272 .{ .iov_base = &conn.read_buf, .iov_len = conn.read_buf.len },
229 }273 };
274 const nread = try conn.readvDirect(&iovecs);
230275
231 try conn.fill();276 if (nread > buffer.len) {
277 conn.read_start = 0;
278 conn.read_end = @intCast(nread - buffer.len);
279 return buffer.len;
232 }280 }
233281
234 return out_index;282 return nread;
235 }
236
237 pub fn read(conn: *Connection, buffer: []u8) ReadError!usize {
238 return conn.readAtLeast(buffer, 1);
239 }283 }
240284
241 pub const ReadError = error{285 pub const ReadError = error{
...@@ -253,26 +297,49 @@ pub const Connection = struct {...@@ -253,26 +297,49 @@ pub const Connection = struct {
253 return Reader{ .context = conn };297 return Reader{ .context = conn };
254 }298 }
255299
256 pub fn writeAll(conn: *Connection, buffer: []const u8) !void {300 pub fn writeAllDirectTls(conn: *Connection, buffer: []const u8) WriteError!void {
257 return switch (conn.protocol) {301 return conn.tls_client.writeAll(conn.stream, buffer) catch |err| switch (err) {
258 .plain => conn.stream.writeAll(buffer),
259 .tls => conn.tls_client.writeAll(conn.stream, buffer),
260 } catch |err| switch (err) {
261 error.BrokenPipe, error.ConnectionResetByPeer => return error.ConnectionResetByPeer,302 error.BrokenPipe, error.ConnectionResetByPeer => return error.ConnectionResetByPeer,
262 else => return error.UnexpectedWriteFailure,303 else => return error.UnexpectedWriteFailure,
263 };304 };
264 }305 }
265306
266 pub fn write(conn: *Connection, buffer: []const u8) !usize {307 pub fn writeAllDirect(conn: *Connection, buffer: []const u8) WriteError!void {
267 return switch (conn.protocol) {308 if (conn.protocol == .tls) {
268 .plain => conn.stream.write(buffer),309 if (disable_tls) unreachable;
269 .tls => conn.tls_client.write(conn.stream, buffer),310
270 } catch |err| switch (err) {311 return conn.writeAllDirectTls(buffer);
312 }
313
314 return conn.stream.writeAll(buffer) catch |err| switch (err) {
271 error.BrokenPipe, error.ConnectionResetByPeer => return error.ConnectionResetByPeer,315 error.BrokenPipe, error.ConnectionResetByPeer => return error.ConnectionResetByPeer,
272 else => return error.UnexpectedWriteFailure,316 else => return error.UnexpectedWriteFailure,
273 };317 };
274 }318 }
275319
320 pub fn write(conn: *Connection, buffer: []const u8) WriteError!usize {
321 if (conn.write_end + buffer.len > conn.write_buf.len) {
322 try conn.flush();
323
324 if (buffer.len > conn.write_buf.len) {
325 try conn.writeAllDirect(buffer);
326 return buffer.len;
327 }
328 }
329
330 @memcpy(conn.write_buf[conn.write_end..][0..buffer.len], buffer);
331 conn.write_end += @intCast(buffer.len);
332
333 return buffer.len;
334 }
335
336 pub fn flush(conn: *Connection) WriteError!void {
337 if (conn.write_end == 0) return;
338
339 try conn.writeAllDirect(conn.write_buf[0..conn.write_end]);
340 conn.write_end = 0;
341 }
342
276 pub const WriteError = error{343 pub const WriteError = error{
277 ConnectionResetByPeer,344 ConnectionResetByPeer,
278 UnexpectedWriteFailure,345 UnexpectedWriteFailure,
...@@ -284,19 +351,17 @@ pub const Connection = struct {...@@ -284,19 +351,17 @@ pub const Connection = struct {
284 return Writer{ .context = conn };351 return Writer{ .context = conn };
285 }352 }
286353
287 pub fn close(conn: *Connection, client: *const Client) void {354 pub fn close(conn: *Connection, allocator: Allocator) void {
288 if (conn.protocol == .tls) {355 if (conn.protocol == .tls) {
356 if (disable_tls) unreachable;
357
289 // try to cleanly close the TLS connection, for any server that cares.358 // try to cleanly close the TLS connection, for any server that cares.
290 _ = conn.tls_client.writeEnd(conn.stream, "", true) catch {};359 _ = conn.tls_client.writeEnd(conn.stream, "", true) catch {};
291 client.allocator.destroy(conn.tls_client);360 allocator.destroy(conn.tls_client);
292 }361 }
293362
294 conn.stream.close();363 conn.stream.close();
295 }364 allocator.free(conn.host);
296
297 pub fn deinit(conn: *Connection, client: *const Client) void {
298 conn.close(client);
299 client.allocator.free(conn.host);
300 }365 }
301};366};
302367
...@@ -331,7 +396,7 @@ pub const Response = struct {...@@ -331,7 +396,7 @@ pub const Response = struct {
331 };396 };
332397
333 pub fn parse(res: *Response, bytes: []const u8, trailing: bool) ParseError!void {398 pub fn parse(res: *Response, bytes: []const u8, trailing: bool) ParseError!void {
334 var it = mem.tokenizeAny(u8, bytes[0 .. bytes.len - 4], "\r\n");399 var it = mem.tokenizeAny(u8, bytes, "\r\n");
335400
336 const first_line = it.next() orelse return error.HttpHeadersInvalid;401 const first_line = it.next() orelse return error.HttpHeadersInvalid;
337 if (first_line.len < 12)402 if (first_line.len < 12)
...@@ -350,6 +415,8 @@ pub const Response = struct {...@@ -350,6 +415,8 @@ pub const Response = struct {
350 res.status = status;415 res.status = status;
351 res.reason = reason;416 res.reason = reason;
352417
418 res.headers.clearRetainingCapacity();
419
353 while (it.next()) |line| {420 while (it.next()) |line| {
354 if (line.len == 0) return error.HttpHeadersInvalid;421 if (line.len == 0) return error.HttpHeadersInvalid;
355 switch (line[0]) {422 switch (line[0]) {
...@@ -365,46 +432,42 @@ pub const Response = struct {...@@ -365,46 +432,42 @@ pub const Response = struct {
365432
366 if (trailing) continue;433 if (trailing) continue;
367434
368 if (std.ascii.eqlIgnoreCase(header_name, "content-length")) {435 if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) {
369 const content_length = std.fmt.parseInt(u64, header_value, 10) catch return error.InvalidContentLength;
370
371 if (res.content_length != null and res.content_length != content_length) return error.HttpHeadersInvalid;
372
373 res.content_length = content_length;
374 } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) {
375 // Transfer-Encoding: second, first436 // Transfer-Encoding: second, first
376 // Transfer-Encoding: deflate, chunked437 // Transfer-Encoding: deflate, chunked
377 var iter = mem.splitBackwardsScalar(u8, header_value, ',');438 var iter = mem.splitBackwardsScalar(u8, header_value, ',');
378439
379 if (iter.next()) |first| {440 const first = iter.first();
380 const trimmed = mem.trim(u8, first, " ");441 const trimmed_first = mem.trim(u8, first, " ");
381442
382 if (std.meta.stringToEnum(http.TransferEncoding, trimmed)) |te| {443 var next: ?[]const u8 = first;
383 if (res.transfer_encoding != null) return error.HttpHeadersInvalid;444 if (std.meta.stringToEnum(http.TransferEncoding, trimmed_first)) |transfer| {
384 res.transfer_encoding = te;445 if (res.transfer_encoding != .none) return error.HttpHeadersInvalid; // we already have a transfer encoding
385 } else if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {446 res.transfer_encoding = transfer;
386 if (res.transfer_compression != null) return error.HttpHeadersInvalid;
387 res.transfer_compression = ce;
388 } else {
389 return error.HttpTransferEncodingUnsupported;
390 }
391 }
392447
393 if (iter.next()) |second| {448 next = iter.next();
394 if (res.transfer_compression != null) return error.HttpTransferEncodingUnsupported;449 }
395450
396 const trimmed = mem.trim(u8, second, " ");451 if (next) |second| {
452 const trimmed_second = mem.trim(u8, second, " ");
397453
398 if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {454 if (std.meta.stringToEnum(http.ContentEncoding, trimmed_second)) |transfer| {
399 res.transfer_compression = ce;455 if (res.transfer_compression != .identity) return error.HttpHeadersInvalid; // double compression is not supported
456 res.transfer_compression = transfer;
400 } else {457 } else {
401 return error.HttpTransferEncodingUnsupported;458 return error.HttpTransferEncodingUnsupported;
402 }459 }
403 }460 }
404461
405 if (iter.next()) |_| return error.HttpTransferEncodingUnsupported;462 if (iter.next()) |_| return error.HttpTransferEncodingUnsupported;
463 } else if (std.ascii.eqlIgnoreCase(header_name, "content-length")) {
464 const content_length = std.fmt.parseInt(u64, header_value, 10) catch return error.InvalidContentLength;
465
466 if (res.content_length != null and res.content_length != content_length) return error.HttpHeadersInvalid;
467
468 res.content_length = content_length;
406 } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) {469 } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) {
407 if (res.transfer_compression != null) return error.HttpHeadersInvalid;470 if (res.transfer_compression != .identity) return error.HttpHeadersInvalid;
408471
409 const trimmed = mem.trim(u8, header_value, " ");472 const trimmed = mem.trim(u8, header_value, " ");
410473
...@@ -440,13 +503,21 @@ pub const Response = struct {...@@ -440,13 +503,21 @@ pub const Response = struct {
440 status: http.Status,503 status: http.Status,
441 reason: []const u8,504 reason: []const u8,
442505
506 /// If present, the number of bytes in the response body.
443 content_length: ?u64 = null,507 content_length: ?u64 = null,
444 transfer_encoding: ?http.TransferEncoding = null,
445 transfer_compression: ?http.ContentEncoding = null,
446508
509 /// If present, the transfer encoding of the response body, otherwise none.
510 transfer_encoding: http.TransferEncoding = .none,
511
512 /// If present, the compression of the response body, otherwise identity (no compression).
513 transfer_compression: http.ContentEncoding = .identity,
514
515 /// The headers received from the server.
447 headers: http.Headers,516 headers: http.Headers,
448 parser: proto.HeadersParser,517 parser: proto.HeadersParser,
449 compression: Compression = .none,518 compression: Compression = .none,
519
520 /// Whether the response body should be skipped. Any data read from the response body will be discarded.
450 skip: bool = false,521 skip: bool = false,
451};522};
452523
...@@ -457,15 +528,18 @@ pub const Request = struct {...@@ -457,15 +528,18 @@ pub const Request = struct {
457 uri: Uri,528 uri: Uri,
458 client: *Client,529 client: *Client,
459 /// is null when this connection is released530 /// is null when this connection is released
460 connection: ?*ConnectionPool.Node,531 connection: ?*Connection,
461532
462 method: http.Method,533 method: http.Method,
463 version: http.Version = .@"HTTP/1.1",534 version: http.Version = .@"HTTP/1.1",
464 headers: http.Headers,535 headers: http.Headers,
536
537 /// The transfer encoding of the request body.
465 transfer_encoding: RequestTransfer = .none,538 transfer_encoding: RequestTransfer = .none,
466539
467 redirects_left: u32,540 redirects_left: u32,
468 handle_redirects: bool,541 handle_redirects: bool,
542 handle_continue: bool,
469543
470 response: Response,544 response: Response,
471545
...@@ -491,9 +565,9 @@ pub const Request = struct {...@@ -491,9 +565,9 @@ pub const Request = struct {
491 if (req.connection) |connection| {565 if (req.connection) |connection| {
492 if (!req.response.parser.done) {566 if (!req.response.parser.done) {
493 // If the response wasn't fully read, then we need to close the connection.567 // If the response wasn't fully read, then we need to close the connection.
494 connection.data.closing = true;568 connection.closing = true;
495 }569 }
496 req.client.connection_pool.release(req.client, connection);570 req.client.connection_pool.release(req.client.allocator, connection);
497 }571 }
498572
499 req.arena.deinit();573 req.arena.deinit();
...@@ -512,7 +586,7 @@ pub const Request = struct {...@@ -512,7 +586,7 @@ pub const Request = struct {
512 .zstd => |*zstd| zstd.deinit(),586 .zstd => |*zstd| zstd.deinit(),
513 }587 }
514588
515 req.client.connection_pool.release(req.client, req.connection.?);589 req.client.connection_pool.release(req.client.allocator, req.connection.?);
516 req.connection = null;590 req.connection = null;
517591
518 const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme;592 const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme;
...@@ -539,42 +613,33 @@ pub const Request = struct {...@@ -539,42 +613,33 @@ pub const Request = struct {
539 };613 };
540 }614 }
541615
542 pub const StartError = Connection.WriteError || error{ InvalidContentLength, UnsupportedTransferEncoding };616 pub const SendError = Connection.WriteError || error{ InvalidContentLength, UnsupportedTransferEncoding };
543617
544 pub const StartOptions = struct {618 pub const SendOptions = struct {
545 /// Specifies that the uri should be used as is619 /// Specifies that the uri should be used as is. You guarantee that the uri is already escaped.
546 raw_uri: bool = false,620 raw_uri: bool = false,
547 };621 };
548622
549 /// Send the request to the server.623 /// Send the HTTP request headers to the server.
550 pub fn start(req: *Request, options: StartOptions) StartError!void {624 pub fn send(req: *Request, options: SendOptions) SendError!void {
551 if (!req.method.requestHasBody() and req.transfer_encoding != .none) return error.UnsupportedTransferEncoding;625 if (!req.method.requestHasBody() and req.transfer_encoding != .none) return error.UnsupportedTransferEncoding;
552626
553 var buffered = std.io.bufferedWriter(req.connection.?.data.writer());627 const w = req.connection.?.writer();
554 const w = buffered.writer();
555628
556 try req.method.write(w);629 try req.method.write(w);
557 try w.writeByte(' ');630 try w.writeByte(' ');
558631
559 if (req.method == .CONNECT) {632 if (req.method == .CONNECT) {
560 try w.writeAll(req.uri.host.?);633 try req.uri.writeToStream(.{ .authority = true }, w);
561 try w.writeByte(':');
562 try w.print("{}", .{req.uri.port.?});
563 } else {634 } else {
564 if (req.connection.?.data.proxied) {635 try req.uri.writeToStream(.{
565 // proxied connections require the full uri636 .scheme = req.connection.?.proxied,
566 if (options.raw_uri) {637 .authentication = req.connection.?.proxied,
567 try w.print("{+/r}", .{req.uri});638 .authority = req.connection.?.proxied,
568 } else {639 .path = true,
569 try w.print("{+/}", .{req.uri});640 .query = true,
570 }641 .raw = options.raw_uri,
571 } else {642 }, w);
572 if (options.raw_uri) {
573 try w.print("{/r}", .{req.uri});
574 } else {
575 try w.print("{/}", .{req.uri});
576 }
577 }
578 }643 }
579 try w.writeByte(' ');644 try w.writeByte(' ');
580 try w.writeAll(@tagName(req.version));645 try w.writeAll(@tagName(req.version));
...@@ -582,7 +647,7 @@ pub const Request = struct {...@@ -582,7 +647,7 @@ pub const Request = struct {
582647
583 if (!req.headers.contains("host")) {648 if (!req.headers.contains("host")) {
584 try w.writeAll("Host: ");649 try w.writeAll("Host: ");
585 try w.writeAll(req.uri.host.?);650 try req.uri.writeToStream(.{ .authority = true }, w);
586 try w.writeAll("\r\n");651 try w.writeAll("\r\n");
587 }652 }
588653
...@@ -614,17 +679,17 @@ pub const Request = struct {...@@ -614,17 +679,17 @@ pub const Request = struct {
614 .none => {},679 .none => {},
615 }680 }
616 } else {681 } else {
617 if (has_content_length) {682 if (has_transfer_encoding) {
618 const content_length = std.fmt.parseInt(u64, req.headers.getFirstValue("content-length").?, 10) catch return error.InvalidContentLength;
619
620 req.transfer_encoding = .{ .content_length = content_length };
621 } else if (has_transfer_encoding) {
622 const transfer_encoding = req.headers.getFirstValue("transfer-encoding").?;683 const transfer_encoding = req.headers.getFirstValue("transfer-encoding").?;
623 if (std.mem.eql(u8, transfer_encoding, "chunked")) {684 if (std.mem.eql(u8, transfer_encoding, "chunked")) {
624 req.transfer_encoding = .chunked;685 req.transfer_encoding = .chunked;
625 } else {686 } else {
626 return error.UnsupportedTransferEncoding;687 return error.UnsupportedTransferEncoding;
627 }688 }
689 } else if (has_content_length) {
690 const content_length = std.fmt.parseInt(u64, req.headers.getFirstValue("content-length").?, 10) catch return error.InvalidContentLength;
691
692 req.transfer_encoding = .{ .content_length = content_length };
628 } else {693 } else {
629 req.transfer_encoding = .none;694 req.transfer_encoding = .none;
630 }695 }
...@@ -639,9 +704,27 @@ pub const Request = struct {...@@ -639,9 +704,27 @@ pub const Request = struct {
639 try w.writeAll("\r\n");704 try w.writeAll("\r\n");
640 }705 }
641706
707 if (req.connection.?.proxied) {
708 const proxy_headers: ?http.Headers = switch (req.connection.?.protocol) {
709 .plain => if (req.client.http_proxy) |proxy| proxy.headers else null,
710 .tls => if (req.client.https_proxy) |proxy| proxy.headers else null,
711 };
712
713 if (proxy_headers) |headers| {
714 for (headers.list.items) |entry| {
715 if (entry.value.len == 0) continue;
716
717 try w.writeAll(entry.name);
718 try w.writeAll(": ");
719 try w.writeAll(entry.value);
720 try w.writeAll("\r\n");
721 }
722 }
723 }
724
642 try w.writeAll("\r\n");725 try w.writeAll("\r\n");
643726
644 try buffered.flush();727 try req.connection.?.flush();
645 }728 }
646729
647 const TransferReadError = Connection.ReadError || proto.HeadersParser.ReadError;730 const TransferReadError = Connection.ReadError || proto.HeadersParser.ReadError;
...@@ -657,7 +740,7 @@ pub const Request = struct {...@@ -657,7 +740,7 @@ pub const Request = struct {
657740
658 var index: usize = 0;741 var index: usize = 0;
659 while (index == 0) {742 while (index == 0) {
660 const amt = try req.response.parser.read(&req.connection.?.data, buf[index..], req.response.skip);743 const amt = try req.response.parser.read(req.connection.?, buf[index..], req.response.skip);
661 if (amt == 0 and req.response.parser.done) break;744 if (amt == 0 and req.response.parser.done) break;
662 index += amt;745 index += amt;
663 }746 }
...@@ -665,20 +748,22 @@ pub const Request = struct {...@@ -665,20 +748,22 @@ pub const Request = struct {
665 return index;748 return index;
666 }749 }
667750
668 pub const WaitError = RequestError || StartError || TransferReadError || proto.HeadersParser.CheckCompleteHeadError || Response.ParseError || Uri.ParseError || error{ TooManyHttpRedirects, RedirectRequiresResend, HttpRedirectMissingLocation, CompressionInitializationFailed, CompressionNotSupported };751 pub const WaitError = RequestError || SendError || TransferReadError || proto.HeadersParser.CheckCompleteHeadError || Response.ParseError || Uri.ParseError || error{ TooManyHttpRedirects, RedirectRequiresResend, HttpRedirectMissingLocation, CompressionInitializationFailed, CompressionNotSupported };
669752
670 /// Waits for a response from the server and parses any headers that are sent.753 /// Waits for a response from the server and parses any headers that are sent.
671 /// This function will block until the final response is received.754 /// This function will block until the final response is received.
672 ///755 ///
673 /// If `handle_redirects` is true and the request has no payload, then this function will automatically follow756 /// If `handle_redirects` is true and the request has no payload, then this function will automatically follow
674 /// redirects. If a request payload is present, then this function will error with error.RedirectRequiresResend.757 /// redirects. If a request payload is present, then this function will error with error.RedirectRequiresResend.
758 ///
759 /// Must be called after `start` and, if any data was written to the request body, then also after `finish`.
675 pub fn wait(req: *Request) WaitError!void {760 pub fn wait(req: *Request) WaitError!void {
676 while (true) { // handle redirects761 while (true) { // handle redirects
677 while (true) { // read headers762 while (true) { // read headers
678 try req.connection.?.data.fill();763 try req.connection.?.fill();
679764
680 const nchecked = try req.response.parser.checkCompleteHead(req.client.allocator, req.connection.?.data.peek());765 const nchecked = try req.response.parser.checkCompleteHead(req.client.allocator, req.connection.?.peek());
681 req.connection.?.data.drop(@as(u16, @intCast(nchecked)));766 req.connection.?.drop(@intCast(nchecked));
682767
683 if (req.response.parser.state.isContent()) break;768 if (req.response.parser.state.isContent()) break;
684 }769 }
...@@ -688,12 +773,16 @@ pub const Request = struct {...@@ -688,12 +773,16 @@ pub const Request = struct {
688 if (req.response.status == .@"continue") {773 if (req.response.status == .@"continue") {
689 req.response.parser.done = true; // we're done parsing the continue response, reset to prepare for the real response774 req.response.parser.done = true; // we're done parsing the continue response, reset to prepare for the real response
690 req.response.parser.reset();775 req.response.parser.reset();
776
777 if (req.handle_continue)
778 continue;
779
691 break;780 break;
692 }781 }
693782
694 // we're switching protocols, so this connection is no longer doing http783 // we're switching protocols, so this connection is no longer doing http
695 if (req.response.status == .switching_protocols or (req.method == .CONNECT and req.response.status == .ok)) {784 if (req.response.status == .switching_protocols or (req.method == .CONNECT and req.response.status == .ok)) {
696 req.connection.?.data.closing = false;785 req.connection.?.closing = false;
697 req.response.parser.done = true;786 req.response.parser.done = true;
698 }787 }
699788
...@@ -704,13 +793,14 @@ pub const Request = struct {...@@ -704,13 +793,14 @@ pub const Request = struct {
704 const res_connection = req.response.headers.getFirstValue("connection");793 const res_connection = req.response.headers.getFirstValue("connection");
705 const res_keepalive = res_connection != null and !std.ascii.eqlIgnoreCase("close", res_connection.?);794 const res_keepalive = res_connection != null and !std.ascii.eqlIgnoreCase("close", res_connection.?);
706 if (res_keepalive and (req_keepalive or req_connection == null)) {795 if (res_keepalive and (req_keepalive or req_connection == null)) {
707 req.connection.?.data.closing = false;796 req.connection.?.closing = false;
708 } else {797 } else {
709 req.connection.?.data.closing = true;798 req.connection.?.closing = true;
710 }799 }
711800
712 if (req.response.transfer_encoding) |te| {801 if (req.response.transfer_encoding != .none) {
713 switch (te) {802 switch (req.response.transfer_encoding) {
803 .none => unreachable,
714 .chunked => {804 .chunked => {
715 req.response.parser.next_chunk_length = 0;805 req.response.parser.next_chunk_length = 0;
716 req.response.parser.state = .chunk_head_size;806 req.response.parser.state = .chunk_head_size;
...@@ -774,23 +864,23 @@ pub const Request = struct {...@@ -774,23 +864,23 @@ pub const Request = struct {
774864
775 try req.redirect(resolved_url);865 try req.redirect(resolved_url);
776866
777 try req.start(.{});867 try req.send(.{});
778 } else {868 } else {
779 req.response.skip = false;869 req.response.skip = false;
780 if (!req.response.parser.done) {870 if (!req.response.parser.done) {
781 if (req.response.transfer_compression) |tc| switch (tc) {871 switch (req.response.transfer_compression) {
782 .identity => req.response.compression = .none,872 .identity => req.response.compression = .none,
783 .compress => return error.CompressionNotSupported,873 .compress, .@"x-compress" => return error.CompressionNotSupported,
784 .deflate => req.response.compression = .{874 .deflate => req.response.compression = .{
785 .deflate = std.compress.zlib.decompressStream(req.client.allocator, req.transferReader()) catch return error.CompressionInitializationFailed,875 .deflate = std.compress.zlib.decompressStream(req.client.allocator, req.transferReader()) catch return error.CompressionInitializationFailed,
786 },876 },
787 .gzip => req.response.compression = .{877 .gzip, .@"x-gzip" => req.response.compression = .{
788 .gzip = std.compress.gzip.decompress(req.client.allocator, req.transferReader()) catch return error.CompressionInitializationFailed,878 .gzip = std.compress.gzip.decompress(req.client.allocator, req.transferReader()) catch return error.CompressionInitializationFailed,
789 },879 },
790 .zstd => req.response.compression = .{880 .zstd => req.response.compression = .{
791 .zstd = std.compress.zstd.decompressStream(req.client.allocator, req.transferReader()),881 .zstd = std.compress.zstd.decompressStream(req.client.allocator, req.transferReader()),
792 },882 },
793 };883 }
794 }884 }
795885
796 break;886 break;
...@@ -806,7 +896,7 @@ pub const Request = struct {...@@ -806,7 +896,7 @@ pub const Request = struct {
806 return .{ .context = req };896 return .{ .context = req };
807 }897 }
808898
809 /// Reads data from the response body. Must be called after `do`.899 /// Reads data from the response body. Must be called after `wait`.
810 pub fn read(req: *Request, buffer: []u8) ReadError!usize {900 pub fn read(req: *Request, buffer: []u8) ReadError!usize {
811 const out_index = switch (req.response.compression) {901 const out_index = switch (req.response.compression) {
812 .deflate => |*deflate| deflate.read(buffer) catch return error.DecompressionFailure,902 .deflate => |*deflate| deflate.read(buffer) catch return error.DecompressionFailure,
...@@ -819,15 +909,13 @@ pub const Request = struct {...@@ -819,15 +909,13 @@ pub const Request = struct {
819 const has_trail = !req.response.parser.state.isContent();909 const has_trail = !req.response.parser.state.isContent();
820910
821 while (!req.response.parser.state.isContent()) { // read trailing headers911 while (!req.response.parser.state.isContent()) { // read trailing headers
822 try req.connection.?.data.fill();912 try req.connection.?.fill();
823913
824 const nchecked = try req.response.parser.checkCompleteHead(req.client.allocator, req.connection.?.data.peek());914 const nchecked = try req.response.parser.checkCompleteHead(req.client.allocator, req.connection.?.peek());
825 req.connection.?.data.drop(@as(u16, @intCast(nchecked)));915 req.connection.?.drop(@intCast(nchecked));
826 }916 }
827917
828 if (has_trail) {918 if (has_trail) {
829 req.response.headers.clearRetainingCapacity();
830
831 // The response headers before the trailers are already guaranteed to be valid, so they will always be parsed again and cannot return an error.919 // The response headers before the trailers are already guaranteed to be valid, so they will always be parsed again and cannot return an error.
832 // This will *only* fail for a malformed trailer.920 // This will *only* fail for a malformed trailer.
833 req.response.parse(req.response.parser.header_bytes.items, true) catch return error.InvalidTrailers;921 req.response.parse(req.response.parser.header_bytes.items, true) catch return error.InvalidTrailers;
...@@ -837,7 +925,7 @@ pub const Request = struct {...@@ -837,7 +925,7 @@ pub const Request = struct {
837 return out_index;925 return out_index;
838 }926 }
839927
840 /// Reads data from the response body. Must be called after `do`.928 /// Reads data from the response body. Must be called after `wait`.
841 pub fn readAll(req: *Request, buffer: []u8) !usize {929 pub fn readAll(req: *Request, buffer: []u8) !usize {
842 var index: usize = 0;930 var index: usize = 0;
843 while (index < buffer.len) {931 while (index < buffer.len) {
...@@ -856,20 +944,21 @@ pub const Request = struct {...@@ -856,20 +944,21 @@ pub const Request = struct {
856 return .{ .context = req };944 return .{ .context = req };
857 }945 }
858946
859 /// Write `bytes` to the server. The `transfer_encoding` request header determines how data will be sent.947 /// Write `bytes` to the server. The `transfer_encoding` field determines how data will be sent.
948 /// Must be called after `start` and before `finish`.
860 pub fn write(req: *Request, bytes: []const u8) WriteError!usize {949 pub fn write(req: *Request, bytes: []const u8) WriteError!usize {
861 switch (req.transfer_encoding) {950 switch (req.transfer_encoding) {
862 .chunked => {951 .chunked => {
863 try req.connection.?.data.writer().print("{x}\r\n", .{bytes.len});952 try req.connection.?.writer().print("{x}\r\n", .{bytes.len});
864 try req.connection.?.data.writeAll(bytes);953 try req.connection.?.writer().writeAll(bytes);
865 try req.connection.?.data.writeAll("\r\n");954 try req.connection.?.writer().writeAll("\r\n");
866955
867 return bytes.len;956 return bytes.len;
868 },957 },
869 .content_length => |*len| {958 .content_length => |*len| {
870 if (len.* < bytes.len) return error.MessageTooLong;959 if (len.* < bytes.len) return error.MessageTooLong;
871960
872 const amt = try req.connection.?.data.write(bytes);961 const amt = try req.connection.?.write(bytes);
873 len.* -= amt;962 len.* -= amt;
874 return amt;963 return amt;
875 },964 },
...@@ -877,6 +966,8 @@ pub const Request = struct {...@@ -877,6 +966,8 @@ pub const Request = struct {
877 }966 }
878 }967 }
879968
969 /// Write `bytes` to the server. The `transfer_encoding` field determines how data will be sent.
970 /// Must be called after `start` and before `finish`.
880 pub fn writeAll(req: *Request, bytes: []const u8) WriteError!void {971 pub fn writeAll(req: *Request, bytes: []const u8) WriteError!void {
881 var index: usize = 0;972 var index: usize = 0;
882 while (index < bytes.len) {973 while (index < bytes.len) {
...@@ -887,50 +978,169 @@ pub const Request = struct {...@@ -887,50 +978,169 @@ pub const Request = struct {
887 pub const FinishError = WriteError || error{MessageNotCompleted};978 pub const FinishError = WriteError || error{MessageNotCompleted};
888979
889 /// Finish the body of a request. This notifies the server that you have no more data to send.980 /// Finish the body of a request. This notifies the server that you have no more data to send.
981 /// Must be called after `start`.
890 pub fn finish(req: *Request) FinishError!void {982 pub fn finish(req: *Request) FinishError!void {
891 switch (req.transfer_encoding) {983 switch (req.transfer_encoding) {
892 .chunked => try req.connection.?.data.writeAll("0\r\n\r\n"),984 .chunked => try req.connection.?.writer().writeAll("0\r\n\r\n"),
893 .content_length => |len| if (len != 0) return error.MessageNotCompleted,985 .content_length => |len| if (len != 0) return error.MessageNotCompleted,
894 .none => {},986 .none => {},
895 }987 }
988
989 try req.connection.?.flush();
896 }990 }
897};991};
898992
899pub const HttpProxy = struct {993pub const Proxy = struct {
900 pub const ProxyAuthentication = union(enum) {994 allocator: Allocator,
901 basic: []const u8,995 headers: http.Headers,
902 custom: []const u8,
903 };
904996
905 protocol: Connection.Protocol,997 protocol: Connection.Protocol,
906 host: []const u8,998 host: []const u8,
907 port: ?u16 = null,999 port: u16,
9081000
909 /// The value for the Proxy-Authorization header.1001 supports_connect: bool = true,
910 auth: ?ProxyAuthentication = null,
911};1002};
9121003
913/// Release all associated resources with the client.1004/// Release all associated resources with the client.
914/// TODO: currently leaks all request allocated data1005///
1006/// All pending requests must be de-initialized and all active connections released
1007/// before calling this function.
915pub fn deinit(client: *Client) void {1008pub fn deinit(client: *Client) void {
916 client.connection_pool.deinit(client);1009 assert(client.connection_pool.used.first == null); // There are still active requests.
1010
1011 client.connection_pool.deinit(client.allocator);
1012
1013 if (client.http_proxy) |*proxy| {
1014 proxy.allocator.free(proxy.host);
1015 proxy.headers.deinit();
1016 }
1017
1018 if (client.https_proxy) |*proxy| {
1019 proxy.allocator.free(proxy.host);
1020 proxy.headers.deinit();
1021 }
1022
1023 if (!disable_tls)
1024 client.ca_bundle.deinit(client.allocator);
9171025
918 client.ca_bundle.deinit(client.allocator);
919 client.* = undefined;1026 client.* = undefined;
920}1027}
9211028
922pub const ConnectUnproxiedError = Allocator.Error || error{ ConnectionRefused, NetworkUnreachable, ConnectionTimedOut, ConnectionResetByPeer, TemporaryNameServerFailure, NameServerFailure, UnknownHostName, HostLacksNetworkAddresses, UnexpectedConnectFailure, TlsInitializationFailed };1029/// Uses the *_proxy environment variable to set any unset proxies for the client.
1030/// This function *must not* be called when the client has any active connections.
1031pub fn loadDefaultProxies(client: *Client) !void {
1032 // Prevent any new connections from being created.
1033 client.connection_pool.mutex.lock();
1034 defer client.connection_pool.mutex.unlock();
1035
1036 assert(client.connection_pool.used.first == null); // There are still active requests.
1037
1038 if (client.http_proxy == null) http: {
1039 const content: []const u8 = if (std.process.hasEnvVarConstant("http_proxy"))
1040 try std.process.getEnvVarOwned(client.allocator, "http_proxy")
1041 else if (std.process.hasEnvVarConstant("HTTP_PROXY"))
1042 try std.process.getEnvVarOwned(client.allocator, "HTTP_PROXY")
1043 else if (std.process.hasEnvVarConstant("all_proxy"))
1044 try std.process.getEnvVarOwned(client.allocator, "all_proxy")
1045 else if (std.process.hasEnvVarConstant("ALL_PROXY"))
1046 try std.process.getEnvVarOwned(client.allocator, "ALL_PROXY")
1047 else
1048 break :http;
1049 defer client.allocator.free(content);
1050
1051 const uri = try Uri.parse(content);
1052
1053 const protocol = protocol_map.get(uri.scheme) orelse break :http; // Unknown scheme, ignore
1054 const host = if (uri.host) |host| try client.allocator.dupe(u8, host) else break :http; // Missing host, ignore
1055 client.http_proxy = .{
1056 .allocator = client.allocator,
1057 .headers = .{ .allocator = client.allocator },
1058
1059 .protocol = protocol,
1060 .host = host,
1061 .port = uri.port orelse switch (protocol) {
1062 .plain => 80,
1063 .tls => 443,
1064 },
1065 };
1066
1067 if (uri.user != null and uri.password != null) {
1068 const prefix_len = "Basic ".len;
1069
1070 const unencoded = try std.fmt.allocPrint(client.allocator, "{s}:{s}", .{ uri.user.?, uri.password.? });
1071 defer client.allocator.free(unencoded);
1072
1073 const buffer = try client.allocator.alloc(u8, std.base64.standard.Encoder.calcSize(unencoded.len) + prefix_len);
1074 defer client.allocator.free(buffer);
1075
1076 const result = std.base64.standard.Encoder.encode(buffer[prefix_len..], unencoded);
1077 @memcpy(buffer[0..prefix_len], "Basic ");
1078
1079 try client.http_proxy.?.headers.append("proxy-authorization", result);
1080 }
1081 }
1082
1083 if (client.https_proxy == null) https: {
1084 const content: []const u8 = if (std.process.hasEnvVarConstant("https_proxy"))
1085 try std.process.getEnvVarOwned(client.allocator, "https_proxy")
1086 else if (std.process.hasEnvVarConstant("HTTPS_PROXY"))
1087 try std.process.getEnvVarOwned(client.allocator, "HTTPS_PROXY")
1088 else if (std.process.hasEnvVarConstant("all_proxy"))
1089 try std.process.getEnvVarOwned(client.allocator, "all_proxy")
1090 else if (std.process.hasEnvVarConstant("ALL_PROXY"))
1091 try std.process.getEnvVarOwned(client.allocator, "ALL_PROXY")
1092 else
1093 break :https;
1094 defer client.allocator.free(content);
1095
1096 const uri = try Uri.parse(content);
1097
1098 const protocol = protocol_map.get(uri.scheme) orelse break :https; // Unknown scheme, ignore
1099 const host = if (uri.host) |host| try client.allocator.dupe(u8, host) else break :https; // Missing host, ignore
1100 client.http_proxy = .{
1101 .allocator = client.allocator,
1102 .headers = .{ .allocator = client.allocator },
1103
1104 .protocol = protocol,
1105 .host = host,
1106 .port = uri.port orelse switch (protocol) {
1107 .plain => 80,
1108 .tls => 443,
1109 },
1110 };
1111
1112 if (uri.user != null and uri.password != null) {
1113 const prefix_len = "Basic ".len;
1114
1115 const unencoded = try std.fmt.allocPrint(client.allocator, "{s}:{s}", .{ uri.user.?, uri.password.? });
1116 defer client.allocator.free(unencoded);
1117
1118 const buffer = try client.allocator.alloc(u8, std.base64.standard.Encoder.calcSize(unencoded.len) + prefix_len);
1119 defer client.allocator.free(buffer);
1120
1121 const result = std.base64.standard.Encoder.encode(buffer[prefix_len..], unencoded);
1122 @memcpy(buffer[0..prefix_len], "Basic ");
1123
1124 try client.https_proxy.?.headers.append("proxy-authorization", result);
1125 }
1126 }
1127}
1128
1129pub const ConnectTcpError = Allocator.Error || error{ ConnectionRefused, NetworkUnreachable, ConnectionTimedOut, ConnectionResetByPeer, TemporaryNameServerFailure, NameServerFailure, UnknownHostName, HostLacksNetworkAddresses, UnexpectedConnectFailure, TlsInitializationFailed };
9231130
924/// Connect to `host:port` using the specified protocol. This will reuse a connection if one is already open.1131/// Connect to `host:port` using the specified protocol. This will reuse a connection if one is already open.
925/// This function is threadsafe.1132/// This function is threadsafe.
926pub fn connectUnproxied(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectUnproxiedError!*ConnectionPool.Node {1133pub fn connectTcp(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectTcpError!*Connection {
927 if (client.connection_pool.findConnection(.{1134 if (client.connection_pool.findConnection(.{
928 .host = host,1135 .host = host,
929 .port = port,1136 .port = port,
930 .is_tls = protocol == .tls,1137 .protocol = protocol,
931 })) |node|1138 })) |node|
932 return node;1139 return node;
9331140
1141 if (disable_tls and protocol == .tls)
1142 return error.TlsInitializationFailed;
1143
934 const conn = try client.allocator.create(ConnectionPool.Node);1144 const conn = try client.allocator.create(ConnectionPool.Node);
935 errdefer client.allocator.destroy(conn);1145 errdefer client.allocator.destroy(conn);
936 conn.* = .{ .data = undefined };1146 conn.* = .{ .data = undefined };
...@@ -951,40 +1161,41 @@ pub fn connectUnproxied(client: *Client, host: []const u8, port: u16, protocol:...@@ -951,40 +1161,41 @@ pub fn connectUnproxied(client: *Client, host: []const u8, port: u16, protocol:
951 conn.data = .{1161 conn.data = .{
952 .stream = stream,1162 .stream = stream,
953 .tls_client = undefined,1163 .tls_client = undefined,
954 .protocol = protocol,
9551164
1165 .protocol = protocol,
956 .host = try client.allocator.dupe(u8, host),1166 .host = try client.allocator.dupe(u8, host),
957 .port = port,1167 .port = port,
958 };1168 };
959 errdefer client.allocator.free(conn.data.host);1169 errdefer client.allocator.free(conn.data.host);
9601170
961 switch (protocol) {1171 if (protocol == .tls) {
962 .plain => {},1172 if (disable_tls) unreachable;
963 .tls => {
964 conn.data.tls_client = try client.allocator.create(std.crypto.tls.Client);
965 errdefer client.allocator.destroy(conn.data.tls_client);
9661173
967 conn.data.tls_client.* = std.crypto.tls.Client.init(stream, client.ca_bundle, host) catch return error.TlsInitializationFailed;1174 conn.data.tls_client = try client.allocator.create(std.crypto.tls.Client);
968 // This is appropriate for HTTPS because the HTTP headers contain1175 errdefer client.allocator.destroy(conn.data.tls_client);
969 // the content length which is used to detect truncation attacks.1176
970 conn.data.tls_client.allow_truncation_attacks = true;1177 conn.data.tls_client.* = std.crypto.tls.Client.init(stream, client.ca_bundle, host) catch return error.TlsInitializationFailed;
971 },1178 // This is appropriate for HTTPS because the HTTP headers contain
1179 // the content length which is used to detect truncation attacks.
1180 conn.data.tls_client.allow_truncation_attacks = true;
972 }1181 }
9731182
974 client.connection_pool.addUsed(conn);1183 client.connection_pool.addUsed(conn);
9751184
976 return conn;1185 return &conn.data;
977}1186}
9781187
979pub const ConnectUnixError = Allocator.Error || std.os.SocketError || error{ NameTooLong, Unsupported } || std.os.ConnectError;1188pub const ConnectUnixError = Allocator.Error || std.os.SocketError || error{ NameTooLong, Unsupported } || std.os.ConnectError;
9801189
981pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*ConnectionPool.Node {1190/// Connect to `path` as a unix domain socket. This will reuse a connection if one is already open.
1191/// This function is threadsafe.
1192pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*Connection {
982 if (!net.has_unix_sockets) return error.Unsupported;1193 if (!net.has_unix_sockets) return error.Unsupported;
9831194
984 if (client.connection_pool.findConnection(.{1195 if (client.connection_pool.findConnection(.{
985 .host = path,1196 .host = path,
986 .port = 0,1197 .port = 0,
987 .is_tls = false,1198 .protocol = .plain,
988 })) |node|1199 })) |node|
989 return node;1200 return node;
9901201
...@@ -1007,37 +1218,130 @@ pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*Connecti...@@ -1007,37 +1218,130 @@ pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*Connecti
10071218
1008 client.connection_pool.addUsed(conn);1219 client.connection_pool.addUsed(conn);
10091220
1010 return conn;1221 return &conn.data;
1011}1222}
10121223
1013// Prevents a dependency loop in request()1224/// Connect to `tunnel_host:tunnel_port` using the specified proxy with HTTP CONNECT. This will reuse a connection if one is already open.
1014const ConnectErrorPartial = ConnectUnproxiedError || error{ UnsupportedUrlScheme, ConnectionRefused };1225/// This function is threadsafe.
1015pub const ConnectError = ConnectErrorPartial || RequestError;1226pub fn connectTunnel(
1227 client: *Client,
1228 proxy: *Proxy,
1229 tunnel_host: []const u8,
1230 tunnel_port: u16,
1231) !*Connection {
1232 if (!proxy.supports_connect) return error.TunnelNotSupported;
10161233
1017pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectError!*ConnectionPool.Node {
1018 if (client.connection_pool.findConnection(.{1234 if (client.connection_pool.findConnection(.{
1019 .host = host,1235 .host = tunnel_host,
1020 .port = port,1236 .port = tunnel_port,
1021 .is_tls = protocol == .tls,1237 .protocol = proxy.protocol,
1022 })) |node|1238 })) |node|
1023 return node;1239 return node;
10241240
1025 if (client.proxy) |proxy| {1241 var maybe_valid = false;
1026 const proxy_port: u16 = proxy.port orelse switch (proxy.protocol) {1242 (tunnel: {
1027 .plain => 80,1243 const conn = try client.connectTcp(proxy.host, proxy.port, proxy.protocol);
1028 .tls => 443,1244 errdefer {
1245 conn.closing = true;
1246 client.connection_pool.release(client.allocator, conn);
1247 }
1248
1249 const uri = Uri{
1250 .scheme = "http",
1251 .user = null,
1252 .password = null,
1253 .host = tunnel_host,
1254 .port = tunnel_port,
1255 .path = "",
1256 .query = null,
1257 .fragment = null,
1258 };
1259
1260 // we can use a small buffer here because a CONNECT response should be very small
1261 var buffer: [8096]u8 = undefined;
1262
1263 var req = client.open(.CONNECT, uri, proxy.headers, .{
1264 .handle_redirects = false,
1265 .connection = conn,
1266 .header_strategy = .{ .static = &buffer },
1267 }) catch |err| {
1268 std.log.debug("err {}", .{err});
1269 break :tunnel err;
1029 };1270 };
1271 defer req.deinit();
1272
1273 req.send(.{ .raw_uri = true }) catch |err| break :tunnel err;
1274 req.wait() catch |err| break :tunnel err;
1275
1276 if (req.response.status.class() == .server_error) {
1277 maybe_valid = true;
1278 break :tunnel error.ServerError;
1279 }
1280
1281 if (req.response.status != .ok) break :tunnel error.ConnectionRefused;
10301282
1031 const conn = try client.connectUnproxied(proxy.host, proxy_port, proxy.protocol);1283 // this connection is now a tunnel, so we can't use it for anything else, it will only be released when the client is de-initialized.
1032 conn.data.proxied = true;1284 req.connection = null;
1285
1286 client.allocator.free(conn.host);
1287 conn.host = try client.allocator.dupe(u8, tunnel_host);
1288 errdefer client.allocator.free(conn.host);
1289
1290 conn.port = tunnel_port;
1291 conn.closing = false;
10331292
1034 return conn;1293 return conn;
1035 } else {1294 }) catch {
1036 return client.connectUnproxied(host, port, protocol);1295 // something went wrong with the tunnel
1296 proxy.supports_connect = maybe_valid;
1297 return error.TunnelNotSupported;
1298 };
1299}
1300
1301// Prevents a dependency loop in request()
1302const ConnectErrorPartial = ConnectTcpError || error{ UnsupportedUrlScheme, ConnectionRefused };
1303pub const ConnectError = ConnectErrorPartial || RequestError;
1304
1305/// Connect to `host:port` using the specified protocol. This will reuse a connection if one is already open.
1306///
1307/// If a proxy is configured for the client, then the proxy will be used to connect to the host.
1308///
1309/// This function is threadsafe.
1310pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectError!*Connection {
1311 // pointer required so that `supports_connect` can be updated if a CONNECT fails
1312 const potential_proxy: ?*Proxy = switch (protocol) {
1313 .plain => if (client.http_proxy) |*proxy_info| proxy_info else null,
1314 .tls => if (client.https_proxy) |*proxy_info| proxy_info else null,
1315 };
1316
1317 if (potential_proxy) |proxy| {
1318 // don't attempt to proxy the proxy thru itself.
1319 if (std.mem.eql(u8, proxy.host, host) and proxy.port == port and proxy.protocol == protocol) {
1320 return client.connectTcp(host, port, protocol);
1321 }
1322
1323 if (proxy.supports_connect) tunnel: {
1324 return connectTunnel(client, proxy, host, port) catch |err| switch (err) {
1325 error.TunnelNotSupported => break :tunnel,
1326 else => |e| return e,
1327 };
1328 }
1329
1330 // fall back to using the proxy as a normal http proxy
1331 const conn = try client.connectTcp(proxy.host, proxy.port, proxy.protocol);
1332 errdefer {
1333 conn.closing = true;
1334 client.connection_pool.release(conn);
1335 }
1336
1337 conn.proxied = true;
1338 return conn;
1037 }1339 }
1340
1341 return client.connectTcp(host, port, protocol);
1038}1342}
10391343
1040pub const RequestError = ConnectUnproxiedError || ConnectErrorPartial || Request.StartError || std.fmt.ParseIntError || Connection.WriteError || error{1344pub const RequestError = ConnectTcpError || ConnectErrorPartial || Request.SendError || std.fmt.ParseIntError || Connection.WriteError || error{
1041 UnsupportedUrlScheme,1345 UnsupportedUrlScheme,
1042 UriMissingHost,1346 UriMissingHost,
10431347
...@@ -1048,12 +1352,20 @@ pub const RequestError = ConnectUnproxiedError || ConnectErrorPartial || Request...@@ -1048,12 +1352,20 @@ pub const RequestError = ConnectUnproxiedError || ConnectErrorPartial || Request
1048pub const RequestOptions = struct {1352pub const RequestOptions = struct {
1049 version: http.Version = .@"HTTP/1.1",1353 version: http.Version = .@"HTTP/1.1",
10501354
1355 /// Automatically ignore 100 Continue responses. This assumes you don't care, and will have sent the body before you
1356 /// wait for the response.
1357 ///
1358 /// If this is not the case AND you know the server will send a 100 Continue, set this to false and wait for a
1359 /// response before sending the body. If you wait AND the server does not send a 100 Continue before you finish the
1360 /// request, then the request *will* deadlock.
1361 handle_continue: bool = true,
1362
1051 handle_redirects: bool = true,1363 handle_redirects: bool = true,
1052 max_redirects: u32 = 3,1364 max_redirects: u32 = 3,
1053 header_strategy: StorageStrategy = .{ .dynamic = 16 * 1024 },1365 header_strategy: StorageStrategy = .{ .dynamic = 16 * 1024 },
10541366
1055 /// Must be an already acquired connection.1367 /// Must be an already acquired connection.
1056 connection: ?*ConnectionPool.Node = null,1368 connection: ?*Connection = null,
10571369
1058 pub const StorageStrategy = union(enum) {1370 pub const StorageStrategy = union(enum) {
1059 /// In this case, the client's Allocator will be used to store the1371 /// In this case, the client's Allocator will be used to store the
...@@ -1076,14 +1388,14 @@ pub const protocol_map = std.ComptimeStringMap(Connection.Protocol, .{...@@ -1076,14 +1388,14 @@ pub const protocol_map = std.ComptimeStringMap(Connection.Protocol, .{
1076 .{ "wss", .tls },1388 .{ "wss", .tls },
1077});1389});
10781390
1079/// Form and send a http request to a server.1391/// Open a connection to the host specified by `uri` and prepare to send a HTTP request.
1080///1392///
1081/// `uri` must remain alive during the entire request.1393/// `uri` must remain alive during the entire request.
1082/// `headers` is cloned and may be freed after this function returns.1394/// `headers` is cloned and may be freed after this function returns.
1083///1395///
1084/// The caller is responsible for calling `deinit()` on the `Request`.1396/// The caller is responsible for calling `deinit()` on the `Request`.
1085/// This function is threadsafe.1397/// This function is threadsafe.
1086pub fn request(client: *Client, method: http.Method, uri: Uri, headers: http.Headers, options: RequestOptions) RequestError!Request {1398pub fn open(client: *Client, method: http.Method, uri: Uri, headers: http.Headers, options: RequestOptions) RequestError!Request {
1087 const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme;1399 const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme;
10881400
1089 const port: u16 = uri.port orelse switch (protocol) {1401 const port: u16 = uri.port orelse switch (protocol) {
...@@ -1094,6 +1406,8 @@ pub fn request(client: *Client, method: http.Method, uri: Uri, headers: http.Hea...@@ -1094,6 +1406,8 @@ pub fn request(client: *Client, method: http.Method, uri: Uri, headers: http.Hea
1094 const host = uri.host orelse return error.UriMissingHost;1406 const host = uri.host orelse return error.UriMissingHost;
10951407
1096 if (protocol == .tls and @atomicLoad(bool, &client.next_https_rescan_certs, .Acquire)) {1408 if (protocol == .tls and @atomicLoad(bool, &client.next_https_rescan_certs, .Acquire)) {
1409 if (disable_tls) unreachable;
1410
1097 client.ca_bundle_mutex.lock();1411 client.ca_bundle_mutex.lock();
1098 defer client.ca_bundle_mutex.unlock();1412 defer client.ca_bundle_mutex.unlock();
10991413
...@@ -1114,6 +1428,7 @@ pub fn request(client: *Client, method: http.Method, uri: Uri, headers: http.Hea...@@ -1114,6 +1428,7 @@ pub fn request(client: *Client, method: http.Method, uri: Uri, headers: http.Hea
1114 .version = options.version,1428 .version = options.version,
1115 .redirects_left = options.max_redirects,1429 .redirects_left = options.max_redirects,
1116 .handle_redirects = options.handle_redirects,1430 .handle_redirects = options.handle_redirects,
1431 .handle_continue = options.handle_continue,
1117 .response = .{1432 .response = .{
1118 .status = undefined,1433 .status = undefined,
1119 .reason = undefined,1434 .reason = undefined,
...@@ -1178,6 +1493,9 @@ pub const FetchResult = struct {...@@ -1178,6 +1493,9 @@ pub const FetchResult = struct {
1178 }1493 }
1179};1494};
11801495
1496/// Perform a one-shot HTTP request with the provided options.
1497///
1498/// This function is threadsafe.
1181pub fn fetch(client: *Client, allocator: Allocator, options: FetchOptions) !FetchResult {1499pub fn fetch(client: *Client, allocator: Allocator, options: FetchOptions) !FetchResult {
1182 const has_transfer_encoding = options.headers.contains("transfer-encoding");1500 const has_transfer_encoding = options.headers.contains("transfer-encoding");
1183 const has_content_length = options.headers.contains("content-length");1501 const has_content_length = options.headers.contains("content-length");
...@@ -1189,7 +1507,7 @@ pub fn fetch(client: *Client, allocator: Allocator, options: FetchOptions) !Fetc...@@ -1189,7 +1507,7 @@ pub fn fetch(client: *Client, allocator: Allocator, options: FetchOptions) !Fetc
1189 .uri => |u| u,1507 .uri => |u| u,
1190 };1508 };
11911509
1192 var req = try request(client, options.method, uri, options.headers, .{1510 var req = try open(client, options.method, uri, options.headers, .{
1193 .header_strategy = options.header_strategy,1511 .header_strategy = options.header_strategy,
1194 .handle_redirects = options.payload == .none,1512 .handle_redirects = options.payload == .none,
1195 });1513 });
...@@ -1206,7 +1524,7 @@ pub fn fetch(client: *Client, allocator: Allocator, options: FetchOptions) !Fetc...@@ -1206,7 +1524,7 @@ pub fn fetch(client: *Client, allocator: Allocator, options: FetchOptions) !Fetc
1206 .none => {},1524 .none => {},
1207 }1525 }
12081526
1209 try req.start(.{ .raw_uri = options.raw_uri });1527 try req.send(.{ .raw_uri = options.raw_uri });
12101528
1211 switch (options.payload) {1529 switch (options.payload) {
1212 .string => |str| try req.writeAll(str),1530 .string => |str| try req.writeAll(str),
lib/std/http/Headers.zig+7-4
...@@ -14,15 +14,18 @@ pub const CaseInsensitiveStringContext = struct {...@@ -14,15 +14,18 @@ pub const CaseInsensitiveStringContext = struct {
14 pub fn hash(self: @This(), s: []const u8) u64 {14 pub fn hash(self: @This(), s: []const u8) u64 {
15 _ = self;15 _ = self;
16 var buf: [64]u8 = undefined;16 var buf: [64]u8 = undefined;
17 var i: u8 = 0;17 var i: usize = 0;
1818
19 var h = std.hash.Wyhash.init(0);19 var h = std.hash.Wyhash.init(0);
20 while (i < s.len) : (i += 64) {20 while (i + 64 < s.len) : (i += 64) {
21 const left = @min(64, s.len - i);21 const ret = ascii.lowerString(buf[0..], s[i..][0..64]);
22 const ret = ascii.lowerString(buf[0..], s[i..][0..left]);
23 h.update(ret);22 h.update(ret);
24 }23 }
2524
25 const left = @min(64, s.len - i);
26 const ret = ascii.lowerString(buf[0..], s[i..][0..left]);
27 h.update(ret);
28
26 return h.final();29 return h.final();
27 }30 }
2831
lib/std/http/Server.zig+44-33
...@@ -14,7 +14,7 @@ allocator: Allocator,...@@ -14,7 +14,7 @@ allocator: Allocator,
1414
15socket: net.StreamServer,15socket: net.StreamServer,
1616
17/// An interface to either a plain or TLS connection.17/// An interface to a plain connection.
18pub const Connection = struct {18pub const Connection = struct {
19 pub const buffer_size = std.crypto.tls.max_ciphertext_record_len;19 pub const buffer_size = std.crypto.tls.max_ciphertext_record_len;
20 pub const Protocol = enum { plain };20 pub const Protocol = enum { plain };
...@@ -178,7 +178,7 @@ pub const Request = struct {...@@ -178,7 +178,7 @@ pub const Request = struct {
178 };178 };
179179
180 pub fn parse(req: *Request, bytes: []const u8) ParseError!void {180 pub fn parse(req: *Request, bytes: []const u8) ParseError!void {
181 var it = mem.tokenizeAny(u8, bytes[0 .. bytes.len - 4], "\r\n");181 var it = mem.tokenizeAny(u8, bytes, "\r\n");
182182
183 const first_line = it.next() orelse return error.HttpHeadersInvalid;183 const first_line = it.next() orelse return error.HttpHeadersInvalid;
184 if (first_line.len < 10)184 if (first_line.len < 10)
...@@ -228,27 +228,23 @@ pub const Request = struct {...@@ -228,27 +228,23 @@ pub const Request = struct {
228 // Transfer-Encoding: deflate, chunked228 // Transfer-Encoding: deflate, chunked
229 var iter = mem.splitBackwardsScalar(u8, header_value, ',');229 var iter = mem.splitBackwardsScalar(u8, header_value, ',');
230230
231 if (iter.next()) |first| {231 const first = iter.first();
232 const trimmed = mem.trim(u8, first, " ");232 const trimmed_first = mem.trim(u8, first, " ");
233233
234 if (std.meta.stringToEnum(http.TransferEncoding, trimmed)) |te| {234 var next: ?[]const u8 = first;
235 if (req.transfer_encoding != null) return error.HttpHeadersInvalid;235 if (std.meta.stringToEnum(http.TransferEncoding, trimmed_first)) |transfer| {
236 req.transfer_encoding = te;236 if (req.transfer_encoding != .none) return error.HttpHeadersInvalid; // we already have a transfer encoding
237 } else if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {237 req.transfer_encoding = transfer;
238 if (req.transfer_compression != null) return error.HttpHeadersInvalid;
239 req.transfer_compression = ce;
240 } else {
241 return error.HttpTransferEncodingUnsupported;
242 }
243 }
244238
245 if (iter.next()) |second| {239 next = iter.next();
246 if (req.transfer_compression != null) return error.HttpTransferEncodingUnsupported;240 }
247241
248 const trimmed = mem.trim(u8, second, " ");242 if (next) |second| {
243 const trimmed_second = mem.trim(u8, second, " ");
249244
250 if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {245 if (std.meta.stringToEnum(http.ContentEncoding, trimmed_second)) |transfer| {
251 req.transfer_compression = ce;246 if (req.transfer_compression != .identity) return error.HttpHeadersInvalid; // double compression is not supported
247 req.transfer_compression = transfer;
252 } else {248 } else {
253 return error.HttpTransferEncodingUnsupported;249 return error.HttpTransferEncodingUnsupported;
254 }250 }
...@@ -256,7 +252,7 @@ pub const Request = struct {...@@ -256,7 +252,7 @@ pub const Request = struct {
256252
257 if (iter.next()) |_| return error.HttpTransferEncodingUnsupported;253 if (iter.next()) |_| return error.HttpTransferEncodingUnsupported;
258 } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) {254 } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) {
259 if (req.transfer_compression != null) return error.HttpHeadersInvalid;255 if (req.transfer_compression != .identity) return error.HttpHeadersInvalid;
260256
261 const trimmed = mem.trim(u8, header_value, " ");257 const trimmed = mem.trim(u8, header_value, " ");
262258
...@@ -277,9 +273,14 @@ pub const Request = struct {...@@ -277,9 +273,14 @@ pub const Request = struct {
277 target: []const u8,273 target: []const u8,
278 version: http.Version,274 version: http.Version,
279275
276 /// The length of the request body, if known.
280 content_length: ?u64 = null,277 content_length: ?u64 = null,
281 transfer_encoding: ?http.TransferEncoding = null,278
282 transfer_compression: ?http.ContentEncoding = null,279 /// The transfer encoding of the request body, or .none if not present.
280 transfer_encoding: http.TransferEncoding = .none,
281
282 /// The compression of the request body, or .identity (no compression) if not present.
283 transfer_compression: http.ContentEncoding = .identity,
283284
284 headers: http.Headers,285 headers: http.Headers,
285 parser: proto.HeadersParser,286 parser: proto.HeadersParser,
...@@ -315,6 +316,7 @@ pub const Response = struct {...@@ -315,6 +316,7 @@ pub const Response = struct {
315 finished,316 finished,
316 };317 };
317318
319 /// Free all resources associated with this response.
318 pub fn deinit(res: *Response) void {320 pub fn deinit(res: *Response) void {
319 res.connection.close();321 res.connection.close();
320322
...@@ -390,10 +392,10 @@ pub const Response = struct {...@@ -390,10 +392,10 @@ pub const Response = struct {
390 }392 }
391 }393 }
392394
393 pub const DoError = Connection.WriteError || error{ UnsupportedTransferEncoding, InvalidContentLength };395 pub const SendError = Connection.WriteError || error{ UnsupportedTransferEncoding, InvalidContentLength };
394396
395 /// Send the response headers.397 /// Send the HTTP response headers to the client.
396 pub fn do(res: *Response) DoError!void {398 pub fn send(res: *Response) SendError!void {
397 switch (res.state) {399 switch (res.state) {
398 .waited => res.state = .responded,400 .waited => res.state = .responded,
399 .first, .start, .responded, .finished => unreachable,401 .first, .start, .responded, .finished => unreachable,
...@@ -511,8 +513,9 @@ pub const Response = struct {...@@ -511,8 +513,9 @@ pub const Response = struct {
511 res.request.headers = .{ .allocator = res.allocator, .owned = true };513 res.request.headers = .{ .allocator = res.allocator, .owned = true };
512 try res.request.parse(res.request.parser.header_bytes.items);514 try res.request.parse(res.request.parser.header_bytes.items);
513515
514 if (res.request.transfer_encoding) |te| {516 if (res.request.transfer_encoding != .none) {
515 switch (te) {517 switch (res.request.transfer_encoding) {
518 .none => unreachable,
516 .chunked => {519 .chunked => {
517 res.request.parser.next_chunk_length = 0;520 res.request.parser.next_chunk_length = 0;
518 res.request.parser.state = .chunk_head_size;521 res.request.parser.state = .chunk_head_size;
...@@ -527,19 +530,19 @@ pub const Response = struct {...@@ -527,19 +530,19 @@ pub const Response = struct {
527 }530 }
528531
529 if (!res.request.parser.done) {532 if (!res.request.parser.done) {
530 if (res.request.transfer_compression) |tc| switch (tc) {533 switch (res.request.transfer_compression) {
531 .identity => res.request.compression = .none,534 .identity => res.request.compression = .none,
532 .compress => return error.CompressionNotSupported,535 .compress, .@"x-compress" => return error.CompressionNotSupported,
533 .deflate => res.request.compression = .{536 .deflate => res.request.compression = .{
534 .deflate = std.compress.zlib.decompressStream(res.allocator, res.transferReader()) catch return error.CompressionInitializationFailed,537 .deflate = std.compress.zlib.decompressStream(res.allocator, res.transferReader()) catch return error.CompressionInitializationFailed,
535 },538 },
536 .gzip => res.request.compression = .{539 .gzip, .@"x-gzip" => res.request.compression = .{
537 .gzip = std.compress.gzip.decompress(res.allocator, res.transferReader()) catch return error.CompressionInitializationFailed,540 .gzip = std.compress.gzip.decompress(res.allocator, res.transferReader()) catch return error.CompressionInitializationFailed,
538 },541 },
539 .zstd => res.request.compression = .{542 .zstd => res.request.compression = .{
540 .zstd = std.compress.zstd.decompressStream(res.allocator, res.transferReader()),543 .zstd = std.compress.zstd.decompressStream(res.allocator, res.transferReader()),
541 },544 },
542 };545 }
543 }546 }
544 }547 }
545548
...@@ -551,6 +554,7 @@ pub const Response = struct {...@@ -551,6 +554,7 @@ pub const Response = struct {
551 return .{ .context = res };554 return .{ .context = res };
552 }555 }
553556
557 /// Reads data from the response body. Must be called after `wait`.
554 pub fn read(res: *Response, buffer: []u8) ReadError!usize {558 pub fn read(res: *Response, buffer: []u8) ReadError!usize {
555 switch (res.state) {559 switch (res.state) {
556 .waited, .responded, .finished => {},560 .waited, .responded, .finished => {},
...@@ -586,6 +590,7 @@ pub const Response = struct {...@@ -586,6 +590,7 @@ pub const Response = struct {
586 return out_index;590 return out_index;
587 }591 }
588592
593 /// Reads data from the response body. Must be called after `wait`.
589 pub fn readAll(res: *Response, buffer: []u8) !usize {594 pub fn readAll(res: *Response, buffer: []u8) !usize {
590 var index: usize = 0;595 var index: usize = 0;
591 while (index < buffer.len) {596 while (index < buffer.len) {
...@@ -605,6 +610,7 @@ pub const Response = struct {...@@ -605,6 +610,7 @@ pub const Response = struct {
605 }610 }
606611
607 /// Write `bytes` to the server. The `transfer_encoding` request header determines how data will be sent.612 /// Write `bytes` to the server. The `transfer_encoding` request header determines how data will be sent.
613 /// Must be called after `start` and before `finish`.
608 pub fn write(res: *Response, bytes: []const u8) WriteError!usize {614 pub fn write(res: *Response, bytes: []const u8) WriteError!usize {
609 switch (res.state) {615 switch (res.state) {
610 .responded => {},616 .responded => {},
...@@ -630,6 +636,8 @@ pub const Response = struct {...@@ -630,6 +636,8 @@ pub const Response = struct {
630 }636 }
631 }637 }
632638
639 /// Write `bytes` to the server. The `transfer_encoding` request header determines how data will be sent.
640 /// Must be called after `start` and before `finish`.
633 pub fn writeAll(req: *Response, bytes: []const u8) WriteError!void {641 pub fn writeAll(req: *Response, bytes: []const u8) WriteError!void {
634 var index: usize = 0;642 var index: usize = 0;
635 while (index < bytes.len) {643 while (index < bytes.len) {
...@@ -640,6 +648,7 @@ pub const Response = struct {...@@ -640,6 +648,7 @@ pub const Response = struct {
640 pub const FinishError = WriteError || error{MessageNotCompleted};648 pub const FinishError = WriteError || error{MessageNotCompleted};
641649
642 /// Finish the body of a request. This notifies the server that you have no more data to send.650 /// Finish the body of a request. This notifies the server that you have no more data to send.
651 /// Must be called after `start`.
643 pub fn finish(res: *Response) FinishError!void {652 pub fn finish(res: *Response) FinishError!void {
644 switch (res.state) {653 switch (res.state) {
645 .responded => res.state = .finished,654 .responded => res.state = .finished,
...@@ -654,6 +663,7 @@ pub const Response = struct {...@@ -654,6 +663,7 @@ pub const Response = struct {
654 }663 }
655};664};
656665
666/// Create a new HTTP server.
657pub fn init(allocator: Allocator, options: net.StreamServer.Options) Server {667pub fn init(allocator: Allocator, options: net.StreamServer.Options) Server {
658 return .{668 return .{
659 .allocator = allocator,669 .allocator = allocator,
...@@ -661,6 +671,7 @@ pub fn init(allocator: Allocator, options: net.StreamServer.Options) Server {...@@ -661,6 +671,7 @@ pub fn init(allocator: Allocator, options: net.StreamServer.Options) Server {
661 };671 };
662}672}
663673
674/// Free all resources associated with this server.
664pub fn deinit(server: *Server) void {675pub fn deinit(server: *Server) void {
665 server.socket.deinit();676 server.socket.deinit();
666}677}
...@@ -756,13 +767,13 @@ test "HTTP server handles a chunked transfer coding request" {...@@ -756,13 +767,13 @@ test "HTTP server handles a chunked transfer coding request" {
756 defer _ = res.reset();767 defer _ = res.reset();
757 try res.wait();768 try res.wait();
758769
759 try expect(res.request.transfer_encoding.? == .chunked);770 try expect(res.request.transfer_encoding == .chunked);
760771
761 const server_body: []const u8 = "message from server!\n";772 const server_body: []const u8 = "message from server!\n";
762 res.transfer_encoding = .{ .content_length = server_body.len };773 res.transfer_encoding = .{ .content_length = server_body.len };
763 try res.headers.append("content-type", "text/plain");774 try res.headers.append("content-type", "text/plain");
764 try res.headers.append("connection", "close");775 try res.headers.append("connection", "close");
765 try res.do();776 try res.send();
766777
767 var buf: [128]u8 = undefined;778 var buf: [128]u8 = undefined;
768 const n = try res.readAll(&buf);779 const n = try res.readAll(&buf);
lib/std/http/protocol.zig+3-3
...@@ -529,7 +529,7 @@ pub const HeadersParser = struct {...@@ -529,7 +529,7 @@ pub const HeadersParser = struct {
529 try conn.fill();529 try conn.fill();
530530
531 const nread = @min(conn.peek().len, data_avail);531 const nread = @min(conn.peek().len, data_avail);
532 conn.drop(@as(u16, @intCast(nread)));532 conn.drop(@intCast(nread));
533 r.next_chunk_length -= nread;533 r.next_chunk_length -= nread;
534534
535 if (r.next_chunk_length == 0) r.done = true;535 if (r.next_chunk_length == 0) r.done = true;
...@@ -553,7 +553,7 @@ pub const HeadersParser = struct {...@@ -553,7 +553,7 @@ pub const HeadersParser = struct {
553 try conn.fill();553 try conn.fill();
554554
555 const i = r.findChunkedLen(conn.peek());555 const i = r.findChunkedLen(conn.peek());
556 conn.drop(@as(u16, @intCast(i)));556 conn.drop(@intCast(i));
557557
558 switch (r.state) {558 switch (r.state) {
559 .invalid => return error.HttpChunkInvalid,559 .invalid => return error.HttpChunkInvalid,
...@@ -582,7 +582,7 @@ pub const HeadersParser = struct {...@@ -582,7 +582,7 @@ pub const HeadersParser = struct {
582 try conn.fill();582 try conn.fill();
583583
584 const nread = @min(conn.peek().len, data_avail);584 const nread = @min(conn.peek().len, data_avail);
585 conn.drop(@as(u16, @intCast(nread)));585 conn.drop(@intCast(nread));
586 r.next_chunk_length -= nread;586 r.next_chunk_length -= nread;
587 } else if (out_avail > 0) {587 } else if (out_avail > 0) {
588 const can_read: usize = @intCast(@min(data_avail, out_avail));588 const can_read: usize = @intCast(@min(data_avail, out_avail));
lib/std/std.zig+8-3
...@@ -283,10 +283,15 @@ pub const options = struct {...@@ -283,10 +283,15 @@ pub const options = struct {
283 else283 else
284 false;284 false;
285285
286 pub const http_connection_pool_size = if (@hasDecl(options_override, "http_connection_pool_size"))286 /// By default, std.http.Client will support HTTPS connections. Set this option to `true` to
287 options_override.http_connection_pool_size287 /// disable TLS support.
288 ///
289 /// This will likely reduce the size of the binary, but it will also make it impossible to
290 /// make a HTTPS connection.
291 pub const http_disable_tls = if (@hasDecl(options_override, "http_disable_tls"))
292 options_override.http_disable_tls
288 else293 else
289 http.Client.default_connection_pool_size;294 false;
290295
291 pub const side_channels_mitigations: crypto.SideChannelsMitigations = if (@hasDecl(options_override, "side_channels_mitigations"))296 pub const side_channels_mitigations: crypto.SideChannelsMitigations = if (@hasDecl(options_override, "side_channels_mitigations"))
292 options_override.side_channels_mitigations297 options_override.side_channels_mitigations
src/Package/Fetch.zig+2-2
...@@ -826,7 +826,7 @@ fn initResource(f: *Fetch, uri: std.Uri) RunError!Resource {...@@ -826,7 +826,7 @@ fn initResource(f: *Fetch, uri: std.Uri) RunError!Resource {
826 var h = std.http.Headers{ .allocator = gpa };826 var h = std.http.Headers{ .allocator = gpa };
827 defer h.deinit();827 defer h.deinit();
828828
829 var req = http_client.request(.GET, uri, h, .{}) catch |err| {829 var req = http_client.open(.GET, uri, h, .{}) catch |err| {
830 return f.fail(f.location_tok, try eb.printString(830 return f.fail(f.location_tok, try eb.printString(
831 "unable to connect to server: {s}",831 "unable to connect to server: {s}",
832 .{@errorName(err)},832 .{@errorName(err)},
...@@ -834,7 +834,7 @@ fn initResource(f: *Fetch, uri: std.Uri) RunError!Resource {...@@ -834,7 +834,7 @@ fn initResource(f: *Fetch, uri: std.Uri) RunError!Resource {
834 };834 };
835 errdefer req.deinit(); // releases more than memory835 errdefer req.deinit(); // releases more than memory
836836
837 req.start(.{}) catch |err| {837 req.send(.{}) catch |err| {
838 return f.fail(f.location_tok, try eb.printString(838 return f.fail(f.location_tok, try eb.printString(
839 "HTTP request failed: {s}",839 "HTTP request failed: {s}",
840 .{@errorName(err)},840 .{@errorName(err)},
src/Package/Fetch/git.zig+6-6
...@@ -518,11 +518,11 @@ pub const Session = struct {...@@ -518,11 +518,11 @@ pub const Session = struct {
518 defer headers.deinit();518 defer headers.deinit();
519 try headers.append("Git-Protocol", "version=2");519 try headers.append("Git-Protocol", "version=2");
520520
521 var request = try session.transport.request(.GET, info_refs_uri, headers, .{521 var request = try session.transport.open(.GET, info_refs_uri, headers, .{
522 .max_redirects = 3,522 .max_redirects = 3,
523 });523 });
524 errdefer request.deinit();524 errdefer request.deinit();
525 try request.start(.{});525 try request.send(.{});
526 try request.finish();526 try request.finish();
527527
528 try request.wait();528 try request.wait();
...@@ -641,12 +641,12 @@ pub const Session = struct {...@@ -641,12 +641,12 @@ pub const Session = struct {
641 }641 }
642 try Packet.write(.flush, body_writer);642 try Packet.write(.flush, body_writer);
643643
644 var request = try session.transport.request(.POST, upload_pack_uri, headers, .{644 var request = try session.transport.open(.POST, upload_pack_uri, headers, .{
645 .handle_redirects = false,645 .handle_redirects = false,
646 });646 });
647 errdefer request.deinit();647 errdefer request.deinit();
648 request.transfer_encoding = .{ .content_length = body.items.len };648 request.transfer_encoding = .{ .content_length = body.items.len };
649 try request.start(.{});649 try request.send(.{});
650 try request.writeAll(body.items);650 try request.writeAll(body.items);
651 try request.finish();651 try request.finish();
652652
...@@ -740,12 +740,12 @@ pub const Session = struct {...@@ -740,12 +740,12 @@ pub const Session = struct {
740 try Packet.write(.{ .data = "done\n" }, body_writer);740 try Packet.write(.{ .data = "done\n" }, body_writer);
741 try Packet.write(.flush, body_writer);741 try Packet.write(.flush, body_writer);
742742
743 var request = try session.transport.request(.POST, upload_pack_uri, headers, .{743 var request = try session.transport.open(.POST, upload_pack_uri, headers, .{
744 .handle_redirects = false,744 .handle_redirects = false,
745 });745 });
746 errdefer request.deinit();746 errdefer request.deinit();
747 request.transfer_encoding = .{ .content_length = body.items.len };747 request.transfer_encoding = .{ .content_length = body.items.len };
748 try request.start(.{});748 try request.send(.{});
749 try request.writeAll(body.items);749 try request.writeAll(body.items);
750 try request.finish();750 try request.finish();
751751
src/main.zig+4
...@@ -5128,6 +5128,8 @@ pub fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !voi...@@ -5128,6 +5128,8 @@ pub fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !voi
5128 var http_client: std.http.Client = .{ .allocator = gpa };5128 var http_client: std.http.Client = .{ .allocator = gpa };
5129 defer http_client.deinit();5129 defer http_client.deinit();
51305130
5131 try http_client.loadDefaultProxies();
5132
5131 var progress: std.Progress = .{ .dont_print_on_dumb = true };5133 var progress: std.Progress = .{ .dont_print_on_dumb = true };
5132 const root_prog_node = progress.start("Fetch Packages", 0);5134 const root_prog_node = progress.start("Fetch Packages", 0);
5133 defer root_prog_node.end();5135 defer root_prog_node.end();
...@@ -7039,6 +7041,8 @@ fn cmdFetch(...@@ -7039,6 +7041,8 @@ fn cmdFetch(
7039 var http_client: std.http.Client = .{ .allocator = gpa };7041 var http_client: std.http.Client = .{ .allocator = gpa };
7040 defer http_client.deinit();7042 defer http_client.deinit();
70417043
7044 try http_client.loadDefaultProxies();
7045
7042 var progress: std.Progress = .{ .dont_print_on_dumb = true };7046 var progress: std.Progress = .{ .dont_print_on_dumb = true };
7043 const root_prog_node = progress.start("Fetch", 0);7047 const root_prog_node = progress.start("Fetch", 0);
7044 defer root_prog_node.end();7048 defer root_prog_node.end();
test/standalone/http.zig+70-63
...@@ -7,6 +7,10 @@ const Client = http.Client;...@@ -7,6 +7,10 @@ const Client = http.Client;
7const mem = std.mem;7const mem = std.mem;
8const testing = std.testing;8const testing = std.testing;
99
10pub const std_options = struct {
11 pub const http_disable_tls = true;
12};
13
10const max_header_size = 8192;14const max_header_size = 8192;
1115
12var gpa_server = std.heap.GeneralPurposeAllocator(.{ .stack_trace_frames = 12 }){};16var gpa_server = std.heap.GeneralPurposeAllocator(.{ .stack_trace_frames = 12 }){};
...@@ -25,11 +29,11 @@ fn handleRequest(res: *Server.Response) !void {...@@ -25,11 +29,11 @@ fn handleRequest(res: *Server.Response) !void {
25 if (res.request.headers.contains("expect")) {29 if (res.request.headers.contains("expect")) {
26 if (mem.eql(u8, res.request.headers.getFirstValue("expect").?, "100-continue")) {30 if (mem.eql(u8, res.request.headers.getFirstValue("expect").?, "100-continue")) {
27 res.status = .@"continue";31 res.status = .@"continue";
28 try res.do();32 try res.send();
29 res.status = .ok;33 res.status = .ok;
30 } else {34 } else {
31 res.status = .expectation_failed;35 res.status = .expectation_failed;
32 try res.do();36 try res.send();
33 return;37 return;
34 }38 }
35 }39 }
...@@ -50,7 +54,7 @@ fn handleRequest(res: *Server.Response) !void {...@@ -50,7 +54,7 @@ fn handleRequest(res: *Server.Response) !void {
5054
51 try res.headers.append("content-type", "text/plain");55 try res.headers.append("content-type", "text/plain");
5256
53 try res.do();57 try res.send();
54 if (res.request.method != .HEAD) {58 if (res.request.method != .HEAD) {
55 try res.writeAll("Hello, ");59 try res.writeAll("Hello, ");
56 try res.writeAll("World!\n");60 try res.writeAll("World!\n");
...@@ -61,7 +65,7 @@ fn handleRequest(res: *Server.Response) !void {...@@ -61,7 +65,7 @@ fn handleRequest(res: *Server.Response) !void {
61 } else if (mem.startsWith(u8, res.request.target, "/large")) {65 } else if (mem.startsWith(u8, res.request.target, "/large")) {
62 res.transfer_encoding = .{ .content_length = 14 * 1024 + 14 * 10 };66 res.transfer_encoding = .{ .content_length = 14 * 1024 + 14 * 10 };
6367
64 try res.do();68 try res.send();
6569
66 var i: u32 = 0;70 var i: u32 = 0;
67 while (i < 5) : (i += 1) {71 while (i < 5) : (i += 1) {
...@@ -88,14 +92,14 @@ fn handleRequest(res: *Server.Response) !void {...@@ -88,14 +92,14 @@ fn handleRequest(res: *Server.Response) !void {
88 try testing.expectEqualStrings("14", res.request.headers.getFirstValue("content-length").?);92 try testing.expectEqualStrings("14", res.request.headers.getFirstValue("content-length").?);
89 }93 }
9094
91 try res.do();95 try res.send();
92 try res.writeAll("Hello, ");96 try res.writeAll("Hello, ");
93 try res.writeAll("World!\n");97 try res.writeAll("World!\n");
94 try res.finish();98 try res.finish();
95 } else if (mem.eql(u8, res.request.target, "/trailer")) {99 } else if (mem.eql(u8, res.request.target, "/trailer")) {
96 res.transfer_encoding = .chunked;100 res.transfer_encoding = .chunked;
97101
98 try res.do();102 try res.send();
99 try res.writeAll("Hello, ");103 try res.writeAll("Hello, ");
100 try res.writeAll("World!\n");104 try res.writeAll("World!\n");
101 // try res.finish();105 // try res.finish();
...@@ -106,7 +110,7 @@ fn handleRequest(res: *Server.Response) !void {...@@ -106,7 +110,7 @@ fn handleRequest(res: *Server.Response) !void {
106 res.status = .found;110 res.status = .found;
107 try res.headers.append("location", "../../get");111 try res.headers.append("location", "../../get");
108112
109 try res.do();113 try res.send();
110 try res.writeAll("Hello, ");114 try res.writeAll("Hello, ");
111 try res.writeAll("Redirected!\n");115 try res.writeAll("Redirected!\n");
112 try res.finish();116 try res.finish();
...@@ -116,7 +120,7 @@ fn handleRequest(res: *Server.Response) !void {...@@ -116,7 +120,7 @@ fn handleRequest(res: *Server.Response) !void {
116 res.status = .found;120 res.status = .found;
117 try res.headers.append("location", "/redirect/1");121 try res.headers.append("location", "/redirect/1");
118122
119 try res.do();123 try res.send();
120 try res.writeAll("Hello, ");124 try res.writeAll("Hello, ");
121 try res.writeAll("Redirected!\n");125 try res.writeAll("Redirected!\n");
122 try res.finish();126 try res.finish();
...@@ -129,7 +133,7 @@ fn handleRequest(res: *Server.Response) !void {...@@ -129,7 +133,7 @@ fn handleRequest(res: *Server.Response) !void {
129 res.status = .found;133 res.status = .found;
130 try res.headers.append("location", location);134 try res.headers.append("location", location);
131135
132 try res.do();136 try res.send();
133 try res.writeAll("Hello, ");137 try res.writeAll("Hello, ");
134 try res.writeAll("Redirected!\n");138 try res.writeAll("Redirected!\n");
135 try res.finish();139 try res.finish();
...@@ -139,7 +143,7 @@ fn handleRequest(res: *Server.Response) !void {...@@ -139,7 +143,7 @@ fn handleRequest(res: *Server.Response) !void {
139 res.status = .found;143 res.status = .found;
140 try res.headers.append("location", "/redirect/3");144 try res.headers.append("location", "/redirect/3");
141145
142 try res.do();146 try res.send();
143 try res.writeAll("Hello, ");147 try res.writeAll("Hello, ");
144 try res.writeAll("Redirected!\n");148 try res.writeAll("Redirected!\n");
145 try res.finish();149 try res.finish();
...@@ -150,11 +154,11 @@ fn handleRequest(res: *Server.Response) !void {...@@ -150,11 +154,11 @@ fn handleRequest(res: *Server.Response) !void {
150154
151 res.status = .found;155 res.status = .found;
152 try res.headers.append("location", location);156 try res.headers.append("location", location);
153 try res.do();157 try res.send();
154 try res.finish();158 try res.finish();
155 } else {159 } else {
156 res.status = .not_found;160 res.status = .not_found;
157 try res.do();161 try res.send();
158 }162 }
159}163}
160164
...@@ -226,8 +230,11 @@ pub fn main() !void {...@@ -226,8 +230,11 @@ pub fn main() !void {
226 const server_thread = try std.Thread.spawn(.{}, serverThread, .{&server});230 const server_thread = try std.Thread.spawn(.{}, serverThread, .{&server});
227231
228 var client = Client{ .allocator = calloc };232 var client = Client{ .allocator = calloc };
233 errdefer client.deinit();
229 // defer client.deinit(); handled below234 // defer client.deinit(); handled below
230235
236 try client.loadDefaultProxies();
237
231 { // read content-length response238 { // read content-length response
232 var h = http.Headers{ .allocator = calloc };239 var h = http.Headers{ .allocator = calloc };
233 defer h.deinit();240 defer h.deinit();
...@@ -237,10 +244,10 @@ pub fn main() !void {...@@ -237,10 +244,10 @@ pub fn main() !void {
237 const uri = try std.Uri.parse(location);244 const uri = try std.Uri.parse(location);
238245
239 log.info("{s}", .{location});246 log.info("{s}", .{location});
240 var req = try client.request(.GET, uri, h, .{});247 var req = try client.open(.GET, uri, h, .{});
241 defer req.deinit();248 defer req.deinit();
242249
243 try req.start(.{});250 try req.send(.{});
244 try req.wait();251 try req.wait();
245252
246 const body = try req.reader().readAllAlloc(calloc, 8192);253 const body = try req.reader().readAllAlloc(calloc, 8192);
...@@ -251,7 +258,7 @@ pub fn main() !void {...@@ -251,7 +258,7 @@ pub fn main() !void {
251 }258 }
252259
253 // connection has been kept alive260 // connection has been kept alive
254 try testing.expect(client.connection_pool.free_len == 1);261 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
255262
256 { // read large content-length response263 { // read large content-length response
257 var h = http.Headers{ .allocator = calloc };264 var h = http.Headers{ .allocator = calloc };
...@@ -262,10 +269,10 @@ pub fn main() !void {...@@ -262,10 +269,10 @@ pub fn main() !void {
262 const uri = try std.Uri.parse(location);269 const uri = try std.Uri.parse(location);
263270
264 log.info("{s}", .{location});271 log.info("{s}", .{location});
265 var req = try client.request(.GET, uri, h, .{});272 var req = try client.open(.GET, uri, h, .{});
266 defer req.deinit();273 defer req.deinit();
267274
268 try req.start(.{});275 try req.send(.{});
269 try req.wait();276 try req.wait();
270277
271 const body = try req.reader().readAllAlloc(calloc, 8192 * 1024);278 const body = try req.reader().readAllAlloc(calloc, 8192 * 1024);
...@@ -275,7 +282,7 @@ pub fn main() !void {...@@ -275,7 +282,7 @@ pub fn main() !void {
275 }282 }
276283
277 // connection has been kept alive284 // connection has been kept alive
278 try testing.expect(client.connection_pool.free_len == 1);285 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
279286
280 { // send head request and not read chunked287 { // send head request and not read chunked
281 var h = http.Headers{ .allocator = calloc };288 var h = http.Headers{ .allocator = calloc };
...@@ -286,10 +293,10 @@ pub fn main() !void {...@@ -286,10 +293,10 @@ pub fn main() !void {
286 const uri = try std.Uri.parse(location);293 const uri = try std.Uri.parse(location);
287294
288 log.info("{s}", .{location});295 log.info("{s}", .{location});
289 var req = try client.request(.HEAD, uri, h, .{});296 var req = try client.open(.HEAD, uri, h, .{});
290 defer req.deinit();297 defer req.deinit();
291298
292 try req.start(.{});299 try req.send(.{});
293 try req.wait();300 try req.wait();
294301
295 const body = try req.reader().readAllAlloc(calloc, 8192);302 const body = try req.reader().readAllAlloc(calloc, 8192);
...@@ -301,7 +308,7 @@ pub fn main() !void {...@@ -301,7 +308,7 @@ pub fn main() !void {
301 }308 }
302309
303 // connection has been kept alive310 // connection has been kept alive
304 try testing.expect(client.connection_pool.free_len == 1);311 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
305312
306 { // read chunked response313 { // read chunked response
307 var h = http.Headers{ .allocator = calloc };314 var h = http.Headers{ .allocator = calloc };
...@@ -312,10 +319,10 @@ pub fn main() !void {...@@ -312,10 +319,10 @@ pub fn main() !void {
312 const uri = try std.Uri.parse(location);319 const uri = try std.Uri.parse(location);
313320
314 log.info("{s}", .{location});321 log.info("{s}", .{location});
315 var req = try client.request(.GET, uri, h, .{});322 var req = try client.open(.GET, uri, h, .{});
316 defer req.deinit();323 defer req.deinit();
317324
318 try req.start(.{});325 try req.send(.{});
319 try req.wait();326 try req.wait();
320327
321 const body = try req.reader().readAllAlloc(calloc, 8192);328 const body = try req.reader().readAllAlloc(calloc, 8192);
...@@ -326,7 +333,7 @@ pub fn main() !void {...@@ -326,7 +333,7 @@ pub fn main() !void {
326 }333 }
327334
328 // connection has been kept alive335 // connection has been kept alive
329 try testing.expect(client.connection_pool.free_len == 1);336 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
330337
331 { // send head request and not read chunked338 { // send head request and not read chunked
332 var h = http.Headers{ .allocator = calloc };339 var h = http.Headers{ .allocator = calloc };
...@@ -337,10 +344,10 @@ pub fn main() !void {...@@ -337,10 +344,10 @@ pub fn main() !void {
337 const uri = try std.Uri.parse(location);344 const uri = try std.Uri.parse(location);
338345
339 log.info("{s}", .{location});346 log.info("{s}", .{location});
340 var req = try client.request(.HEAD, uri, h, .{});347 var req = try client.open(.HEAD, uri, h, .{});
341 defer req.deinit();348 defer req.deinit();
342349
343 try req.start(.{});350 try req.send(.{});
344 try req.wait();351 try req.wait();
345352
346 const body = try req.reader().readAllAlloc(calloc, 8192);353 const body = try req.reader().readAllAlloc(calloc, 8192);
...@@ -352,7 +359,7 @@ pub fn main() !void {...@@ -352,7 +359,7 @@ pub fn main() !void {
352 }359 }
353360
354 // connection has been kept alive361 // connection has been kept alive
355 try testing.expect(client.connection_pool.free_len == 1);362 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
356363
357 { // check trailing headers364 { // check trailing headers
358 var h = http.Headers{ .allocator = calloc };365 var h = http.Headers{ .allocator = calloc };
...@@ -363,10 +370,10 @@ pub fn main() !void {...@@ -363,10 +370,10 @@ pub fn main() !void {
363 const uri = try std.Uri.parse(location);370 const uri = try std.Uri.parse(location);
364371
365 log.info("{s}", .{location});372 log.info("{s}", .{location});
366 var req = try client.request(.GET, uri, h, .{});373 var req = try client.open(.GET, uri, h, .{});
367 defer req.deinit();374 defer req.deinit();
368375
369 try req.start(.{});376 try req.send(.{});
370 try req.wait();377 try req.wait();
371378
372 const body = try req.reader().readAllAlloc(calloc, 8192);379 const body = try req.reader().readAllAlloc(calloc, 8192);
...@@ -377,7 +384,7 @@ pub fn main() !void {...@@ -377,7 +384,7 @@ pub fn main() !void {
377 }384 }
378385
379 // connection has been kept alive386 // connection has been kept alive
380 try testing.expect(client.connection_pool.free_len == 1);387 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
381388
382 { // send content-length request389 { // send content-length request
383 var h = http.Headers{ .allocator = calloc };390 var h = http.Headers{ .allocator = calloc };
...@@ -390,12 +397,12 @@ pub fn main() !void {...@@ -390,12 +397,12 @@ pub fn main() !void {
390 const uri = try std.Uri.parse(location);397 const uri = try std.Uri.parse(location);
391398
392 log.info("{s}", .{location});399 log.info("{s}", .{location});
393 var req = try client.request(.POST, uri, h, .{});400 var req = try client.open(.POST, uri, h, .{});
394 defer req.deinit();401 defer req.deinit();
395402
396 req.transfer_encoding = .{ .content_length = 14 };403 req.transfer_encoding = .{ .content_length = 14 };
397404
398 try req.start(.{});405 try req.send(.{});
399 try req.writeAll("Hello, ");406 try req.writeAll("Hello, ");
400 try req.writeAll("World!\n");407 try req.writeAll("World!\n");
401 try req.finish();408 try req.finish();
...@@ -409,7 +416,7 @@ pub fn main() !void {...@@ -409,7 +416,7 @@ pub fn main() !void {
409 }416 }
410417
411 // connection has been kept alive418 // connection has been kept alive
412 try testing.expect(client.connection_pool.free_len == 1);419 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
413420
414 { // read content-length response with connection close421 { // read content-length response with connection close
415 var h = http.Headers{ .allocator = calloc };422 var h = http.Headers{ .allocator = calloc };
...@@ -422,10 +429,10 @@ pub fn main() !void {...@@ -422,10 +429,10 @@ pub fn main() !void {
422 const uri = try std.Uri.parse(location);429 const uri = try std.Uri.parse(location);
423430
424 log.info("{s}", .{location});431 log.info("{s}", .{location});
425 var req = try client.request(.GET, uri, h, .{});432 var req = try client.open(.GET, uri, h, .{});
426 defer req.deinit();433 defer req.deinit();
427434
428 try req.start(.{});435 try req.send(.{});
429 try req.wait();436 try req.wait();
430437
431 const body = try req.reader().readAllAlloc(calloc, 8192);438 const body = try req.reader().readAllAlloc(calloc, 8192);
...@@ -449,12 +456,12 @@ pub fn main() !void {...@@ -449,12 +456,12 @@ pub fn main() !void {
449 const uri = try std.Uri.parse(location);456 const uri = try std.Uri.parse(location);
450457
451 log.info("{s}", .{location});458 log.info("{s}", .{location});
452 var req = try client.request(.POST, uri, h, .{});459 var req = try client.open(.POST, uri, h, .{});
453 defer req.deinit();460 defer req.deinit();
454461
455 req.transfer_encoding = .chunked;462 req.transfer_encoding = .chunked;
456463
457 try req.start(.{});464 try req.send(.{});
458 try req.writeAll("Hello, ");465 try req.writeAll("Hello, ");
459 try req.writeAll("World!\n");466 try req.writeAll("World!\n");
460 try req.finish();467 try req.finish();
...@@ -468,7 +475,7 @@ pub fn main() !void {...@@ -468,7 +475,7 @@ pub fn main() !void {
468 }475 }
469476
470 // connection has been kept alive477 // connection has been kept alive
471 try testing.expect(client.connection_pool.free_len == 1);478 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
472479
473 { // relative redirect480 { // relative redirect
474 var h = http.Headers{ .allocator = calloc };481 var h = http.Headers{ .allocator = calloc };
...@@ -479,10 +486,10 @@ pub fn main() !void {...@@ -479,10 +486,10 @@ pub fn main() !void {
479 const uri = try std.Uri.parse(location);486 const uri = try std.Uri.parse(location);
480487
481 log.info("{s}", .{location});488 log.info("{s}", .{location});
482 var req = try client.request(.GET, uri, h, .{});489 var req = try client.open(.GET, uri, h, .{});
483 defer req.deinit();490 defer req.deinit();
484491
485 try req.start(.{});492 try req.send(.{});
486 try req.wait();493 try req.wait();
487494
488 const body = try req.reader().readAllAlloc(calloc, 8192);495 const body = try req.reader().readAllAlloc(calloc, 8192);
...@@ -492,7 +499,7 @@ pub fn main() !void {...@@ -492,7 +499,7 @@ pub fn main() !void {
492 }499 }
493500
494 // connection has been kept alive501 // connection has been kept alive
495 try testing.expect(client.connection_pool.free_len == 1);502 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
496503
497 { // redirect from root504 { // redirect from root
498 var h = http.Headers{ .allocator = calloc };505 var h = http.Headers{ .allocator = calloc };
...@@ -503,10 +510,10 @@ pub fn main() !void {...@@ -503,10 +510,10 @@ pub fn main() !void {
503 const uri = try std.Uri.parse(location);510 const uri = try std.Uri.parse(location);
504511
505 log.info("{s}", .{location});512 log.info("{s}", .{location});
506 var req = try client.request(.GET, uri, h, .{});513 var req = try client.open(.GET, uri, h, .{});
507 defer req.deinit();514 defer req.deinit();
508515
509 try req.start(.{});516 try req.send(.{});
510 try req.wait();517 try req.wait();
511518
512 const body = try req.reader().readAllAlloc(calloc, 8192);519 const body = try req.reader().readAllAlloc(calloc, 8192);
...@@ -516,7 +523,7 @@ pub fn main() !void {...@@ -516,7 +523,7 @@ pub fn main() !void {
516 }523 }
517524
518 // connection has been kept alive525 // connection has been kept alive
519 try testing.expect(client.connection_pool.free_len == 1);526 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
520527
521 { // absolute redirect528 { // absolute redirect
522 var h = http.Headers{ .allocator = calloc };529 var h = http.Headers{ .allocator = calloc };
...@@ -527,10 +534,10 @@ pub fn main() !void {...@@ -527,10 +534,10 @@ pub fn main() !void {
527 const uri = try std.Uri.parse(location);534 const uri = try std.Uri.parse(location);
528535
529 log.info("{s}", .{location});536 log.info("{s}", .{location});
530 var req = try client.request(.GET, uri, h, .{});537 var req = try client.open(.GET, uri, h, .{});
531 defer req.deinit();538 defer req.deinit();
532539
533 try req.start(.{});540 try req.send(.{});
534 try req.wait();541 try req.wait();
535542
536 const body = try req.reader().readAllAlloc(calloc, 8192);543 const body = try req.reader().readAllAlloc(calloc, 8192);
...@@ -540,7 +547,7 @@ pub fn main() !void {...@@ -540,7 +547,7 @@ pub fn main() !void {
540 }547 }
541548
542 // connection has been kept alive549 // connection has been kept alive
543 try testing.expect(client.connection_pool.free_len == 1);550 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
544551
545 { // too many redirects552 { // too many redirects
546 var h = http.Headers{ .allocator = calloc };553 var h = http.Headers{ .allocator = calloc };
...@@ -551,10 +558,10 @@ pub fn main() !void {...@@ -551,10 +558,10 @@ pub fn main() !void {
551 const uri = try std.Uri.parse(location);558 const uri = try std.Uri.parse(location);
552559
553 log.info("{s}", .{location});560 log.info("{s}", .{location});
554 var req = try client.request(.GET, uri, h, .{});561 var req = try client.open(.GET, uri, h, .{});
555 defer req.deinit();562 defer req.deinit();
556563
557 try req.start(.{});564 try req.send(.{});
558 req.wait() catch |err| switch (err) {565 req.wait() catch |err| switch (err) {
559 error.TooManyHttpRedirects => {},566 error.TooManyHttpRedirects => {},
560 else => return err,567 else => return err,
...@@ -562,7 +569,7 @@ pub fn main() !void {...@@ -562,7 +569,7 @@ pub fn main() !void {
562 }569 }
563570
564 // connection has been kept alive571 // connection has been kept alive
565 try testing.expect(client.connection_pool.free_len == 1);572 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
566573
567 { // check client without segfault by connection error after redirection574 { // check client without segfault by connection error after redirection
568 var h = http.Headers{ .allocator = calloc };575 var h = http.Headers{ .allocator = calloc };
...@@ -573,17 +580,20 @@ pub fn main() !void {...@@ -573,17 +580,20 @@ pub fn main() !void {
573 const uri = try std.Uri.parse(location);580 const uri = try std.Uri.parse(location);
574581
575 log.info("{s}", .{location});582 log.info("{s}", .{location});
576 var req = try client.request(.GET, uri, h, .{});583 var req = try client.open(.GET, uri, h, .{});
577 defer req.deinit();584 defer req.deinit();
578585
579 try req.start(.{});586 try req.send(.{});
580 const result = req.wait();587 const result = req.wait();
581588
582 try testing.expectError(error.ConnectionRefused, result); // expects not segfault but the regular error589 // a proxy without an upstream is likely to return a 5xx status.
590 if (client.http_proxy == null) {
591 try testing.expectError(error.ConnectionRefused, result); // expects not segfault but the regular error
592 }
583 }593 }
584594
585 // connection has been kept alive595 // connection has been kept alive
586 try testing.expect(client.connection_pool.free_len == 1);596 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
587597
588 { // Client.fetch()598 { // Client.fetch()
589 var h = http.Headers{ .allocator = calloc };599 var h = http.Headers{ .allocator = calloc };
...@@ -618,15 +628,12 @@ pub fn main() !void {...@@ -618,15 +628,12 @@ pub fn main() !void {
618 const uri = try std.Uri.parse(location);628 const uri = try std.Uri.parse(location);
619629
620 log.info("{s}", .{location});630 log.info("{s}", .{location});
621 var req = try client.request(.POST, uri, h, .{});631 var req = try client.open(.POST, uri, h, .{});
622 defer req.deinit();632 defer req.deinit();
623633
624 req.transfer_encoding = .chunked;634 req.transfer_encoding = .chunked;
625635
626 try req.start(.{});636 try req.send(.{});
627 try req.wait();
628 try testing.expectEqual(http.Status.@"continue", req.response.status);
629
630 try req.writeAll("Hello, ");637 try req.writeAll("Hello, ");
631 try req.writeAll("World!\n");638 try req.writeAll("World!\n");
632 try req.finish();639 try req.finish();
...@@ -652,12 +659,12 @@ pub fn main() !void {...@@ -652,12 +659,12 @@ pub fn main() !void {
652 const uri = try std.Uri.parse(location);659 const uri = try std.Uri.parse(location);
653660
654 log.info("{s}", .{location});661 log.info("{s}", .{location});
655 var req = try client.request(.POST, uri, h, .{});662 var req = try client.open(.POST, uri, h, .{});
656 defer req.deinit();663 defer req.deinit();
657664
658 req.transfer_encoding = .chunked;665 req.transfer_encoding = .chunked;
659666
660 try req.start(.{});667 try req.send(.{});
661 try req.wait();668 try req.wait();
662 try testing.expectEqual(http.Status.expectation_failed, req.response.status);669 try testing.expectEqual(http.Status.expectation_failed, req.response.status);
663 }670 }
...@@ -672,9 +679,9 @@ pub fn main() !void {...@@ -672,9 +679,9 @@ pub fn main() !void {
672 defer calloc.free(requests);679 defer calloc.free(requests);
673680
674 for (0..total_connections) |i| {681 for (0..total_connections) |i| {
675 var req = try client.request(.GET, uri, .{ .allocator = calloc }, .{});682 var req = try client.open(.GET, uri, .{ .allocator = calloc }, .{});
676 req.response.parser.done = true;683 req.response.parser.done = true;
677 req.connection.?.data.closing = false;684 req.connection.?.closing = false;
678 requests[i] = req;685 requests[i] = req;
679 }686 }
680687