authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-02-11 17:17:09-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-02-23 02:37:10-07:00
log90bd4f226e2ba03634d31c73df06bf0a90fa0231
tree0a5cc52fa31ac0e0d9f8f40bce9534c67c4bd89c
parentf1cf300c8fa9842ec9c812310bdc9f3aeeb75359

std.http: remove the ability to heap-allocate headers

The buffer for HTTP headers is now always provided via a static buffer. As a consequence, OutOfMemory is no longer a member of the read() error set, and the API and implementation of Client and Server are simplified. error.HttpHeadersExceededSizeLimit is renamed to error.HttpHeadersOversize.

5 files changed, 210 insertions(+), 179 deletions(-)

lib/std/http/Client.zig+78-54
...@@ -20,9 +20,7 @@ const proto = @import("protocol.zig");...@@ -20,9 +20,7 @@ const proto = @import("protocol.zig");
2020
21pub const disable_tls = std.options.http_disable_tls;21pub const disable_tls = std.options.http_disable_tls;
2222
23/// Allocator used for all allocations made by the client.23/// Used for all client allocations. Must be thread-safe.
24///
25/// This allocator must be thread-safe.
26allocator: Allocator,24allocator: Allocator,
2725
28ca_bundle: if (disable_tls) void else std.crypto.Certificate.Bundle = if (disable_tls) {} else .{},26ca_bundle: if (disable_tls) void else std.crypto.Certificate.Bundle = if (disable_tls) {} else .{},
...@@ -35,10 +33,12 @@ next_https_rescan_certs: bool = true,...@@ -35,10 +33,12 @@ next_https_rescan_certs: bool = true,
35/// The pool of connections that can be reused (and currently in use).33/// The pool of connections that can be reused (and currently in use).
36connection_pool: ConnectionPool = .{},34connection_pool: ConnectionPool = .{},
3735
38/// This is the proxy that will handle http:// connections. It *must not* be modified when the client has any active connections.36/// This is the proxy that will handle http:// connections. It *must not* be
37/// modified when the client has any active connections.
39http_proxy: ?Proxy = null,38http_proxy: ?Proxy = null,
4039
41/// This is the proxy that will handle https:// connections. It *must not* be modified when the client has any active connections.40/// This is the proxy that will handle https:// connections. It *must not* be
41/// modified when the client has any active connections.
42https_proxy: ?Proxy = null,42https_proxy: ?Proxy = null,
4343
44/// A set of linked lists of connections that can be reused.44/// A set of linked lists of connections that can be reused.
...@@ -609,10 +609,6 @@ pub const Request = struct {...@@ -609,10 +609,6 @@ pub const Request = struct {
609 req.headers.deinit();609 req.headers.deinit();
610 req.response.headers.deinit();610 req.response.headers.deinit();
611611
612 if (req.response.parser.header_bytes_owned) {
613 req.response.parser.header_bytes.deinit(req.client.allocator);
614 }
615
616 if (req.connection) |connection| {612 if (req.connection) |connection| {
617 if (!req.response.parser.done) {613 if (!req.response.parser.done) {
618 // If the response wasn't fully read, then we need to close the connection.614 // If the response wasn't fully read, then we need to close the connection.
...@@ -810,27 +806,38 @@ pub const Request = struct {...@@ -810,27 +806,38 @@ pub const Request = struct {
810 return index;806 return index;
811 }807 }
812808
813 pub const WaitError = RequestError || SendError || TransferReadError || proto.HeadersParser.CheckCompleteHeadError || Response.ParseError || Uri.ParseError || error{ TooManyHttpRedirects, RedirectRequiresResend, HttpRedirectMissingLocation, CompressionInitializationFailed, CompressionNotSupported };809 pub const WaitError = RequestError || SendError || TransferReadError ||
810 proto.HeadersParser.CheckCompleteHeadError || Response.ParseError || Uri.ParseError ||
811 error{ // TODO: file zig fmt issue for this bad indentation
812 TooManyHttpRedirects,
813 RedirectRequiresResend,
814 HttpRedirectMissingLocation,
815 CompressionInitializationFailed,
816 CompressionNotSupported,
817 };
814818
815 /// Waits for a response from the server and parses any headers that are sent.819 /// Waits for a response from the server and parses any headers that are sent.
816 /// This function will block until the final response is received.820 /// This function will block until the final response is received.
817 ///821 ///
818 /// If `handle_redirects` is true and the request has no payload, then this function will automatically follow822 /// If `handle_redirects` is true and the request has no payload, then this
819 /// redirects. If a request payload is present, then this function will error with error.RedirectRequiresResend.823 /// function will automatically follow redirects. If a request payload is
824 /// present, then this function will error with
825 /// error.RedirectRequiresResend.
820 ///826 ///
821 /// Must be called after `send` and, if any data was written to the request body, then also after `finish`.827 /// Must be called after `send` and, if any data was written to the request
828 /// body, then also after `finish`.
822 pub fn wait(req: *Request) WaitError!void {829 pub fn wait(req: *Request) WaitError!void {
823 while (true) { // handle redirects830 while (true) { // handle redirects
824 while (true) { // read headers831 while (true) { // read headers
825 try req.connection.?.fill();832 try req.connection.?.fill();
826833
827 const nchecked = try req.response.parser.checkCompleteHead(req.client.allocator, req.connection.?.peek());834 const nchecked = try req.response.parser.checkCompleteHead(req.connection.?.peek());
828 req.connection.?.drop(@intCast(nchecked));835 req.connection.?.drop(@intCast(nchecked));
829836
830 if (req.response.parser.state.isContent()) break;837 if (req.response.parser.state.isContent()) break;
831 }838 }
832839
833 try req.response.parse(req.response.parser.header_bytes.items, false);840 try req.response.parse(req.response.parser.get(), false);
834841
835 if (req.response.status == .@"continue") {842 if (req.response.status == .@"continue") {
836 req.response.parser.done = true; // we're done parsing the continue response, reset to prepare for the real response843 req.response.parser.done = true; // we're done parsing the continue response, reset to prepare for the real response
...@@ -891,7 +898,8 @@ pub const Request = struct {...@@ -891,7 +898,8 @@ pub const Request = struct {
891 if (req.response.status.class() == .redirect and req.handle_redirects) {898 if (req.response.status.class() == .redirect and req.handle_redirects) {
892 req.response.skip = true;899 req.response.skip = true;
893900
894 // skip the body of the redirect response, this will at least leave the connection in a known good state.901 // skip the body of the redirect response, this will at least
902 // leave the connection in a known good state.
895 const empty = @as([*]u8, undefined)[0..0];903 const empty = @as([*]u8, undefined)[0..0];
896 assert(try req.transferRead(empty) == 0); // we're skipping, no buffer is necessary904 assert(try req.transferRead(empty) == 0); // we're skipping, no buffer is necessary
897905
...@@ -908,7 +916,10 @@ pub const Request = struct {...@@ -908,7 +916,10 @@ pub const Request = struct {
908 const resolved_url = try req.uri.resolve(new_url, false, arena);916 const resolved_url = try req.uri.resolve(new_url, false, arena);
909917
910 // is the redirect location on the same domain, or a subdomain of the original request?918 // is the redirect location on the same domain, or a subdomain of the original request?
911 const is_same_domain_or_subdomain = std.ascii.endsWithIgnoreCase(resolved_url.host.?, req.uri.host.?) and (resolved_url.host.?.len == req.uri.host.?.len or resolved_url.host.?[resolved_url.host.?.len - req.uri.host.?.len - 1] == '.');919 const is_same_domain_or_subdomain =
920 std.ascii.endsWithIgnoreCase(resolved_url.host.?, req.uri.host.?) and
921 (resolved_url.host.?.len == req.uri.host.?.len or
922 resolved_url.host.?[resolved_url.host.?.len - req.uri.host.?.len - 1] == '.');
912923
913 if (resolved_url.host == null or !is_same_domain_or_subdomain or !std.ascii.eqlIgnoreCase(resolved_url.scheme, req.uri.scheme)) {924 if (resolved_url.host == null or !is_same_domain_or_subdomain or !std.ascii.eqlIgnoreCase(resolved_url.scheme, req.uri.scheme)) {
914 // we're redirecting to a different domain, strip privileged headers like cookies925 // we're redirecting to a different domain, strip privileged headers like cookies
...@@ -957,7 +968,8 @@ pub const Request = struct {...@@ -957,7 +968,8 @@ pub const Request = struct {
957 }968 }
958 }969 }
959970
960 pub const ReadError = TransferReadError || proto.HeadersParser.CheckCompleteHeadError || error{ DecompressionFailure, InvalidTrailers };971 pub const ReadError = TransferReadError || proto.HeadersParser.CheckCompleteHeadError ||
972 error{ DecompressionFailure, InvalidTrailers };
961973
962 pub const Reader = std.io.Reader(*Request, ReadError, read);974 pub const Reader = std.io.Reader(*Request, ReadError, read);
963975
...@@ -980,14 +992,16 @@ pub const Request = struct {...@@ -980,14 +992,16 @@ pub const Request = struct {
980 while (!req.response.parser.state.isContent()) { // read trailing headers992 while (!req.response.parser.state.isContent()) { // read trailing headers
981 try req.connection.?.fill();993 try req.connection.?.fill();
982994
983 const nchecked = try req.response.parser.checkCompleteHead(req.client.allocator, req.connection.?.peek());995 const nchecked = try req.response.parser.checkCompleteHead(req.connection.?.peek());
984 req.connection.?.drop(@intCast(nchecked));996 req.connection.?.drop(@intCast(nchecked));
985 }997 }
986998
987 if (has_trail) {999 if (has_trail) {
988 // The response headers before the trailers are already guaranteed to be valid, so they will always be parsed again and cannot return an error.1000 // The response headers before the trailers are already
1001 // guaranteed to be valid, so they will always be parsed again
1002 // and cannot return an error.
989 // This will *only* fail for a malformed trailer.1003 // This will *only* fail for a malformed trailer.
990 req.response.parse(req.response.parser.header_bytes.items, true) catch return error.InvalidTrailers;1004 req.response.parse(req.response.parser.get(), true) catch return error.InvalidTrailers;
991 }1005 }
992 }1006 }
9931007
...@@ -1362,13 +1376,11 @@ pub fn connectTunnel(...@@ -1362,13 +1376,11 @@ pub fn connectTunnel(
1362 .fragment = null,1376 .fragment = null,
1363 };1377 };
13641378
1365 // we can use a small buffer here because a CONNECT response should be very small
1366 var buffer: [8096]u8 = undefined;1379 var buffer: [8096]u8 = undefined;
1367
1368 var req = client.open(.CONNECT, uri, proxy.headers, .{1380 var req = client.open(.CONNECT, uri, proxy.headers, .{
1369 .handle_redirects = false,1381 .handle_redirects = false,
1370 .connection = conn,1382 .connection = conn,
1371 .header_strategy = .{ .static = &buffer },1383 .server_header_buffer = &buffer,
1372 }) catch |err| {1384 }) catch |err| {
1373 std.log.debug("err {}", .{err});1385 std.log.debug("err {}", .{err});
1374 break :tunnel err;1386 break :tunnel err;
...@@ -1445,7 +1457,9 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio...@@ -1445,7 +1457,9 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio
1445 return client.connectTcp(host, port, protocol);1457 return client.connectTcp(host, port, protocol);
1446}1458}
14471459
1448pub const RequestError = ConnectTcpError || ConnectErrorPartial || Request.SendError || std.fmt.ParseIntError || Connection.WriteError || error{1460pub const RequestError = ConnectTcpError || ConnectErrorPartial || Request.SendError ||
1461 std.fmt.ParseIntError || Connection.WriteError ||
1462 error{ // TODO: file a zig fmt issue for this bad indentation
1449 UnsupportedUrlScheme,1463 UnsupportedUrlScheme,
1450 UriMissingHost,1464 UriMissingHost,
14511465
...@@ -1456,36 +1470,29 @@ pub const RequestError = ConnectTcpError || ConnectErrorPartial || Request.SendE...@@ -1456,36 +1470,29 @@ pub const RequestError = ConnectTcpError || ConnectErrorPartial || Request.SendE
1456pub const RequestOptions = struct {1470pub const RequestOptions = struct {
1457 version: http.Version = .@"HTTP/1.1",1471 version: http.Version = .@"HTTP/1.1",
14581472
1459 /// Automatically ignore 100 Continue responses. This assumes you don't care, and will have sent the body before you1473 /// Automatically ignore 100 Continue responses. This assumes you don't
1460 /// wait for the response.1474 /// care, and will have sent the body before you wait for the response.
1461 ///1475 ///
1462 /// If this is not the case AND you know the server will send a 100 Continue, set this to false and wait for a1476 /// If this is not the case AND you know the server will send a 100
1463 /// response before sending the body. If you wait AND the server does not send a 100 Continue before you finish the1477 /// Continue, set this to false and wait for a response before sending the
1464 /// request, then the request *will* deadlock.1478 /// body. If you wait AND the server does not send a 100 Continue before
1479 /// you finish the request, then the request *will* deadlock.
1465 handle_continue: bool = true,1480 handle_continue: bool = true,
14661481
1467 /// Automatically follow redirects. This will only follow redirects for repeatable requests (ie. with no payload or the server has acknowledged the payload)1482 /// Automatically follow redirects. This will only follow redirects for
1483 /// repeatable requests (ie. with no payload or the server has acknowledged
1484 /// the payload).
1468 handle_redirects: bool = true,1485 handle_redirects: bool = true,
14691486
1470 /// How many redirects to follow before returning an error.1487 /// How many redirects to follow before returning an error.
1471 max_redirects: u32 = 3,1488 max_redirects: u32 = 3,
1472 header_strategy: StorageStrategy = .{ .dynamic = 16 * 1024 },1489 /// Externally-owned memory used to store the server's entire HTTP header.
1490 /// `error.HttpHeadersOversize` is returned from read() when a
1491 /// client sends too many bytes of HTTP headers.
1492 server_header_buffer: []u8,
14731493
1474 /// Must be an already acquired connection.1494 /// Must be an already acquired connection.
1475 connection: ?*Connection = null,1495 connection: ?*Connection = null,
1476
1477 pub const StorageStrategy = union(enum) {
1478 /// In this case, the client's Allocator will be used to store the
1479 /// entire HTTP header. This value is the maximum total size of
1480 /// HTTP headers allowed, otherwise
1481 /// error.HttpHeadersExceededSizeLimit is returned from read().
1482 dynamic: usize,
1483 /// This is used to store the entire HTTP header. If the HTTP
1484 /// header is too big to fit, `error.HttpHeadersExceededSizeLimit`
1485 /// is returned from read(). When this is used, `error.OutOfMemory`
1486 /// cannot be returned from `read()`.
1487 static: []u8,
1488 };
1489};1496};
14901497
1491pub const protocol_map = std.ComptimeStringMap(Connection.Protocol, .{1498pub const protocol_map = std.ComptimeStringMap(Connection.Protocol, .{
...@@ -1502,7 +1509,13 @@ pub const protocol_map = std.ComptimeStringMap(Connection.Protocol, .{...@@ -1502,7 +1509,13 @@ pub const protocol_map = std.ComptimeStringMap(Connection.Protocol, .{
1502///1509///
1503/// The caller is responsible for calling `deinit()` on the `Request`.1510/// The caller is responsible for calling `deinit()` on the `Request`.
1504/// This function is threadsafe.1511/// This function is threadsafe.
1505pub fn open(client: *Client, method: http.Method, uri: Uri, headers: http.Headers, options: RequestOptions) RequestError!Request {1512pub fn open(
1513 client: *Client,
1514 method: http.Method,
1515 uri: Uri,
1516 headers: http.Headers,
1517 options: RequestOptions,
1518) RequestError!Request {
1506 const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme;1519 const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme;
15071520
1508 const port: u16 = uri.port orelse switch (protocol) {1521 const port: u16 = uri.port orelse switch (protocol) {
...@@ -1541,10 +1554,7 @@ pub fn open(client: *Client, method: http.Method, uri: Uri, headers: http.Header...@@ -1541,10 +1554,7 @@ pub fn open(client: *Client, method: http.Method, uri: Uri, headers: http.Header
1541 .reason = undefined,1554 .reason = undefined,
1542 .version = undefined,1555 .version = undefined,
1543 .headers = http.Headers{ .allocator = client.allocator, .owned = false },1556 .headers = http.Headers{ .allocator = client.allocator, .owned = false },
1544 .parser = switch (options.header_strategy) {1557 .parser = proto.HeadersParser.init(options.server_header_buffer),
1545 .dynamic => |max| proto.HeadersParser.initDynamic(max),
1546 .static => |buf| proto.HeadersParser.initStatic(buf),
1547 },
1548 },1558 },
1549 .arena = undefined,1559 .arena = undefined,
1550 };1560 };
...@@ -1568,17 +1578,30 @@ pub const FetchOptions = struct {...@@ -1568,17 +1578,30 @@ pub const FetchOptions = struct {
1568 };1578 };
15691579
1570 pub const ResponseStrategy = union(enum) {1580 pub const ResponseStrategy = union(enum) {
1571 storage: RequestOptions.StorageStrategy,1581 storage: StorageStrategy,
1572 file: std.fs.File,1582 file: std.fs.File,
1573 none,1583 none,
1574 };1584 };
15751585
1576 header_strategy: RequestOptions.StorageStrategy = .{ .dynamic = 16 * 1024 },1586 pub const StorageStrategy = union(enum) {
1587 /// In this case, the client's Allocator will be used to store the
1588 /// entire HTTP header. This value is the maximum total size of
1589 /// HTTP headers allowed, otherwise
1590 /// error.HttpHeadersExceededSizeLimit is returned from read().
1591 dynamic: usize,
1592 /// This is used to store the entire HTTP header. If the HTTP
1593 /// header is too big to fit, `error.HttpHeadersExceededSizeLimit`
1594 /// is returned from read(). When this is used, `error.OutOfMemory`
1595 /// cannot be returned from `read()`.
1596 static: []u8,
1597 };
1598
1599 server_header_buffer: ?[]u8 = null,
1577 response_strategy: ResponseStrategy = .{ .storage = .{ .dynamic = 16 * 1024 * 1024 } },1600 response_strategy: ResponseStrategy = .{ .storage = .{ .dynamic = 16 * 1024 * 1024 } },
15781601
1579 location: Location,1602 location: Location,
1580 method: http.Method = .GET,1603 method: http.Method = .GET,
1581 headers: http.Headers = http.Headers{ .allocator = std.heap.page_allocator, .owned = false },1604 headers: http.Headers = .{ .allocator = std.heap.page_allocator, .owned = false },
1582 payload: Payload = .none,1605 payload: Payload = .none,
1583 raw_uri: bool = false,1606 raw_uri: bool = false,
1584};1607};
...@@ -1613,9 +1636,10 @@ pub fn fetch(client: *Client, allocator: Allocator, options: FetchOptions) !Fetc...@@ -1613,9 +1636,10 @@ pub fn fetch(client: *Client, allocator: Allocator, options: FetchOptions) !Fetc
1613 .url => |u| try Uri.parse(u),1636 .url => |u| try Uri.parse(u),
1614 .uri => |u| u,1637 .uri => |u| u,
1615 };1638 };
1639 var server_header_buffer: [16 * 1024]u8 = undefined;
16161640
1617 var req = try open(client, options.method, uri, options.headers, .{1641 var req = try open(client, options.method, uri, options.headers, .{
1618 .header_strategy = options.header_strategy,1642 .server_header_buffer = options.server_header_buffer orelse &server_header_buffer,
1619 .handle_redirects = options.payload == .none,1643 .handle_redirects = options.payload == .none,
1620 });1644 });
1621 defer req.deinit();1645 defer req.deinit();
lib/std/http/Server.zig+35-42
...@@ -1,6 +1,7 @@...@@ -1,6 +1,7 @@
1//! HTTP Server implementation.1//! HTTP Server implementation.
2//!2//!
3//! This server assumes *all* clients are well behaved and standard compliant; it can and will deadlock if a client holds a connection open without sending a request.3//! This server assumes clients are well behaved and standard compliant; it
4//! deadlocks if a client holds a connection open without sending a request.
4//!5//!
5//! Example usage:6//! Example usage:
6//!7//!
...@@ -17,7 +18,7 @@...@@ -17,7 +18,7 @@
17//! while (res.reset() != .closing) {18//! while (res.reset() != .closing) {
18//! res.wait() catch |err| switch (err) {19//! res.wait() catch |err| switch (err) {
19//! error.HttpHeadersInvalid => break,20//! error.HttpHeadersInvalid => break,
20//! error.HttpHeadersExceededSizeLimit => {21//! error.HttpHeadersOversize => {
21//! res.status = .request_header_fields_too_large;22//! res.status = .request_header_fields_too_large;
22//! res.send() catch break;23//! res.send() catch break;
23//! break;24//! break;
...@@ -39,6 +40,7 @@...@@ -39,6 +40,7 @@
39//! }40//! }
40//! ```41//! ```
4142
43const builtin = @import("builtin");
42const std = @import("../std.zig");44const std = @import("../std.zig");
43const testing = std.testing;45const testing = std.testing;
44const http = std.http;46const http = std.http;
...@@ -86,7 +88,7 @@ pub const Connection = struct {...@@ -86,7 +88,7 @@ pub const Connection = struct {
86 const nread = try conn.rawReadAtLeast(conn.read_buf[0..], 1);88 const nread = try conn.rawReadAtLeast(conn.read_buf[0..], 1);
87 if (nread == 0) return error.EndOfStream;89 if (nread == 0) return error.EndOfStream;
88 conn.read_start = 0;90 conn.read_start = 0;
89 conn.read_end = @as(u16, @intCast(nread));91 conn.read_end = @intCast(nread);
90 }92 }
9193
92 pub fn peek(conn: *Connection) []const u8 {94 pub fn peek(conn: *Connection) []const u8 {
...@@ -382,10 +384,6 @@ pub const Response = struct {...@@ -382,10 +384,6 @@ pub const Response = struct {
382384
383 res.headers.deinit();385 res.headers.deinit();
384 res.request.headers.deinit();386 res.request.headers.deinit();
385
386 if (res.request.parser.header_bytes_owned) {
387 res.request.parser.header_bytes.deinit(res.allocator);
388 }
389 }387 }
390388
391 pub const ResetState = enum { reset, closing };389 pub const ResetState = enum { reset, closing };
...@@ -548,17 +546,24 @@ pub const Response = struct {...@@ -548,17 +546,24 @@ pub const Response = struct {
548 return index;546 return index;
549 }547 }
550548
551 pub const WaitError = Connection.ReadError || proto.HeadersParser.CheckCompleteHeadError || Request.ParseError || error{ CompressionInitializationFailed, CompressionNotSupported };549 pub const WaitError = Connection.ReadError ||
550 proto.HeadersParser.CheckCompleteHeadError || Request.ParseError ||
551 error{ CompressionInitializationFailed, CompressionNotSupported };
552552
553 /// Wait for the client to send a complete request head.553 /// Wait for the client to send a complete request head.
554 ///554 ///
555 /// For correct behavior, the following rules must be followed:555 /// For correct behavior, the following rules must be followed:
556 ///556 ///
557 /// * If this returns any error in `Connection.ReadError`, you MUST immediately close the connection by calling `deinit`.557 /// * If this returns any error in `Connection.ReadError`, you MUST
558 /// * If this returns `error.HttpHeadersInvalid`, you MAY immediately close the connection by calling `deinit`.558 /// immediately close the connection by calling `deinit`.
559 /// * If this returns `error.HttpHeadersExceededSizeLimit`, you MUST respond with a 431 status code and then call `deinit`.559 /// * If this returns `error.HttpHeadersInvalid`, you MAY immediately close
560 /// * If this returns any error in `Request.ParseError`, you MUST respond with a 400 status code and then call `deinit`.560 /// the connection by calling `deinit`.
561 /// * If this returns any other error, you MUST respond with a 400 status code and then call `deinit`.561 /// * If this returns `error.HttpHeadersOversize`, you MUST
562 /// respond with a 431 status code and then call `deinit`.
563 /// * If this returns any error in `Request.ParseError`, you MUST respond
564 /// with a 400 status code and then call `deinit`.
565 /// * If this returns any other error, you MUST respond with a 400 status
566 /// code and then call `deinit`.
562 /// * If the request has an Expect header containing 100-continue, you MUST either:567 /// * If the request has an Expect header containing 100-continue, you MUST either:
563 /// * Respond with a 100 status code, then call `wait` again.568 /// * Respond with a 100 status code, then call `wait` again.
564 /// * Respond with a 417 status code.569 /// * Respond with a 417 status code.
...@@ -571,14 +576,14 @@ pub const Response = struct {...@@ -571,14 +576,14 @@ pub const Response = struct {
571 while (true) {576 while (true) {
572 try res.connection.fill();577 try res.connection.fill();
573578
574 const nchecked = try res.request.parser.checkCompleteHead(res.allocator, res.connection.peek());579 const nchecked = try res.request.parser.checkCompleteHead(res.connection.peek());
575 res.connection.drop(@as(u16, @intCast(nchecked)));580 res.connection.drop(@intCast(nchecked));
576581
577 if (res.request.parser.state.isContent()) break;582 if (res.request.parser.state.isContent()) break;
578 }583 }
579584
580 res.request.headers = .{ .allocator = res.allocator, .owned = true };585 res.request.headers = .{ .allocator = res.allocator, .owned = true };
581 try res.request.parse(res.request.parser.header_bytes.items);586 try res.request.parse(res.request.parser.get());
582587
583 if (res.request.transfer_encoding != .none) {588 if (res.request.transfer_encoding != .none) {
584 switch (res.request.transfer_encoding) {589 switch (res.request.transfer_encoding) {
...@@ -641,16 +646,18 @@ pub const Response = struct {...@@ -641,16 +646,18 @@ pub const Response = struct {
641 while (!res.request.parser.state.isContent()) { // read trailing headers646 while (!res.request.parser.state.isContent()) { // read trailing headers
642 try res.connection.fill();647 try res.connection.fill();
643648
644 const nchecked = try res.request.parser.checkCompleteHead(res.allocator, res.connection.peek());649 const nchecked = try res.request.parser.checkCompleteHead(res.connection.peek());
645 res.connection.drop(@as(u16, @intCast(nchecked)));650 res.connection.drop(@intCast(nchecked));
646 }651 }
647652
648 if (has_trail) {653 if (has_trail) {
649 res.request.headers = http.Headers{ .allocator = res.allocator, .owned = false };654 res.request.headers = http.Headers{ .allocator = res.allocator, .owned = false };
650655
651 // The response headers before the trailers are already guaranteed to be valid, so they will always be parsed again and cannot return an error.656 // The response headers before the trailers are already
657 // guaranteed to be valid, so they will always be parsed again
658 // and cannot return an error.
652 // This will *only* fail for a malformed trailer.659 // This will *only* fail for a malformed trailer.
653 res.request.parse(res.request.parser.header_bytes.items) catch return error.InvalidTrailers;660 res.request.parse(res.request.parser.get()) catch return error.InvalidTrailers;
654 }661 }
655 }662 }
656663
...@@ -751,29 +758,19 @@ pub fn listen(server: *Server, address: net.Address) ListenError!void {...@@ -751,29 +758,19 @@ pub fn listen(server: *Server, address: net.Address) ListenError!void {
751758
752pub const AcceptError = net.StreamServer.AcceptError || Allocator.Error;759pub const AcceptError = net.StreamServer.AcceptError || Allocator.Error;
753760
754pub const HeaderStrategy = union(enum) {
755 /// In this case, the client's Allocator will be used to store the
756 /// entire HTTP header. This value is the maximum total size of
757 /// HTTP headers allowed, otherwise
758 /// error.HttpHeadersExceededSizeLimit is returned from read().
759 dynamic: usize,
760 /// This is used to store the entire HTTP header. If the HTTP
761 /// header is too big to fit, `error.HttpHeadersExceededSizeLimit`
762 /// is returned from read(). When this is used, `error.OutOfMemory`
763 /// cannot be returned from `read()`.
764 static: []u8,
765};
766
767pub const AcceptOptions = struct {761pub const AcceptOptions = struct {
768 allocator: Allocator,762 allocator: Allocator,
769 header_strategy: HeaderStrategy = .{ .dynamic = 8192 },763 /// Externally-owned memory used to store the client's entire HTTP header.
764 /// `error.HttpHeadersOversize` is returned from read() when a
765 /// client sends too many bytes of HTTP headers.
766 client_header_buffer: []u8,
770};767};
771768
772/// Accept a new connection.769/// Accept a new connection.
773pub fn accept(server: *Server, options: AcceptOptions) AcceptError!Response {770pub fn accept(server: *Server, options: AcceptOptions) AcceptError!Response {
774 const in = try server.socket.accept();771 const in = try server.socket.accept();
775772
776 return Response{773 return .{
777 .allocator = options.allocator,774 .allocator = options.allocator,
778 .address = in.address,775 .address = in.address,
779 .connection = .{776 .connection = .{
...@@ -786,17 +783,12 @@ pub fn accept(server: *Server, options: AcceptOptions) AcceptError!Response {...@@ -786,17 +783,12 @@ pub fn accept(server: *Server, options: AcceptOptions) AcceptError!Response {
786 .method = undefined,783 .method = undefined,
787 .target = undefined,784 .target = undefined,
788 .headers = .{ .allocator = options.allocator, .owned = false },785 .headers = .{ .allocator = options.allocator, .owned = false },
789 .parser = switch (options.header_strategy) {786 .parser = proto.HeadersParser.init(options.client_header_buffer),
790 .dynamic => |max| proto.HeadersParser.initDynamic(max),
791 .static => |buf| proto.HeadersParser.initStatic(buf),
792 },
793 },787 },
794 };788 };
795}789}
796790
797test "HTTP server handles a chunked transfer coding request" {791test "HTTP server handles a chunked transfer coding request" {
798 const builtin = @import("builtin");
799
800 // This test requires spawning threads.792 // This test requires spawning threads.
801 if (builtin.single_threaded) {793 if (builtin.single_threaded) {
802 return error.SkipZigTest;794 return error.SkipZigTest;
...@@ -823,9 +815,10 @@ test "HTTP server handles a chunked transfer coding request" {...@@ -823,9 +815,10 @@ test "HTTP server handles a chunked transfer coding request" {
823815
824 const server_thread = try std.Thread.spawn(.{}, (struct {816 const server_thread = try std.Thread.spawn(.{}, (struct {
825 fn apply(s: *std.http.Server) !void {817 fn apply(s: *std.http.Server) !void {
818 var header_buffer: [max_header_size]u8 = undefined;
826 var res = try s.accept(.{819 var res = try s.accept(.{
827 .allocator = allocator,820 .allocator = allocator,
828 .header_strategy = .{ .dynamic = max_header_size },821 .client_header_buffer = &header_buffer,
829 });822 });
830 defer res.deinit();823 defer res.deinit();
831 defer _ = res.reset();824 defer _ = res.reset();
lib/std/http/protocol.zig+70-74
...@@ -34,54 +34,49 @@ pub const State = enum {...@@ -34,54 +34,49 @@ pub const State = enum {
3434
35pub const HeadersParser = struct {35pub const HeadersParser = struct {
36 state: State = .start,36 state: State = .start,
37 /// Whether or not `header_bytes` is allocated or was provided as a fixed buffer.37 /// A fixed buffer of len `max_header_bytes`.
38 header_bytes_owned: bool,
39 /// Either a fixed buffer of len `max_header_bytes` or a dynamic buffer that can grow up to `max_header_bytes`.
40 /// Pointers into this buffer are not stable until after a message is complete.38 /// Pointers into this buffer are not stable until after a message is complete.
41 header_bytes: std.ArrayListUnmanaged(u8),39 header_bytes_buffer: []u8,
42 /// The maximum allowed size of `header_bytes`.40 header_bytes_len: u32,
43 max_header_bytes: usize,41 next_chunk_length: u64,
44 next_chunk_length: u64 = 0,
45 /// Whether this parser is done parsing a complete message.42 /// Whether this parser is done parsing a complete message.
46 /// A message is only done when the entire payload has been read.43 /// A message is only done when the entire payload has been read.
47 done: bool = false,44 done: bool,
4845
49 /// Initializes the parser with a dynamically growing header buffer of up to `max` bytes.46 /// Initializes the parser with a provided buffer `buf`.
50 pub fn initDynamic(max: usize) HeadersParser {47 pub fn init(buf: []u8) HeadersParser {
51 return .{48 return .{
52 .header_bytes = .{},49 .header_bytes_buffer = buf,
53 .max_header_bytes = max,50 .header_bytes_len = 0,
54 .header_bytes_owned = true,51 .done = false,
52 .next_chunk_length = 0,
55 };53 };
56 }54 }
5755
58 /// Initializes the parser with a provided buffer `buf`.56 /// Reinitialize the parser.
59 pub fn initStatic(buf: []u8) HeadersParser {57 /// Asserts the parser is in the "done" state.
60 return .{58 pub fn reset(hp: *HeadersParser) void {
61 .header_bytes = .{ .items = buf[0..0], .capacity = buf.len },59 assert(hp.done);
62 .max_header_bytes = buf.len,60 hp.* = .{
63 .header_bytes_owned = false,61 .state = .start,
62 .header_bytes_buffer = hp.header_bytes_buffer,
63 .header_bytes_len = 0,
64 .done = false,
65 .next_chunk_length = 0,
64 };66 };
65 }67 }
6668
67 /// Completely resets the parser to it's initial state.69 pub fn get(hp: HeadersParser) []u8 {
68 /// This must be called after a message is complete.70 return hp.header_bytes_buffer[0..hp.header_bytes_len];
69 pub fn reset(r: *HeadersParser) void {
70 assert(r.done); // The message must be completely read before reset, otherwise the parser is in an invalid state.
71
72 r.header_bytes.clearRetainingCapacity();
73
74 r.* = .{
75 .header_bytes = r.header_bytes,
76 .max_header_bytes = r.max_header_bytes,
77 .header_bytes_owned = r.header_bytes_owned,
78 };
79 }71 }
8072
81 /// Returns the number of bytes consumed by headers. This is always less than or equal to `bytes.len`.73 /// Returns the number of bytes consumed by headers. This is always less
82 /// You should check `r.state.isContent()` after this to check if the headers are done.74 /// than or equal to `bytes.len`.
75 /// You should check `r.state.isContent()` after this to check if the
76 /// headers are done.
83 ///77 ///
84 /// If the amount returned is less than `bytes.len`, you may assume that the parser is in a content state and the78 /// If the amount returned is less than `bytes.len`, you may assume that
79 /// the parser is in a content state and the
85 /// first byte of content is located at `bytes[result]`.80 /// first byte of content is located at `bytes[result]`.
86 pub fn findHeadersEnd(r: *HeadersParser, bytes: []const u8) u32 {81 pub fn findHeadersEnd(r: *HeadersParser, bytes: []const u8) u32 {
87 const vector_len: comptime_int = @max(std.simd.suggestVectorLength(u8) orelse 1, 8);82 const vector_len: comptime_int = @max(std.simd.suggestVectorLength(u8) orelse 1, 8);
...@@ -410,11 +405,14 @@ pub const HeadersParser = struct {...@@ -410,11 +405,14 @@ pub const HeadersParser = struct {
410 }405 }
411 }406 }
412407
413 /// Returns the number of bytes consumed by the chunk size. This is always less than or equal to `bytes.len`.408 /// Returns the number of bytes consumed by the chunk size. This is always
414 /// You should check `r.state == .chunk_data` after this to check if the chunk size has been fully parsed.409 /// less than or equal to `bytes.len`.
410 /// You should check `r.state == .chunk_data` after this to check if the
411 /// chunk size has been fully parsed.
415 ///412 ///
416 /// If the amount returned is less than `bytes.len`, you may assume that the parser is in the `chunk_data` state413 /// If the amount returned is less than `bytes.len`, you may assume that
417 /// and that the first byte of the chunk is at `bytes[result]`.414 /// the parser is in the `chunk_data` state and that the first byte of the
415 /// chunk is at `bytes[result]`.
418 pub fn findChunkedLen(r: *HeadersParser, bytes: []const u8) u32 {416 pub fn findChunkedLen(r: *HeadersParser, bytes: []const u8) u32 {
419 const len = @as(u32, @intCast(bytes.len));417 const len = @as(u32, @intCast(bytes.len));
420418
...@@ -488,30 +486,27 @@ pub const HeadersParser = struct {...@@ -488,30 +486,27 @@ pub const HeadersParser = struct {
488 return len;486 return len;
489 }487 }
490488
491 /// Returns whether or not the parser has finished parsing a complete message. A message is only complete after the489 /// Returns whether or not the parser has finished parsing a complete
492 /// entire body has been read and any trailing headers have been parsed.490 /// message. A message is only complete after the entire body has been read
491 /// and any trailing headers have been parsed.
493 pub fn isComplete(r: *HeadersParser) bool {492 pub fn isComplete(r: *HeadersParser) bool {
494 return r.done and r.state == .finished;493 return r.done and r.state == .finished;
495 }494 }
496495
497 pub const CheckCompleteHeadError = mem.Allocator.Error || error{HttpHeadersExceededSizeLimit};496 pub const CheckCompleteHeadError = error{HttpHeadersOversize};
498497
499 /// Pushes `in` into the parser. Returns the number of bytes consumed by the header. Any header bytes are appended498 /// Pushes `in` into the parser. Returns the number of bytes consumed by
500 /// to the `header_bytes` buffer.499 /// the header. Any header bytes are appended to `header_bytes_buffer`.
501 ///500 pub fn checkCompleteHead(hp: *HeadersParser, in: []const u8) CheckCompleteHeadError!u32 {
502 /// This function only uses `allocator` if `r.header_bytes_owned` is true, and may be undefined otherwise.501 if (hp.state.isContent()) return 0;
503 pub fn checkCompleteHead(r: *HeadersParser, allocator: std.mem.Allocator, in: []const u8) CheckCompleteHeadError!u32 {
504 if (r.state.isContent()) return 0;
505502
506 const i = r.findHeadersEnd(in);503 const i = hp.findHeadersEnd(in);
507 const data = in[0..i];504 const data = in[0..i];
508 if (r.header_bytes.items.len + data.len > r.max_header_bytes) {505 if (hp.header_bytes_len + data.len > hp.header_bytes_buffer.len)
509 return error.HttpHeadersExceededSizeLimit;506 return error.HttpHeadersOversize;
510 } else {
511 if (r.header_bytes_owned) try r.header_bytes.ensureUnusedCapacity(allocator, data.len);
512507
513 r.header_bytes.appendSliceAssumeCapacity(data);508 @memcpy(hp.header_bytes_buffer[hp.header_bytes_len..][0..data.len], data);
514 }509 hp.header_bytes_len += @intCast(data.len);
515510
516 return i;511 return i;
517 }512 }
...@@ -520,7 +515,8 @@ pub const HeadersParser = struct {...@@ -520,7 +515,8 @@ pub const HeadersParser = struct {
520 HttpChunkInvalid,515 HttpChunkInvalid,
521 };516 };
522517
523 /// Reads the body of the message into `buffer`. Returns the number of bytes placed in the buffer.518 /// Reads the body of the message into `buffer`. Returns the number of
519 /// bytes placed in the buffer.
524 ///520 ///
525 /// If `skip` is true, the buffer will be unused and the body will be skipped.521 /// If `skip` is true, the buffer will be unused and the body will be skipped.
526 ///522 ///
...@@ -718,7 +714,7 @@ test "HeadersParser.findHeadersEnd" {...@@ -718,7 +714,7 @@ test "HeadersParser.findHeadersEnd" {
718 const data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\nHello";714 const data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\nHello";
719715
720 for (0..36) |i| {716 for (0..36) |i| {
721 r = HeadersParser.initDynamic(0);717 r = HeadersParser.init(&.{});
722 try std.testing.expectEqual(@as(u32, @intCast(i)), r.findHeadersEnd(data[0..i]));718 try std.testing.expectEqual(@as(u32, @intCast(i)), r.findHeadersEnd(data[0..i]));
723 try std.testing.expectEqual(@as(u32, @intCast(35 - i)), r.findHeadersEnd(data[i..]));719 try std.testing.expectEqual(@as(u32, @intCast(35 - i)), r.findHeadersEnd(data[i..]));
724 }720 }
...@@ -728,7 +724,7 @@ test "HeadersParser.findChunkedLen" {...@@ -728,7 +724,7 @@ test "HeadersParser.findChunkedLen" {
728 var r: HeadersParser = undefined;724 var r: HeadersParser = undefined;
729 const data = "Ff\r\nf0f000 ; ext\n0\r\nffffffffffffffffffffffffffffffffffffffff\r\n";725 const data = "Ff\r\nf0f000 ; ext\n0\r\nffffffffffffffffffffffffffffffffffffffff\r\n";
730726
731 r = HeadersParser.initDynamic(0);727 r = HeadersParser.init(&.{});
732 r.state = .chunk_head_size;728 r.state = .chunk_head_size;
733 r.next_chunk_length = 0;729 r.next_chunk_length = 0;
734730
...@@ -761,9 +757,9 @@ test "HeadersParser.findChunkedLen" {...@@ -761,9 +757,9 @@ test "HeadersParser.findChunkedLen" {
761757
762test "HeadersParser.read length" {758test "HeadersParser.read length" {
763 // mock BufferedConnection for read759 // mock BufferedConnection for read
760 var headers_buf: [256]u8 = undefined;
764761
765 var r = HeadersParser.initDynamic(256);762 var r = HeadersParser.init(&headers_buf);
766 defer r.header_bytes.deinit(std.testing.allocator);
767 const data = "GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 5\r\n\r\nHello";763 const data = "GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 5\r\n\r\nHello";
768764
769 var conn: MockBufferedConnection = .{765 var conn: MockBufferedConnection = .{
...@@ -773,8 +769,8 @@ test "HeadersParser.read length" {...@@ -773,8 +769,8 @@ test "HeadersParser.read length" {
773 while (true) { // read headers769 while (true) { // read headers
774 try conn.fill();770 try conn.fill();
775771
776 const nchecked = try r.checkCompleteHead(std.testing.allocator, conn.peek());772 const nchecked = try r.checkCompleteHead(conn.peek());
777 conn.drop(@as(u16, @intCast(nchecked)));773 conn.drop(@intCast(nchecked));
778774
779 if (r.state.isContent()) break;775 if (r.state.isContent()) break;
780 }776 }
...@@ -786,14 +782,14 @@ test "HeadersParser.read length" {...@@ -786,14 +782,14 @@ test "HeadersParser.read length" {
786 try std.testing.expectEqual(@as(usize, 5), len);782 try std.testing.expectEqual(@as(usize, 5), len);
787 try std.testing.expectEqualStrings("Hello", buf[0..len]);783 try std.testing.expectEqualStrings("Hello", buf[0..len]);
788784
789 try std.testing.expectEqualStrings("GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 5\r\n\r\n", r.header_bytes.items);785 try std.testing.expectEqualStrings("GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 5\r\n\r\n", r.get());
790}786}
791787
792test "HeadersParser.read chunked" {788test "HeadersParser.read chunked" {
793 // mock BufferedConnection for read789 // mock BufferedConnection for read
794790
795 var r = HeadersParser.initDynamic(256);791 var headers_buf: [256]u8 = undefined;
796 defer r.header_bytes.deinit(std.testing.allocator);792 var r = HeadersParser.init(&headers_buf);
797 const data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n2\r\nHe\r\n2\r\nll\r\n1\r\no\r\n0\r\n\r\n";793 const data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n2\r\nHe\r\n2\r\nll\r\n1\r\no\r\n0\r\n\r\n";
798794
799 var conn: MockBufferedConnection = .{795 var conn: MockBufferedConnection = .{
...@@ -803,8 +799,8 @@ test "HeadersParser.read chunked" {...@@ -803,8 +799,8 @@ test "HeadersParser.read chunked" {
803 while (true) { // read headers799 while (true) { // read headers
804 try conn.fill();800 try conn.fill();
805801
806 const nchecked = try r.checkCompleteHead(std.testing.allocator, conn.peek());802 const nchecked = try r.checkCompleteHead(conn.peek());
807 conn.drop(@as(u16, @intCast(nchecked)));803 conn.drop(@intCast(nchecked));
808804
809 if (r.state.isContent()) break;805 if (r.state.isContent()) break;
810 }806 }
...@@ -815,14 +811,14 @@ test "HeadersParser.read chunked" {...@@ -815,14 +811,14 @@ test "HeadersParser.read chunked" {
815 try std.testing.expectEqual(@as(usize, 5), len);811 try std.testing.expectEqual(@as(usize, 5), len);
816 try std.testing.expectEqualStrings("Hello", buf[0..len]);812 try std.testing.expectEqualStrings("Hello", buf[0..len]);
817813
818 try std.testing.expectEqualStrings("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n", r.header_bytes.items);814 try std.testing.expectEqualStrings("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n", r.get());
819}815}
820816
821test "HeadersParser.read chunked trailer" {817test "HeadersParser.read chunked trailer" {
822 // mock BufferedConnection for read818 // mock BufferedConnection for read
823819
824 var r = HeadersParser.initDynamic(256);820 var headers_buf: [256]u8 = undefined;
825 defer r.header_bytes.deinit(std.testing.allocator);821 var r = HeadersParser.init(&headers_buf);
826 const data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n2\r\nHe\r\n2\r\nll\r\n1\r\no\r\n0\r\nContent-Type: text/plain\r\n\r\n";822 const data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n2\r\nHe\r\n2\r\nll\r\n1\r\no\r\n0\r\nContent-Type: text/plain\r\n\r\n";
827823
828 var conn: MockBufferedConnection = .{824 var conn: MockBufferedConnection = .{
...@@ -832,8 +828,8 @@ test "HeadersParser.read chunked trailer" {...@@ -832,8 +828,8 @@ test "HeadersParser.read chunked trailer" {
832 while (true) { // read headers828 while (true) { // read headers
833 try conn.fill();829 try conn.fill();
834830
835 const nchecked = try r.checkCompleteHead(std.testing.allocator, conn.peek());831 const nchecked = try r.checkCompleteHead(conn.peek());
836 conn.drop(@as(u16, @intCast(nchecked)));832 conn.drop(@intCast(nchecked));
837833
838 if (r.state.isContent()) break;834 if (r.state.isContent()) break;
839 }835 }
...@@ -847,11 +843,11 @@ test "HeadersParser.read chunked trailer" {...@@ -847,11 +843,11 @@ test "HeadersParser.read chunked trailer" {
847 while (true) { // read headers843 while (true) { // read headers
848 try conn.fill();844 try conn.fill();
849845
850 const nchecked = try r.checkCompleteHead(std.testing.allocator, conn.peek());846 const nchecked = try r.checkCompleteHead(conn.peek());
851 conn.drop(@as(u16, @intCast(nchecked)));847 conn.drop(@intCast(nchecked));
852848
853 if (r.state.isContent()) break;849 if (r.state.isContent()) break;
854 }850 }
855851
856 try std.testing.expectEqualStrings("GET / HTTP/1.1\r\nHost: localhost\r\n\r\nContent-Type: text/plain\r\n\r\n", r.header_bytes.items);852 try std.testing.expectEqualStrings("GET / HTTP/1.1\r\nHost: localhost\r\n\r\nContent-Type: text/plain\r\n\r\n", r.get());
857}853}
src/Package/Fetch.zig+14-7
...@@ -354,7 +354,8 @@ pub fn run(f: *Fetch) RunError!void {...@@ -354,7 +354,8 @@ pub fn run(f: *Fetch) RunError!void {
354 .{ path_or_url, @errorName(file_err), @errorName(uri_err) },354 .{ path_or_url, @errorName(file_err), @errorName(uri_err) },
355 ));355 ));
356 };356 };
357 var resource = try f.initResource(uri);357 var server_header_buffer: [header_buffer_size]u8 = undefined;
358 var resource = try f.initResource(uri, &server_header_buffer);
358 return runResource(f, uri.path, &resource, null);359 return runResource(f, uri.path, &resource, null);
359 }360 }
360 },361 },
...@@ -415,7 +416,8 @@ pub fn run(f: *Fetch) RunError!void {...@@ -415,7 +416,8 @@ pub fn run(f: *Fetch) RunError!void {
415 f.location_tok,416 f.location_tok,
416 try eb.printString("invalid URI: {s}", .{@errorName(err)}),417 try eb.printString("invalid URI: {s}", .{@errorName(err)}),
417 );418 );
418 var resource = try f.initResource(uri);419 var server_header_buffer: [header_buffer_size]u8 = undefined;
420 var resource = try f.initResource(uri, &server_header_buffer);
419 return runResource(f, uri.path, &resource, remote.hash);421 return runResource(f, uri.path, &resource, remote.hash);
420}422}
421423
...@@ -876,7 +878,9 @@ const FileType = enum {...@@ -876,7 +878,9 @@ const FileType = enum {
876 }878 }
877};879};
878880
879fn initResource(f: *Fetch, uri: std.Uri) RunError!Resource {881const header_buffer_size = 16 * 1024;
882
883fn initResource(f: *Fetch, uri: std.Uri, server_header_buffer: []u8) RunError!Resource {
880 const gpa = f.arena.child_allocator;884 const gpa = f.arena.child_allocator;
881 const arena = f.arena.allocator();885 const arena = f.arena.allocator();
882 const eb = &f.error_bundle;886 const eb = &f.error_bundle;
...@@ -894,10 +898,12 @@ fn initResource(f: *Fetch, uri: std.Uri) RunError!Resource {...@@ -894,10 +898,12 @@ fn initResource(f: *Fetch, uri: std.Uri) RunError!Resource {
894 if (ascii.eqlIgnoreCase(uri.scheme, "http") or898 if (ascii.eqlIgnoreCase(uri.scheme, "http") or
895 ascii.eqlIgnoreCase(uri.scheme, "https"))899 ascii.eqlIgnoreCase(uri.scheme, "https"))
896 {900 {
897 var h = std.http.Headers{ .allocator = gpa };901 var h: std.http.Headers = .{ .allocator = gpa };
898 defer h.deinit();902 defer h.deinit();
899903
900 var req = http_client.open(.GET, uri, h, .{}) catch |err| {904 var req = http_client.open(.GET, uri, h, .{
905 .server_header_buffer = server_header_buffer,
906 }) catch |err| {
901 return f.fail(f.location_tok, try eb.printString(907 return f.fail(f.location_tok, try eb.printString(
902 "unable to connect to server: {s}",908 "unable to connect to server: {s}",
903 .{@errorName(err)},909 .{@errorName(err)},
...@@ -935,7 +941,7 @@ fn initResource(f: *Fetch, uri: std.Uri) RunError!Resource {...@@ -935,7 +941,7 @@ fn initResource(f: *Fetch, uri: std.Uri) RunError!Resource {
935 transport_uri.scheme = uri.scheme["git+".len..];941 transport_uri.scheme = uri.scheme["git+".len..];
936 var redirect_uri: []u8 = undefined;942 var redirect_uri: []u8 = undefined;
937 var session: git.Session = .{ .transport = http_client, .uri = transport_uri };943 var session: git.Session = .{ .transport = http_client, .uri = transport_uri };
938 session.discoverCapabilities(gpa, &redirect_uri) catch |err| switch (err) {944 session.discoverCapabilities(gpa, &redirect_uri, server_header_buffer) catch |err| switch (err) {
939 error.Redirected => {945 error.Redirected => {
940 defer gpa.free(redirect_uri);946 defer gpa.free(redirect_uri);
941 return f.fail(f.location_tok, try eb.printString(947 return f.fail(f.location_tok, try eb.printString(
...@@ -961,6 +967,7 @@ fn initResource(f: *Fetch, uri: std.Uri) RunError!Resource {...@@ -961,6 +967,7 @@ fn initResource(f: *Fetch, uri: std.Uri) RunError!Resource {
961 var ref_iterator = session.listRefs(gpa, .{967 var ref_iterator = session.listRefs(gpa, .{
962 .ref_prefixes = &.{ want_ref, want_ref_head, want_ref_tag },968 .ref_prefixes = &.{ want_ref, want_ref_head, want_ref_tag },
963 .include_peeled = true,969 .include_peeled = true,
970 .server_header_buffer = server_header_buffer,
964 }) catch |err| {971 }) catch |err| {
965 return f.fail(f.location_tok, try eb.printString(972 return f.fail(f.location_tok, try eb.printString(
966 "unable to list refs: {s}",973 "unable to list refs: {s}",
...@@ -1003,7 +1010,7 @@ fn initResource(f: *Fetch, uri: std.Uri) RunError!Resource {...@@ -1003,7 +1010,7 @@ fn initResource(f: *Fetch, uri: std.Uri) RunError!Resource {
1003 _ = std.fmt.bufPrint(&want_oid_buf, "{}", .{1010 _ = std.fmt.bufPrint(&want_oid_buf, "{}", .{
1004 std.fmt.fmtSliceHexLower(&want_oid),1011 std.fmt.fmtSliceHexLower(&want_oid),
1005 }) catch unreachable;1012 }) catch unreachable;
1006 var fetch_stream = session.fetch(gpa, &.{&want_oid_buf}) catch |err| {1013 var fetch_stream = session.fetch(gpa, &.{&want_oid_buf}, server_header_buffer) catch |err| {
1007 return f.fail(f.location_tok, try eb.printString(1014 return f.fail(f.location_tok, try eb.printString(
1008 "unable to create fetch stream: {s}",1015 "unable to create fetch stream: {s}",
1009 .{@errorName(err)},1016 .{@errorName(err)},
src/Package/Fetch/git.zig+13-2
...@@ -494,8 +494,9 @@ pub const Session = struct {...@@ -494,8 +494,9 @@ pub const Session = struct {
494 session: *Session,494 session: *Session,
495 allocator: Allocator,495 allocator: Allocator,
496 redirect_uri: *[]u8,496 redirect_uri: *[]u8,
497 http_headers_buffer: []u8,
497 ) !void {498 ) !void {
498 var capability_iterator = try session.getCapabilities(allocator, redirect_uri);499 var capability_iterator = try session.getCapabilities(allocator, redirect_uri, http_headers_buffer);
499 defer capability_iterator.deinit();500 defer capability_iterator.deinit();
500 while (try capability_iterator.next()) |capability| {501 while (try capability_iterator.next()) |capability| {
501 if (mem.eql(u8, capability.key, "agent")) {502 if (mem.eql(u8, capability.key, "agent")) {
...@@ -521,6 +522,7 @@ pub const Session = struct {...@@ -521,6 +522,7 @@ pub const Session = struct {
521 session: Session,522 session: Session,
522 allocator: Allocator,523 allocator: Allocator,
523 redirect_uri: *[]u8,524 redirect_uri: *[]u8,
525 http_headers_buffer: []u8,
524 ) !CapabilityIterator {526 ) !CapabilityIterator {
525 var info_refs_uri = session.uri;527 var info_refs_uri = session.uri;
526 info_refs_uri.path = try std.fs.path.resolvePosix(allocator, &.{ "/", session.uri.path, "info/refs" });528 info_refs_uri.path = try std.fs.path.resolvePosix(allocator, &.{ "/", session.uri.path, "info/refs" });
...@@ -534,6 +536,7 @@ pub const Session = struct {...@@ -534,6 +536,7 @@ pub const Session = struct {
534536
535 var request = try session.transport.open(.GET, info_refs_uri, headers, .{537 var request = try session.transport.open(.GET, info_refs_uri, headers, .{
536 .max_redirects = 3,538 .max_redirects = 3,
539 .server_header_buffer = http_headers_buffer,
537 });540 });
538 errdefer request.deinit();541 errdefer request.deinit();
539 try request.send(.{});542 try request.send(.{});
...@@ -620,6 +623,7 @@ pub const Session = struct {...@@ -620,6 +623,7 @@ pub const Session = struct {
620 include_symrefs: bool = false,623 include_symrefs: bool = false,
621 /// Whether to include the peeled object ID for returned tag refs.624 /// Whether to include the peeled object ID for returned tag refs.
622 include_peeled: bool = false,625 include_peeled: bool = false,
626 server_header_buffer: []u8,
623 };627 };
624628
625 /// Returns an iterator over refs known to the server.629 /// Returns an iterator over refs known to the server.
...@@ -658,6 +662,7 @@ pub const Session = struct {...@@ -658,6 +662,7 @@ pub const Session = struct {
658662
659 var request = try session.transport.open(.POST, upload_pack_uri, headers, .{663 var request = try session.transport.open(.POST, upload_pack_uri, headers, .{
660 .handle_redirects = false,664 .handle_redirects = false,
665 .server_header_buffer = options.server_header_buffer,
661 });666 });
662 errdefer request.deinit();667 errdefer request.deinit();
663 request.transfer_encoding = .{ .content_length = body.items.len };668 request.transfer_encoding = .{ .content_length = body.items.len };
...@@ -721,7 +726,12 @@ pub const Session = struct {...@@ -721,7 +726,12 @@ pub const Session = struct {
721726
722 /// Fetches the given refs from the server. A shallow fetch (depth 1) is727 /// Fetches the given refs from the server. A shallow fetch (depth 1) is
723 /// performed if the server supports it.728 /// performed if the server supports it.
724 pub fn fetch(session: Session, allocator: Allocator, wants: []const []const u8) !FetchStream {729 pub fn fetch(
730 session: Session,
731 allocator: Allocator,
732 wants: []const []const u8,
733 http_headers_buffer: []u8,
734 ) !FetchStream {
725 var upload_pack_uri = session.uri;735 var upload_pack_uri = session.uri;
726 upload_pack_uri.path = try std.fs.path.resolvePosix(allocator, &.{ "/", session.uri.path, "git-upload-pack" });736 upload_pack_uri.path = try std.fs.path.resolvePosix(allocator, &.{ "/", session.uri.path, "git-upload-pack" });
727 defer allocator.free(upload_pack_uri.path);737 defer allocator.free(upload_pack_uri.path);
...@@ -758,6 +768,7 @@ pub const Session = struct {...@@ -758,6 +768,7 @@ pub const Session = struct {
758768
759 var request = try session.transport.open(.POST, upload_pack_uri, headers, .{769 var request = try session.transport.open(.POST, upload_pack_uri, headers, .{
760 .handle_redirects = false,770 .handle_redirects = false,
771 .server_header_buffer = http_headers_buffer,
761 });772 });
762 errdefer request.deinit();773 errdefer request.deinit();
763 request.transfer_encoding = .{ .content_length = body.items.len };774 request.transfer_encoding = .{ .content_length = body.items.len };