diff --git a/lib/std/http.zig b/lib/std/http.zig index 1ef90fd126..fd667e7832 100644 --- a/lib/std/http.zig +++ b/lib/std/http.zig @@ -1,6 +1,10 @@ pub const Client = @import("http/Client.zig"); pub const Server = @import("http/Server.zig"); pub const protocol = @import("http/protocol.zig"); +const headers = @import("http/Headers.zig"); + +pub const Headers = headers.Headers; +pub const Header = headers.HeaderEntry; pub const Version = enum { @"HTTP/1.0", @@ -265,11 +269,6 @@ pub const Connection = enum { close, }; -pub const Header = struct { - name: []const u8, - value: []const u8, -}; - const std = @import("std.zig"); test { diff --git a/lib/std/http/Client.zig b/lib/std/http/Client.zig index 07a56db36a..614b70b216 100644 --- a/lib/std/http/Client.zig +++ b/lib/std/http/Client.zig @@ -348,140 +348,125 @@ pub const Compression = union(enum) { /// A HTTP response originating from a server. pub const Response = struct { - pub const Headers = struct { - status: http.Status, - version: http.Version, - location: ?[]const u8 = null, - content_length: ?u64 = null, - transfer_encoding: ?http.TransferEncoding = null, - transfer_compression: ?http.ContentEncoding = null, - connection: http.Connection = .close, - upgrade: ?[]const u8 = null, + pub const ParseError = Allocator.Error || error{ + ShortHttpStatusLine, + BadHttpVersion, + HttpHeadersInvalid, + HttpHeaderContinuationsUnsupported, + HttpTransferEncodingUnsupported, + HttpConnectionHeaderUnsupported, + InvalidContentLength, + CompressionNotSupported, + }; - pub const ParseError = error{ - ShortHttpStatusLine, - BadHttpVersion, - HttpHeadersInvalid, - HttpHeaderContinuationsUnsupported, - HttpTransferEncodingUnsupported, - HttpConnectionHeaderUnsupported, - InvalidContentLength, - CompressionNotSupported, + pub fn parse(res: *Response, bytes: []const u8) ParseError!void { + var it = mem.tokenize(u8, bytes[0 .. bytes.len - 4], "\r\n"); + + const first_line = it.next() orelse return error.HttpHeadersInvalid; + if (first_line.len < 12) + return error.ShortHttpStatusLine; + + const version: http.Version = switch (int64(first_line[0..8])) { + int64("HTTP/1.0") => .@"HTTP/1.0", + int64("HTTP/1.1") => .@"HTTP/1.1", + else => return error.BadHttpVersion, }; + if (first_line[8] != ' ') return error.HttpHeadersInvalid; + const status = @intToEnum(http.Status, parseInt3(first_line[9..12].*)); + const reason = mem.trimLeft(u8, first_line[12..], " "); - pub fn parse(bytes: []const u8) ParseError!Headers { - var it = mem.tokenize(u8, bytes[0 .. bytes.len - 4], "\r\n"); + res.version = version; + res.status = status; + res.reason = reason; - const first_line = it.next() orelse return error.HttpHeadersInvalid; - if (first_line.len < 12) - return error.ShortHttpStatusLine; + while (it.next()) |line| { + if (line.len == 0) return error.HttpHeadersInvalid; + switch (line[0]) { + ' ', '\t' => return error.HttpHeaderContinuationsUnsupported, + else => {}, + } - const version: http.Version = switch (int64(first_line[0..8])) { - int64("HTTP/1.0") => .@"HTTP/1.0", - int64("HTTP/1.1") => .@"HTTP/1.1", - else => return error.BadHttpVersion, - }; - if (first_line[8] != ' ') return error.HttpHeadersInvalid; - const status = @intToEnum(http.Status, parseInt3(first_line[9..12].*)); + var line_it = mem.tokenize(u8, line, ": "); + const header_name = line_it.next() orelse return error.HttpHeadersInvalid; + const header_value = line_it.rest(); - var headers: Headers = .{ - .version = version, - .status = status, - }; + try res.headers.append(header_name, header_value); - while (it.next()) |line| { - if (line.len == 0) return error.HttpHeadersInvalid; - switch (line[0]) { - ' ', '\t' => return error.HttpHeaderContinuationsUnsupported, - else => {}, - } + if (std.ascii.eqlIgnoreCase(header_name, "content-length")) { + if (res.content_length != null) return error.HttpHeadersInvalid; + res.content_length = std.fmt.parseInt(u64, header_value, 10) catch return error.InvalidContentLength; + } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) { + // Transfer-Encoding: second, first + // Transfer-Encoding: deflate, chunked + var iter = mem.splitBackwards(u8, header_value, ","); - var line_it = mem.tokenize(u8, line, ": "); - const header_name = line_it.next() orelse return error.HttpHeadersInvalid; - const header_value = line_it.rest(); - if (std.ascii.eqlIgnoreCase(header_name, "location")) { - if (headers.location != null) return error.HttpHeadersInvalid; - headers.location = header_value; - } else if (std.ascii.eqlIgnoreCase(header_name, "content-length")) { - if (headers.content_length != null) return error.HttpHeadersInvalid; - headers.content_length = std.fmt.parseInt(u64, header_value, 10) catch return error.InvalidContentLength; - } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) { - // Transfer-Encoding: second, first - // Transfer-Encoding: deflate, chunked - var iter = mem.splitBackwards(u8, header_value, ","); + if (iter.next()) |first| { + const trimmed = mem.trim(u8, first, " "); - if (iter.next()) |first| { - const trimmed = mem.trim(u8, first, " "); - - if (std.meta.stringToEnum(http.TransferEncoding, trimmed)) |te| { - if (headers.transfer_encoding != null) return error.HttpHeadersInvalid; - headers.transfer_encoding = te; - } else if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { - if (headers.transfer_compression != null) return error.HttpHeadersInvalid; - headers.transfer_compression = ce; - } else { - return error.HttpTransferEncodingUnsupported; - } - } - - if (iter.next()) |second| { - if (headers.transfer_compression != null) return error.HttpTransferEncodingUnsupported; - - const trimmed = mem.trim(u8, second, " "); - - if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { - headers.transfer_compression = ce; - } else { - return error.HttpTransferEncodingUnsupported; - } - } - - if (iter.next()) |_| return error.HttpTransferEncodingUnsupported; - } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) { - if (headers.transfer_compression != null) return error.HttpHeadersInvalid; - - const trimmed = mem.trim(u8, header_value, " "); - - if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { - headers.transfer_compression = ce; + if (std.meta.stringToEnum(http.TransferEncoding, trimmed)) |te| { + if (res.transfer_encoding != null) return error.HttpHeadersInvalid; + res.transfer_encoding = te; + } else if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { + if (res.transfer_compression != null) return error.HttpHeadersInvalid; + res.transfer_compression = ce; } else { return error.HttpTransferEncodingUnsupported; } - } else if (std.ascii.eqlIgnoreCase(header_name, "connection")) { - if (std.ascii.eqlIgnoreCase(header_value, "keep-alive")) { - headers.connection = .keep_alive; - } else if (std.ascii.eqlIgnoreCase(header_value, "close")) { - headers.connection = .close; + } + + if (iter.next()) |second| { + if (res.transfer_compression != null) return error.HttpTransferEncodingUnsupported; + + const trimmed = mem.trim(u8, second, " "); + + if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { + res.transfer_compression = ce; } else { - return error.HttpConnectionHeaderUnsupported; + return error.HttpTransferEncodingUnsupported; } - } else if (std.ascii.eqlIgnoreCase(header_name, "upgrade")) { - headers.upgrade = header_value; + } + + if (iter.next()) |_| return error.HttpTransferEncodingUnsupported; + } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) { + if (res.transfer_compression != null) return error.HttpHeadersInvalid; + + const trimmed = mem.trim(u8, header_value, " "); + + if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { + res.transfer_compression = ce; + } else { + return error.HttpTransferEncodingUnsupported; } } - - return headers; } + } - inline fn int64(array: *const [8]u8) u64 { - return @bitCast(u64, array.*); - } + inline fn int64(array: *const [8]u8) u64 { + return @bitCast(u64, array.*); + } - fn parseInt3(nnn: @Vector(3, u8)) u10 { - const zero: @Vector(3, u8) = .{ '0', '0', '0' }; - const mmm: @Vector(3, u10) = .{ 100, 10, 1 }; - return @reduce(.Add, @as(@Vector(3, u10), nnn -% zero) *% mmm); - } + fn parseInt3(nnn: @Vector(3, u8)) u10 { + const zero: @Vector(3, u8) = .{ '0', '0', '0' }; + const mmm: @Vector(3, u10) = .{ 100, 10, 1 }; + return @reduce(.Add, @as(@Vector(3, u10), nnn -% zero) *% mmm); + } - test parseInt3 { - const expectEqual = testing.expectEqual; - try expectEqual(@as(u10, 0), parseInt3("000".*)); - try expectEqual(@as(u10, 418), parseInt3("418".*)); - try expectEqual(@as(u10, 999), parseInt3("999".*)); - } - }; + test parseInt3 { + const expectEqual = testing.expectEqual; + try expectEqual(@as(u10, 0), parseInt3("000".*)); + try expectEqual(@as(u10, 418), parseInt3("418".*)); + try expectEqual(@as(u10, 999), parseInt3("999".*)); + } - headers: Headers = undefined, + version: http.Version, + status: http.Status, + reason: []const u8, + + content_length: ?u64 = null, + transfer_encoding: ?http.TransferEncoding = null, + transfer_compression: ?http.ContentEncoding = null, + + headers: http.Headers, parser: proto.HeadersParser, compression: Compression = .none, skip: bool = false, @@ -491,22 +476,14 @@ pub const Response = struct { /// /// Order of operations: request[ -> write -> finish] -> do -> read pub const Request = struct { - pub const Headers = struct { - version: http.Version = .@"HTTP/1.1", - method: http.Method = .GET, - user_agent: []const u8 = "zig (std.http)", - connection: http.Connection = .keep_alive, - transfer_encoding: RequestTransfer = .none, - - custom: []const http.CustomHeader = &[_]http.CustomHeader{}, - }; - uri: Uri, client: *Client, connection: *ConnectionPool.Node, - /// These are stored in Request so that they are available when following - /// redirects. - headers: Headers, + + method: http.Method, + version: http.Version = .@"HTTP/1.1", + headers: http.Headers, + transfer_encoding: RequestTransfer = .none, redirects_left: u32, handle_redirects: bool, @@ -526,6 +503,7 @@ pub const Request = struct { } if (req.response.parser.header_bytes_owned) { + req.response.headers.deinit(); req.response.parser.header_bytes.deinit(req.client.allocator); } @@ -540,14 +518,14 @@ pub const Request = struct { req.* = undefined; } - pub fn start(req: *Request, uri: Uri, headers: Headers) !void { + pub fn start(req: *Request, uri: Uri) !void { var buffered = std.io.bufferedWriter(req.connection.data.buffered.writer()); const w = buffered.writer(); - try w.writeAll(@tagName(headers.method)); + try w.writeAll(@tagName(req.method)); try w.writeByte(' '); - if (req.headers.method == .CONNECT) { + if (req.method == .CONNECT) { try w.writeAll(uri.host.?); try w.writeByte(':'); try w.print("{}", .{uri.port.?}); @@ -559,33 +537,62 @@ pub const Request = struct { } try w.writeByte(' '); - try w.writeAll(@tagName(headers.version)); - try w.writeAll("\r\nHost: "); - try w.writeAll(uri.host.?); - try w.writeAll("\r\nUser-Agent: "); - try w.writeAll(headers.user_agent); - if (headers.connection == .close) { - try w.writeAll("\r\nConnection: close"); - } else { - try w.writeAll("\r\nConnection: keep-alive"); - } - try w.writeAll("\r\nAccept-Encoding: gzip, deflate, zstd"); - try w.writeAll("\r\nTE: gzip, deflate"); // TODO: add trailers when someone finds a nice way to integrate them without completely invalidating all pointers to headers. + try w.writeAll(@tagName(req.version)); + try w.writeAll("\r\n"); - switch (headers.transfer_encoding) { - .chunked => try w.writeAll("\r\nTransfer-Encoding: chunked"), - .content_length => |content_length| try w.print("\r\nContent-Length: {d}", .{content_length}), - .none => {}, - } - - for (headers.custom) |header| { + if (!req.headers.contains("host")) { + try w.writeAll("Host: "); + try w.writeAll(uri.host.?); try w.writeAll("\r\n"); - try w.writeAll(header.name); - try w.writeAll(": "); - try w.writeAll(header.value); } - try w.writeAll("\r\n\r\n"); + if (!req.headers.contains("user-agent")) { + try w.writeAll("User-Agent: zig/"); + try w.writeAll(@import("builtin").zig_version_string); + try w.writeAll(" (std.http)\r\n"); + } + + if (!req.headers.contains("connection")) { + try w.writeAll("Connection: keep-alive\r\n"); + } + + if (!req.headers.contains("accept-encoding")) { + try w.writeAll("Accept-Encoding: gzip, deflate, zstd\r\n"); + } + + if (!req.headers.contains("te")) { + try w.writeAll("TE: gzip, deflate, trailers\r\n"); + } + + const has_transfer_encoding = req.headers.contains("transfer-encoding"); + const has_content_length = req.headers.contains("content-length"); + + if (!has_transfer_encoding and !has_content_length) { + switch (req.transfer_encoding) { + .chunked => try w.writeAll("Transfer-Encoding: chunked\r\n"), + .content_length => |content_length| try w.print("Content-Length: {d}\r\n", .{content_length}), + .none => {}, + } + } else { + if (has_content_length) { + const content_length = try std.fmt.parseInt(u64, req.headers.getFirstValue("content-length").?, 10); + + req.transfer_encoding = .{ .content_length = content_length }; + } else if (has_transfer_encoding) { + const transfer_encoding = req.headers.getFirstValue("content-length").?; + if (std.mem.eql(u8, transfer_encoding, "chunked")) { + req.transfer_encoding = .chunked; + } else { + return error.UnsupportedTransferEncoding; + } + } else { + req.transfer_encoding = .none; + } + } + + try w.print("{}", .{req.headers}); + + try w.writeAll("\r\n"); try buffered.flush(); } @@ -611,7 +618,7 @@ pub const Request = struct { return index; } - pub const DoError = RequestError || TransferReadError || proto.HeadersParser.CheckCompleteHeadError || Response.Headers.ParseError || Uri.ParseError || error{ TooManyHttpRedirects, HttpRedirectMissingLocation, CompressionInitializationFailed }; + pub const DoError = RequestError || TransferReadError || proto.HeadersParser.CheckCompleteHeadError || Response.ParseError || Uri.ParseError || error{ TooManyHttpRedirects, HttpRedirectMissingLocation, CompressionInitializationFailed }; /// Waits for a response from the server and parses any headers that are sent. /// This function will block until the final response is received. @@ -629,33 +636,39 @@ pub const Request = struct { if (req.response.parser.state.isContent()) break; } - req.response.headers = try Response.Headers.parse(req.response.parser.header_bytes.items); + req.response.headers = http.Headers{ .allocator = req.client.allocator, .owned = false }; + try req.response.parse(req.response.parser.header_bytes.items); - if (req.response.headers.status == .switching_protocols) { + if (req.response.status == .switching_protocols) { req.connection.data.closing = false; req.response.parser.done = true; } - if (req.headers.method == .CONNECT and req.response.headers.status == .ok) { + if (req.method == .CONNECT and req.response.status == .ok) { req.connection.data.closing = false; req.connection.data.proxied = true; req.response.parser.done = true; } - if (req.headers.connection == .keep_alive and req.response.headers.connection == .keep_alive) { + const req_connection = req.headers.getFirstValue("connection"); + const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?); + + const res_connection = req.response.headers.getFirstValue("connection"); + const res_keepalive = res_connection != null and !std.ascii.eqlIgnoreCase("close", res_connection.?); + if (req_keepalive and res_keepalive) { req.connection.data.closing = false; } else { req.connection.data.closing = true; } - if (req.response.headers.transfer_encoding) |te| { + if (req.response.transfer_encoding) |te| { switch (te) { .chunked => { req.response.parser.next_chunk_length = 0; req.response.parser.state = .chunk_head_size; }, } - } else if (req.response.headers.content_length) |cl| { + } else if (req.response.content_length) |cl| { req.response.parser.next_chunk_length = cl; if (cl == 0) req.response.parser.done = true; @@ -663,7 +676,7 @@ pub const Request = struct { req.response.parser.done = true; } - if (req.response.headers.status.class() == .redirect and req.handle_redirects) { + if (req.response.status.class() == .redirect and req.handle_redirects) { req.response.skip = true; const empty = @as([*]u8, undefined)[0..0]; @@ -671,7 +684,7 @@ pub const Request = struct { if (req.redirects_left == 0) return error.TooManyHttpRedirects; - const location = req.response.headers.location orelse + const location = req.response.headers.getFirstValue("location") orelse return error.HttpRedirectMissingLocation; const new_url = Uri.parse(location) catch try Uri.parseWithoutScheme(location); @@ -683,6 +696,8 @@ pub const Request = struct { req.arena = new_arena; const new_req = try req.client.request(resolved_url, req.headers, .{ + .method = req.method, + .version = req.version, .max_redirects = req.redirects_left - 1, .header_strategy = if (req.response.parser.header_bytes_owned) .{ .dynamic = req.response.parser.max_header_bytes, @@ -695,7 +710,7 @@ pub const Request = struct { } else { req.response.skip = false; if (!req.response.parser.done) { - if (req.response.headers.transfer_compression) |tc| switch (tc) { + if (req.response.transfer_compression) |tc| switch (tc) { .compress => return error.CompressionNotSupported, .deflate => req.response.compression = .{ .deflate = std.compress.zlib.zlibStream(req.client.allocator, req.transferReader()) catch return error.CompressionInitializationFailed, @@ -789,7 +804,7 @@ pub const Request = struct { /// Finish the body of a request. This notifies the server that you have no more data to send. pub fn finish(req: *Request) FinishError!void { - switch (req.headers.transfer_encoding) { + switch (req.transfer_encoding) { .chunked => req.connection.data.conn.writeAll("0\r\n\r\n") catch |err| { req.client.last_error = .{ .write = err }; return error.WriteFailed; @@ -908,14 +923,18 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio } } -pub const RequestError = ConnectUnproxiedError || ConnectErrorPartial || BufferedConnection.WriteError || error{ +pub const RequestError = ConnectUnproxiedError || ConnectErrorPartial || std.fmt.ParseIntError || BufferedConnection.WriteError || error{ UnsupportedUrlScheme, UriMissingHost, CertificateBundleLoadFailure, + UnsupportedTransferEncoding, }; pub const Options = struct { + method: http.Method = .GET, + version: http.Version = .@"HTTP/1.1", + handle_redirects: bool = true, max_redirects: u32 = 3, header_strategy: HeaderStrategy = .{ .dynamic = 16 * 1024 }, @@ -946,7 +965,7 @@ pub const protocol_map = std.ComptimeStringMap(Connection.Protocol, .{ /// Form and send a http request to a server. /// This function is threadsafe. -pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Options) RequestError!Request { +pub fn request(client: *Client, uri: Uri, headers: http.Headers, options: Options) RequestError!Request { const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme; const port: u16 = uri.port orelse switch (protocol) { @@ -973,9 +992,14 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Opt .client = client, .connection = conn, .headers = headers, + .method = options.method, + .version = options.version, .redirects_left = options.max_redirects, .handle_redirects = options.handle_redirects, .response = .{ + .status = undefined, + .version = undefined, + .headers = undefined, .parser = switch (options.header_strategy) { .dynamic => |max| proto.HeadersParser.initDynamic(max), .static => |buf| proto.HeadersParser.initStatic(buf), @@ -987,7 +1011,7 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Opt req.arena = std.heap.ArenaAllocator.init(client.allocator); - try req.start(uri, headers); + try req.start(uri); return req; } diff --git a/lib/std/http/Headers.zig b/lib/std/http/Headers.zig new file mode 100644 index 0000000000..81fb06a0c8 --- /dev/null +++ b/lib/std/http/Headers.zig @@ -0,0 +1,386 @@ +const std = @import("../std.zig"); + +const Allocator = std.mem.Allocator; + +const testing = std.testing; +const ascii = std.ascii; +const assert = std.debug.assert; + +pub const HeaderList = std.ArrayListUnmanaged(HeaderEntry); +pub const HeaderIndexList = std.ArrayListUnmanaged(usize); +pub const HeaderIndex = std.HashMapUnmanaged([]const u8, HeaderIndexList, CaseInsensitiveStringContext, std.hash_map.default_max_load_percentage); + +pub const CaseInsensitiveStringContext = struct { + pub fn hash(self: @This(), s: []const u8) u64 { + _ = self; + var buf: [64]u8 = undefined; + var i: u8 = 0; + + var h = std.hash.Wyhash.init(0); + while (i < s.len) : (i += 64) { + const left = @min(64, s.len - i); + const ret = ascii.lowerString(buf[0..], s[i..][0..left]); + h.update(ret); + } + + return h.final(); + } + + pub fn eql(self: @This(), a: []const u8, b: []const u8) bool { + _ = self; + return ascii.eqlIgnoreCase(a, b); + } +}; + +pub const HeaderEntry = struct { + name: []const u8, + value: []const u8, + + pub fn modify(entry: *HeaderEntry, allocator: Allocator, new_value: []const u8) !void { + if (entry.value.len <= new_value.len) { + std.mem.copy(u8, @constCast(entry.value), new_value); + } else { + allocator.free(entry.value); + + entry.value = try allocator.dupe(u8, new_value); + } + } + + fn lessThan(ctx: void, a: HeaderEntry, b: HeaderEntry) bool { + _ = ctx; + if (a.name.ptr == b.name.ptr) return false; + + return ascii.lessThanIgnoreCase(a.name, b.name); + } +}; + +pub const Headers = struct { + allocator: Allocator, + list: HeaderList = .{}, + index: HeaderIndex = .{}, + + /// When this is false, names and values will not be duplicated. + /// Use with caution. + owned: bool = true, + + pub fn init(allocator: Allocator) Headers { + return .{ .allocator = allocator }; + } + + pub fn deinit(headers: *Headers) void { + var it = headers.index.iterator(); + while (it.next()) |entry| { + entry.value_ptr.deinit(headers.allocator); + + if (headers.owned) headers.allocator.free(entry.key_ptr.*); + } + + for (headers.list.items) |entry| { + if (headers.owned) headers.allocator.free(entry.value); + } + + headers.index.deinit(headers.allocator); + headers.list.deinit(headers.allocator); + + headers.* = undefined; + } + + /// Appends a header to the list. Both name and value are copied. + pub fn append(headers: *Headers, name: []const u8, value: []const u8) !void { + const n = headers.list.items.len; + + const value_duped = if (headers.owned) try headers.allocator.dupe(u8, value) else value; + errdefer if (headers.owned) headers.allocator.free(value_duped); + + var entry = HeaderEntry{ .name = undefined, .value = value_duped }; + + if (headers.index.getEntry(name)) |kv| { + entry.name = kv.key_ptr.*; + try kv.value_ptr.append(headers.allocator, n); + } else { + const name_duped = if (headers.owned) try headers.allocator.dupe(u8, name) else name; + errdefer if (headers.owned) headers.allocator.free(name_duped); + + entry.name = name_duped; + + var new_index = try HeaderIndexList.initCapacity(headers.allocator, 1); + errdefer new_index.deinit(headers.allocator); + + new_index.appendAssumeCapacity(n); + try headers.index.put(headers.allocator, name_duped, new_index); + } + + try headers.list.append(headers.allocator, entry); + } + + pub fn contains(headers: Headers, name: []const u8) bool { + return headers.index.contains(name); + } + + pub fn delete(headers: *Headers, name: []const u8) bool { + if (headers.index.fetchRemove(name)) |kv| { + var index = kv.value; + + // iterate backwards + var i = index.items.len; + while (i > 0) { + i -= 1; + const data_index = index.items[i]; + const removed = headers.list.orderedRemove(data_index); + + assert(ascii.eqlIgnoreCase(removed.name, name)); // ensure the index hasn't been corrupted + if (headers.owned) headers.allocator.free(removed.value); + } + + if (headers.owned) headers.allocator.free(kv.key); + index.deinit(headers.allocator); + headers.rebuildIndex(); + + return true; + } else { + return false; + } + } + + /// Returns the index of the first occurrence of a header with the given name. + pub fn firstIndexOf(headers: Headers, name: []const u8) ?usize { + const index = headers.index.get(name) orelse return null; + + return index.items[0]; + } + + /// Returns a list of indices containing headers with the given name. + pub fn getIndices(headers: Headers, name: []const u8) ?[]const usize { + const index = headers.index.get(name) orelse return null; + + return index.items; + } + + /// Returns the entry of the first occurrence of a header with the given name. + pub fn getFirstEntry(headers: Headers, name: []const u8) ?HeaderEntry { + const first_index = headers.firstIndexOf(name) orelse return null; + + return headers.list.items[first_index]; + } + + /// Returns a slice containing each header with the given name. + /// The caller owns the returned slice, but NOT the values in the slice. + pub fn getEntries(headers: Headers, allocator: Allocator, name: []const u8) !?[]const HeaderEntry { + const indices = headers.getIndices(name) orelse return null; + + const buf = try allocator.alloc(HeaderEntry, indices.len); + for (indices, 0..) |idx, n| { + buf[n] = headers.list.items[idx]; + } + + return buf; + } + + /// Returns the value in the entry of the first occurrence of a header with the given name. + pub fn getFirstValue(headers: Headers, name: []const u8) ?[]const u8 { + const first_index = headers.firstIndexOf(name) orelse return null; + + return headers.list.items[first_index].value; + } + + /// Returns a slice containing the value of each header with the given name. + /// The caller owns the returned slice, but NOT the values in the slice. + pub fn getValues(headers: Headers, allocator: Allocator, name: []const u8) !?[]const []const u8 { + const indices = headers.getIndices(name) orelse return null; + + const buf = try allocator.alloc([]const u8, indices.len); + for (indices, 0..) |idx, n| { + buf[n] = headers.list.items[idx].value; + } + + return buf; + } + + fn rebuildIndex(headers: *Headers) void { + // clear out the indexes + var it = headers.index.iterator(); + while (it.next()) |entry| { + entry.value_ptr.shrinkRetainingCapacity(0); + } + + // fill up indexes again; we know capacity is fine from before + for (headers.list.items, 0..) |entry, i| { + headers.index.getEntry(entry.name).?.value_ptr.appendAssumeCapacity(i); + } + } + + /// Sorts the headers in lexicographical order. + pub fn sort(headers: *Headers) void { + std.sort.sort(HeaderEntry, headers.list.items, {}, HeaderEntry.lessThan); + headers.rebuildIndex(); + } + + /// Writes the headers to the given stream. + pub fn format( + headers: Headers, + comptime fmt: []const u8, + options: std.fmt.FormatOptions, + out_stream: anytype, + ) !void { + _ = fmt; + _ = options; + + for (headers.list.items) |entry| { + if (entry.value.len == 0) continue; + + try out_stream.writeAll(entry.name); + try out_stream.writeAll(": "); + try out_stream.writeAll(entry.value); + try out_stream.writeAll("\r\n"); + } + } + + /// Writes all of the headers with the given name to the given stream, separated by commas. + /// + /// This is useful for headers like `Set-Cookie` which can have multiple values. RFC 9110, Section 5.2 + pub fn formatCommaSeparated( + headers: Headers, + name: []const u8, + out_stream: anytype, + ) !void { + const indices = headers.getIndices(name) orelse return; + + try out_stream.writeAll(name); + try out_stream.writeAll(": "); + + for (indices, 0..) |idx, n| { + if (n != 0) try out_stream.writeAll(", "); + try out_stream.writeAll(headers.list.items[idx].value); + } + + try out_stream.writeAll("\r\n"); + } +}; + +test "Headers.append" { + var h = Headers{ .allocator = std.testing.allocator }; + defer h.deinit(); + + try h.append("foo", "bar"); + try h.append("hello", "world"); + + try testing.expect(h.contains("Foo")); + try testing.expect(!h.contains("Bar")); +} + +test "Headers.delete" { + var h = Headers{ .allocator = std.testing.allocator }; + defer h.deinit(); + + try h.append("foo", "bar"); + try h.append("hello", "world"); + + try testing.expect(h.contains("Foo")); + + _ = h.delete("Foo"); + + try testing.expect(!h.contains("foo")); +} + +test "Headers consistency" { + var h = Headers{ .allocator = std.testing.allocator }; + defer h.deinit(); + + try h.append("foo", "bar"); + try h.append("hello", "world"); + _ = h.delete("Foo"); + + try h.append("foo", "bar"); + try h.append("bar", "world"); + try h.append("foo", "baz"); + try h.append("baz", "hello"); + + try testing.expectEqual(@as(?usize, 0), h.firstIndexOf("hello")); + try testing.expectEqual(@as(?usize, 1), h.firstIndexOf("foo")); + try testing.expectEqual(@as(?usize, 2), h.firstIndexOf("bar")); + try testing.expectEqual(@as(?usize, 4), h.firstIndexOf("baz")); + try testing.expectEqual(@as(?usize, null), h.firstIndexOf("pog")); + + try testing.expectEqualSlices(usize, &[_]usize{0}, h.getIndices("hello").?); + try testing.expectEqualSlices(usize, &[_]usize{ 1, 3 }, h.getIndices("foo").?); + try testing.expectEqualSlices(usize, &[_]usize{2}, h.getIndices("bar").?); + try testing.expectEqualSlices(usize, &[_]usize{4}, h.getIndices("baz").?); + try testing.expectEqual(@as(?[]const usize, null), h.getIndices("pog")); + + try testing.expectEqualStrings("world", h.getFirstEntry("hello").?.value); + try testing.expectEqualStrings("bar", h.getFirstEntry("foo").?.value); + try testing.expectEqualStrings("world", h.getFirstEntry("bar").?.value); + try testing.expectEqualStrings("hello", h.getFirstEntry("baz").?.value); + + const hello_entries = (try h.getEntries(testing.allocator, "hello")).?; + defer testing.allocator.free(hello_entries); + try testing.expectEqualDeep(@as([]const HeaderEntry, &[_]HeaderEntry{ + .{ .name = "hello", .value = "world" }, + }), hello_entries); + + const foo_entries = (try h.getEntries(testing.allocator, "foo")).?; + defer testing.allocator.free(foo_entries); + try testing.expectEqualDeep(@as([]const HeaderEntry, &[_]HeaderEntry{ + .{ .name = "foo", .value = "bar" }, + .{ .name = "foo", .value = "baz" }, + }), foo_entries); + + const bar_entries = (try h.getEntries(testing.allocator, "bar")).?; + defer testing.allocator.free(bar_entries); + try testing.expectEqualDeep(@as([]const HeaderEntry, &[_]HeaderEntry{ + .{ .name = "bar", .value = "world" }, + }), bar_entries); + + const baz_entries = (try h.getEntries(testing.allocator, "baz")).?; + defer testing.allocator.free(baz_entries); + try testing.expectEqualDeep(@as([]const HeaderEntry, &[_]HeaderEntry{ + .{ .name = "baz", .value = "hello" }, + }), baz_entries); + + const pog_entries = (try h.getEntries(testing.allocator, "pog")); + try testing.expectEqual(@as(?[]const HeaderEntry, null), pog_entries); + + try testing.expectEqualStrings("world", h.getFirstValue("hello").?); + try testing.expectEqualStrings("bar", h.getFirstValue("foo").?); + try testing.expectEqualStrings("world", h.getFirstValue("bar").?); + try testing.expectEqualStrings("hello", h.getFirstValue("baz").?); + try testing.expectEqual(@as(?[]const u8, null), h.getFirstValue("pog")); + + const hello_values = (try h.getValues(testing.allocator, "hello")).?; + defer testing.allocator.free(hello_values); + try testing.expectEqualDeep(@as([]const []const u8, &[_][]const u8{"world"}), hello_values); + + const foo_values = (try h.getValues(testing.allocator, "foo")).?; + defer testing.allocator.free(foo_values); + try testing.expectEqualDeep(@as([]const []const u8, &[_][]const u8{ "bar", "baz" }), foo_values); + + const bar_values = (try h.getValues(testing.allocator, "bar")).?; + defer testing.allocator.free(bar_values); + try testing.expectEqualDeep(@as([]const []const u8, &[_][]const u8{"world"}), bar_values); + + const baz_values = (try h.getValues(testing.allocator, "baz")).?; + defer testing.allocator.free(baz_values); + try testing.expectEqualDeep(@as([]const []const u8, &[_][]const u8{"hello"}), baz_values); + + const pog_values = (try h.getValues(testing.allocator, "pog")); + try testing.expectEqual(@as(?[]const []const u8, null), pog_values); + + h.sort(); + + try testing.expectEqualSlices(usize, &[_]usize{0}, h.getIndices("bar").?); + try testing.expectEqualSlices(usize, &[_]usize{1}, h.getIndices("baz").?); + try testing.expectEqualSlices(usize, &[_]usize{ 2, 3 }, h.getIndices("foo").?); + try testing.expectEqualSlices(usize, &[_]usize{4}, h.getIndices("hello").?); + + const formatted_values = try std.fmt.allocPrint(testing.allocator, "{}", .{h}); + defer testing.allocator.free(formatted_values); + + try testing.expectEqualStrings("bar: world\r\nbaz: hello\r\nfoo: bar\r\nfoo: baz\r\nhello: world\r\n", formatted_values); + + var buf: [128]u8 = undefined; + var fbs = std.io.fixedBufferStream(&buf); + const writer = fbs.writer(); + + try h.formatCommaSeparated("foo", writer); + try testing.expectEqualStrings("foo: bar, baz\r\n", fbs.getWritten()); +} diff --git a/lib/std/http/Server.zig b/lib/std/http/Server.zig index 1ecb8fbd69..acf6f3c22d 100644 --- a/lib/std/http/Server.zig +++ b/lib/std/http/Server.zig @@ -157,134 +157,120 @@ pub const BufferedConnection = struct { /// A HTTP request originating from a client. pub const Request = struct { - pub const Headers = struct { - method: http.Method, - target: []const u8, - version: http.Version, - content_length: ?u64 = null, - transfer_encoding: ?http.TransferEncoding = null, - transfer_compression: ?http.ContentEncoding = null, - connection: http.Connection = .close, - host: ?[]const u8 = null, + pub const ParseError = Allocator.Error || error{ + ShortHttpStatusLine, + BadHttpVersion, + UnknownHttpMethod, + HttpHeadersInvalid, + HttpHeaderContinuationsUnsupported, + HttpTransferEncodingUnsupported, + HttpConnectionHeaderUnsupported, + InvalidCharacter, + }; - pub const ParseError = error{ - ShortHttpStatusLine, - BadHttpVersion, - UnknownHttpMethod, - HttpHeadersInvalid, - HttpHeaderContinuationsUnsupported, - HttpTransferEncodingUnsupported, - HttpConnectionHeaderUnsupported, - InvalidCharacter, + pub fn parse(req: *Request, bytes: []const u8) !void { + var it = mem.tokenize(u8, bytes[0 .. bytes.len - 4], "\r\n"); + + const first_line = it.next() orelse return error.HttpHeadersInvalid; + if (first_line.len < 10) + return error.ShortHttpStatusLine; + + const method_end = mem.indexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid; + const method_str = first_line[0..method_end]; + const method = std.meta.stringToEnum(http.Method, method_str) orelse return error.UnknownHttpMethod; + + const version_start = mem.lastIndexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid; + if (version_start == method_end) return error.HttpHeadersInvalid; + + const version_str = first_line[version_start + 1 ..]; + if (version_str.len != 8) return error.HttpHeadersInvalid; + const version: http.Version = switch (int64(version_str[0..8])) { + int64("HTTP/1.0") => .@"HTTP/1.0", + int64("HTTP/1.1") => .@"HTTP/1.1", + else => return error.BadHttpVersion, }; - pub fn parse(bytes: []const u8) !Headers { - var it = mem.tokenize(u8, bytes[0 .. bytes.len - 4], "\r\n"); + const target = first_line[method_end + 1 .. version_start]; - const first_line = it.next() orelse return error.HttpHeadersInvalid; - if (first_line.len < 10) - return error.ShortHttpStatusLine; + req.method = method; + req.target = target; + req.version = version; - const method_end = mem.indexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid; - const method_str = first_line[0..method_end]; - const method = std.meta.stringToEnum(http.Method, method_str) orelse return error.UnknownHttpMethod; + while (it.next()) |line| { + if (line.len == 0) return error.HttpHeadersInvalid; + switch (line[0]) { + ' ', '\t' => return error.HttpHeaderContinuationsUnsupported, + else => {}, + } - const version_start = mem.lastIndexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid; - if (version_start == method_end) return error.HttpHeadersInvalid; + var line_it = mem.tokenize(u8, line, ": "); + const header_name = line_it.next() orelse return error.HttpHeadersInvalid; + const header_value = line_it.rest(); - const version_str = first_line[version_start + 1 ..]; - if (version_str.len != 8) return error.HttpHeadersInvalid; - const version: http.Version = switch (int64(version_str[0..8])) { - int64("HTTP/1.0") => .@"HTTP/1.0", - int64("HTTP/1.1") => .@"HTTP/1.1", - else => return error.BadHttpVersion, - }; + try req.headers.append(header_name, header_value); - const target = first_line[method_end + 1 .. version_start]; + if (std.ascii.eqlIgnoreCase(header_name, "content-length")) { + if (req.content_length != null) return error.HttpHeadersInvalid; + req.content_length = try std.fmt.parseInt(u64, header_value, 10); + } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) { + // Transfer-Encoding: second, first + // Transfer-Encoding: deflate, chunked + var iter = mem.splitBackwards(u8, header_value, ","); - var headers: Headers = .{ - .method = method, - .target = target, - .version = version, - }; + if (iter.next()) |first| { + const trimmed = mem.trim(u8, first, " "); - while (it.next()) |line| { - if (line.len == 0) return error.HttpHeadersInvalid; - switch (line[0]) { - ' ', '\t' => return error.HttpHeaderContinuationsUnsupported, - else => {}, - } - - var line_it = mem.tokenize(u8, line, ": "); - const header_name = line_it.next() orelse return error.HttpHeadersInvalid; - const header_value = line_it.rest(); - if (std.ascii.eqlIgnoreCase(header_name, "content-length")) { - if (headers.content_length != null) return error.HttpHeadersInvalid; - headers.content_length = try std.fmt.parseInt(u64, header_value, 10); - } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) { - // Transfer-Encoding: second, first - // Transfer-Encoding: deflate, chunked - var iter = mem.splitBackwards(u8, header_value, ","); - - if (iter.next()) |first| { - const trimmed = mem.trim(u8, first, " "); - - if (std.meta.stringToEnum(http.TransferEncoding, trimmed)) |te| { - if (headers.transfer_encoding != null) return error.HttpHeadersInvalid; - headers.transfer_encoding = te; - } else if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { - if (headers.transfer_compression != null) return error.HttpHeadersInvalid; - headers.transfer_compression = ce; - } else { - return error.HttpTransferEncodingUnsupported; - } - } - - if (iter.next()) |second| { - if (headers.transfer_compression != null) return error.HttpTransferEncodingUnsupported; - - const trimmed = mem.trim(u8, second, " "); - - if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { - headers.transfer_compression = ce; - } else { - return error.HttpTransferEncodingUnsupported; - } - } - - if (iter.next()) |_| return error.HttpTransferEncodingUnsupported; - } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) { - if (headers.transfer_compression != null) return error.HttpHeadersInvalid; - - const trimmed = mem.trim(u8, header_value, " "); - - if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { - headers.transfer_compression = ce; + if (std.meta.stringToEnum(http.TransferEncoding, trimmed)) |te| { + if (req.transfer_encoding != null) return error.HttpHeadersInvalid; + req.transfer_encoding = te; + } else if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { + if (req.transfer_compression != null) return error.HttpHeadersInvalid; + req.transfer_compression = ce; } else { return error.HttpTransferEncodingUnsupported; } - } else if (std.ascii.eqlIgnoreCase(header_name, "connection")) { - if (std.ascii.eqlIgnoreCase(header_value, "keep-alive")) { - headers.connection = .keep_alive; - } else if (std.ascii.eqlIgnoreCase(header_value, "close")) { - headers.connection = .close; + } + + if (iter.next()) |second| { + if (req.transfer_compression != null) return error.HttpTransferEncodingUnsupported; + + const trimmed = mem.trim(u8, second, " "); + + if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { + req.transfer_compression = ce; } else { - return error.HttpConnectionHeaderUnsupported; + return error.HttpTransferEncodingUnsupported; } - } else if (std.ascii.eqlIgnoreCase(header_name, "host")) { - headers.host = header_value; + } + + if (iter.next()) |_| return error.HttpTransferEncodingUnsupported; + } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) { + if (req.transfer_compression != null) return error.HttpHeadersInvalid; + + const trimmed = mem.trim(u8, header_value, " "); + + if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { + req.transfer_compression = ce; + } else { + return error.HttpTransferEncodingUnsupported; } } - - return headers; } + } - inline fn int64(array: *const [8]u8) u64 { - return @bitCast(u64, array.*); - } - }; + inline fn int64(array: *const [8]u8) u64 { + return @bitCast(u64, array.*); + } - headers: Headers = undefined, + method: http.Method, + target: []const u8, + version: http.Version, + + content_length: ?u64 = null, + transfer_encoding: ?http.TransferEncoding = null, + transfer_compression: ?http.ContentEncoding = null, + + headers: http.Headers = undefined, parser: proto.HeadersParser, compression: Compression = .none, }; @@ -295,23 +281,17 @@ pub const Request = struct { /// Order of operations: accept -> wait -> do [ -> write -> finish][ -> reset /] /// \ -> read / pub const Response = struct { - pub const Headers = struct { - version: http.Version = .@"HTTP/1.1", - status: http.Status = .ok, - reason: ?[]const u8 = null, + version: http.Version = .@"HTTP/1.1", + status: http.Status = .ok, + reason: ?[]const u8 = null, - server: ?[]const u8 = "zig (std.http)", - connection: http.Connection = .keep_alive, - transfer_encoding: RequestTransfer = .none, - - custom: []const http.CustomHeader = &[_]http.CustomHeader{}, - }; + transfer_encoding: ResponseTransfer = .none, server: *Server, address: net.Address, connection: BufferedConnection, - headers: Headers = .{}, + headers: http.Headers, request: Request, /// Reset this response to its initial state. This must be called before handling a second request on the same connection. @@ -346,41 +326,54 @@ pub const Response = struct { var buffered = std.io.bufferedWriter(res.connection.writer()); const w = buffered.writer(); - try w.writeAll(@tagName(res.headers.version)); + try w.writeAll(@tagName(res.version)); try w.writeByte(' '); - try w.print("{d}", .{@enumToInt(res.headers.status)}); + try w.print("{d}", .{@enumToInt(res.status)}); try w.writeByte(' '); - if (res.headers.reason) |reason| { + if (res.reason) |reason| { try w.writeAll(reason); - } else if (res.headers.status.phrase()) |phrase| { + } else if (res.status.phrase()) |phrase| { try w.writeAll(phrase); } + try w.writeAll("\r\n"); - if (res.headers.server) |server| { - try w.writeAll("\r\nServer: "); - try w.writeAll(server); + if (!res.headers.contains("server")) { + try w.writeAll("Server: zig (std.http)\r\n"); } - if (res.headers.connection == .close) { - try w.writeAll("\r\nConnection: close"); + if (!res.headers.contains("connection")) { + try w.writeAll("Connection: keep-alive\r\n"); + } + + const has_transfer_encoding = res.headers.contains("transfer-encoding"); + const has_content_length = res.headers.contains("content-length"); + + if (!has_transfer_encoding and !has_content_length) { + switch (res.transfer_encoding) { + .chunked => try w.writeAll("Transfer-Encoding: chunked\r\n"), + .content_length => |content_length| try w.print("Content-Length: {d}\r\n", .{content_length}), + .none => {}, + } } else { - try w.writeAll("\r\nConnection: keep-alive"); + if (has_content_length) { + const content_length = try std.fmt.parseInt(u64, res.headers.getFirstValue("content-length").?, 10); + + res.transfer_encoding = .{ .content_length = content_length }; + } else if (has_transfer_encoding) { + const transfer_encoding = res.headers.getFirstValue("content-length").?; + if (std.mem.eql(u8, transfer_encoding, "chunked")) { + res.transfer_encoding = .chunked; + } else { + return error.UnsupportedTransferEncoding; + } + } else { + res.transfer_encoding = .none; + } } - switch (res.headers.transfer_encoding) { - .chunked => try w.writeAll("\r\nTransfer-Encoding: chunked"), - .content_length => |content_length| try w.print("\r\nContent-Length: {d}", .{content_length}), - .none => {}, - } + try w.print("{}", .{res.headers}); - for (res.headers.custom) |header| { - try w.writeAll("\r\n"); - try w.writeAll(header.name); - try w.writeAll(": "); - try w.writeAll(header.value); - } - - try w.writeAll("\r\n\r\n"); + try w.writeAll("\r\n"); try buffered.flush(); } @@ -419,22 +412,28 @@ pub const Response = struct { if (res.request.parser.state.isContent()) break; } - res.request.headers = try Request.Headers.parse(res.request.parser.header_bytes.items); + res.request.headers = .{ .allocator = res.server.allocator, .owned = true }; + try res.request.parse(res.request.parser.header_bytes.items); - if (res.headers.connection == .keep_alive and res.request.headers.connection == .keep_alive) { + const res_connection = res.headers.getFirstValue("connection"); + const res_keepalive = res_connection != null and !std.ascii.eqlIgnoreCase("close", res_connection.?); + + const req_connection = res.request.headers.getFirstValue("connection"); + const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?); + if (res_keepalive and req_keepalive) { res.connection.conn.closing = false; } else { res.connection.conn.closing = true; } - if (res.request.headers.transfer_encoding) |te| { + if (res.request.transfer_encoding) |te| { switch (te) { .chunked => { res.request.parser.next_chunk_length = 0; res.request.parser.state = .chunk_head_size; }, } - } else if (res.request.headers.content_length) |cl| { + } else if (res.request.content_length) |cl| { res.request.parser.next_chunk_length = cl; if (cl == 0) res.request.parser.done = true; @@ -443,7 +442,7 @@ pub const Response = struct { } if (!res.request.parser.done) { - if (res.request.headers.transfer_compression) |tc| switch (tc) { + if (res.request.transfer_compression) |tc| switch (tc) { .compress => return error.CompressionNotSupported, .deflate => res.request.compression = .{ .deflate = try std.compress.zlib.zlibStream(res.server.allocator, res.transferReader()), @@ -495,7 +494,7 @@ pub const Response = struct { /// Write `bytes` to the server. The `transfer_encoding` request header determines how data will be sent. pub fn write(res: *Response, bytes: []const u8) WriteError!usize { - switch (res.headers.transfer_encoding) { + switch (res.transfer_encoding) { .chunked => { try res.connection.writer().print("{x}\r\n", .{bytes.len}); try res.connection.writeAll(bytes); @@ -525,7 +524,7 @@ pub const Response = struct { }; /// The mode of transport for responses. -pub const RequestTransfer = union(enum) { +pub const ResponseTransfer = union(enum) { content_length: u64, chunked: void, none: void, @@ -588,7 +587,11 @@ pub fn accept(server: *Server, options: HeaderStrategy) AcceptError!*Response { .stream = in.stream, .protocol = .plain, } }, + .headers = .{ .allocator = server.allocator }, .request = .{ + .version = undefined, + .method = undefined, + .target = undefined, .parser = switch (options) { .dynamic => |max| proto.HeadersParser.initDynamic(max), .static => |buf| proto.HeadersParser.initStatic(buf), diff --git a/src/Package.zig b/src/Package.zig index dba00c2c08..f471e2d606 100644 --- a/src/Package.zig +++ b/src/Package.zig @@ -479,7 +479,10 @@ fn fetchAndUnpack( }; defer tmp_directory.closeAndFree(gpa); - var req = try http_client.request(uri, .{}, .{}); + var h = std.http.Headers{ .allocator = gpa }; + defer h.deinit(); + + var req = try http_client.request(uri, h, .{ .method = .GET }); defer req.deinit(); try req.do();