struct Request [src]

Fields

uri: UriThis field is provided so that clients can observe redirected URIs. Its backing memory is externally provided by API users when creating a request, and then again provided externally via redirect_buffer to receiveHead.
client: *Client
connection: ?*ConnectionThis is null when the connection is released.
reader: http.Reader
keep_alive: bool
method: http.Method
version: http.Version = .@"HTTP/1.1"
transfer_encoding: TransferEncoding
redirect_behavior: RedirectBehavior
accept_encoding: @TypeOf(default_accept_encoding) = default_accept_encoding
handle_continue: boolWhether the request should handle a 100-continue response before sending the request body.
headers: HeadersStandard headers that have default, but overridable, behavior.
response_content_length: ?u64 = nullPopulated in receiveHead; used in deinit to determine whether to discard the body to reuse the connection.
response_transfer_encoding: http.TransferEncoding = .nonePopulated in receiveHead; used in deinit to determine whether to discard the body to reuse the connection.
extra_headers: []const http.HeaderThese headers are kept including when following a redirect to a different domain. Externally-owned; must outlive the Request.
privileged_headers: []const http.HeaderThese headers are stripped when following a redirect to a different domain. Externally-owned; must outlive the Request.

Members

Source

pub const Request = struct { /// This field is provided so that clients can observe redirected URIs. /// /// Its backing memory is externally provided by API users when creating a /// request, and then again provided externally via `redirect_buffer` to /// `receiveHead`. uri: Uri, client: *Client, /// This is null when the connection is released. connection: ?*Connection, reader: http.Reader, keep_alive: bool, method: http.Method, version: http.Version = .@"HTTP/1.1", transfer_encoding: TransferEncoding, redirect_behavior: RedirectBehavior, accept_encoding: @TypeOf(default_accept_encoding) = default_accept_encoding, /// Whether the request should handle a 100-continue response before sending the request body. handle_continue: bool, /// Standard headers that have default, but overridable, behavior. headers: Headers, /// Populated in `receiveHead`; used in `deinit` to determine whether to /// discard the body to reuse the connection. response_content_length: ?u64 = null, /// Populated in `receiveHead`; used in `deinit` to determine whether to /// discard the body to reuse the connection. response_transfer_encoding: http.TransferEncoding = .none, /// These headers are kept including when following a redirect to a /// different domain. /// Externally-owned; must outlive the Request. extra_headers: []const http.Header, /// These headers are stripped when following a redirect to a different /// domain. /// Externally-owned; must outlive the Request. privileged_headers: []const http.Header, pub const default_accept_encoding: [@typeInfo(http.ContentEncoding).@"enum".fields.len]bool = b: { var result: [@typeInfo(http.ContentEncoding).@"enum".fields.len]bool = @splat(false); result[@intFromEnum(http.ContentEncoding.gzip)] = true; result[@intFromEnum(http.ContentEncoding.deflate)] = true; result[@intFromEnum(http.ContentEncoding.identity)] = true; break :b result; }; pub const TransferEncoding = union(enum) { content_length: u64, chunked: void, none: void, }; pub const Headers = struct { host: Value = .default, authorization: Value = .default, user_agent: Value = .default, connection: Value = .default, accept_encoding: Value = .default, content_type: Value = .default, pub const Value = union(enum) { default, omit, override: []const u8, }; }; /// Any value other than `not_allowed` or `unhandled` means that integer represents /// how many remaining redirects are allowed. pub const RedirectBehavior = enum(u16) { /// The next redirect will cause an error. not_allowed = 0, /// Redirects are passed to the client to analyze the redirect response /// directly. unhandled = std.math.maxInt(u16), _, pub fn init(n: u16) RedirectBehavior { assert(n != std.math.maxInt(u16)); return @enumFromInt(n); } pub fn subtractOne(rb: *RedirectBehavior) void { switch (rb.*) { .not_allowed => unreachable, .unhandled => unreachable, _ => rb.* = @enumFromInt(@intFromEnum(rb.*) - 1), } } pub fn remaining(rb: RedirectBehavior) u16 { assert(rb != .unhandled); return @intFromEnum(rb); } }; /// Returns the request's `Connection` back to the pool of the `Client`. pub fn deinit(r: *Request) void { if (r.connection) |connection| { connection.closing = connection.closing or switch (r.reader.state) { .ready => false, .received_head => c: { if (r.method.requestHasBody()) break :c true; if (!r.method.responseHasBody()) break :c false; const reader = r.reader.bodyReader(&.{}, r.response_transfer_encoding, r.response_content_length); _ = reader.discardRemaining() catch |err| switch (err) { error.ReadFailed => break :c true, }; break :c r.reader.state != .ready; }, else => true, }; r.client.connection_pool.release(connection); } r.* = undefined; } /// Sends and flushes a complete request as only HTTP head, no body. pub fn sendBodiless(r: *Request) Writer.Error!void { try sendBodilessUnflushed(r); try r.connection.?.flush(); } /// Sends but does not flush a complete request as only HTTP head, no body. pub fn sendBodilessUnflushed(r: *Request) Writer.Error!void { assert(r.transfer_encoding == .none); assert(!r.method.requestHasBody()); try sendHead(r); } /// Transfers the HTTP head over the connection and flushes. /// /// See also: /// * `sendBodyUnflushed` pub fn sendBody(r: *Request, buffer: []u8) Writer.Error!http.BodyWriter { const result = try sendBodyUnflushed(r, buffer); try r.connection.?.flush(); return result; } /// Transfers the HTTP head and body over the connection and flushes. pub fn sendBodyComplete(r: *Request, body: []u8) Writer.Error!void { r.transfer_encoding = .{ .content_length = body.len }; var bw = try sendBodyUnflushed(r, body); bw.writer.end = body.len; try bw.end(); try r.connection.?.flush(); } /// Transfers the HTTP head over the connection, which is not flushed until /// `BodyWriter.flush` or `BodyWriter.end` is called. /// /// See also: /// * `sendBody` pub fn sendBodyUnflushed(r: *Request, buffer: []u8) Writer.Error!http.BodyWriter { assert(r.method.requestHasBody()); try sendHead(r); const http_protocol_output = r.connection.?.writer(); return switch (r.transfer_encoding) { .chunked => .{ .http_protocol_output = http_protocol_output, .state = .init_chunked, .writer = .{ .buffer = buffer, .vtable = &.{ .drain = http.BodyWriter.chunkedDrain, .sendFile = http.BodyWriter.chunkedSendFile, }, }, }, .content_length => |len| .{ .http_protocol_output = http_protocol_output, .state = .{ .content_length = len }, .writer = .{ .buffer = buffer, .vtable = &.{ .drain = http.BodyWriter.contentLengthDrain, .sendFile = http.BodyWriter.contentLengthSendFile, }, }, }, .none => .{ .http_protocol_output = http_protocol_output, .state = .none, .writer = .{ .buffer = buffer, .vtable = &.{ .drain = http.BodyWriter.noneDrain, .sendFile = http.BodyWriter.noneSendFile, }, }, }, }; } /// Sends HTTP headers without flushing. fn sendHead(r: *Request) Writer.Error!void { const uri = r.uri; const connection = r.connection.?; const w = connection.writer(); try w.writeAll(@tagName(r.method)); try w.writeByte(' '); if (r.method == .CONNECT) { try uri.writeToStream(w, .{ .authority = true }); } else { try uri.writeToStream(w, .{ .scheme = connection.proxied, .authentication = connection.proxied, .authority = connection.proxied, .path = true, .query = true, }); } try w.writeByte(' '); try w.writeAll(@tagName(r.version)); try w.writeAll("\r\n"); if (try emitOverridableHeader("host: ", r.headers.host, w)) { try w.writeAll("host: "); try uri.writeToStream(w, .{ .authority = true }); try w.writeAll("\r\n"); } if (try emitOverridableHeader("authorization: ", r.headers.authorization, w)) { if (uri.user != null or uri.password != null) { try w.writeAll("authorization: "); try basic_authorization.write(uri, w); try w.writeAll("\r\n"); } } if (try emitOverridableHeader("user-agent: ", r.headers.user_agent, w)) { try w.writeAll("user-agent: zig/"); try w.writeAll(builtin.zig_version_string); try w.writeAll(" (std.http)\r\n"); } if (try emitOverridableHeader("connection: ", r.headers.connection, w)) { if (r.keep_alive) { try w.writeAll("connection: keep-alive\r\n"); } else { try w.writeAll("connection: close\r\n"); } } if (try emitOverridableHeader("accept-encoding: ", r.headers.accept_encoding, w)) { try w.writeAll("accept-encoding: "); for (r.accept_encoding, 0..) |enabled, i| { if (!enabled) continue; const tag: http.ContentEncoding = @enumFromInt(i); if (tag == .identity) continue; const tag_name = @tagName(tag); try w.ensureUnusedCapacity(tag_name.len + 2); try w.writeAll(tag_name); try w.writeAll(", "); } w.undo(2); try w.writeAll("\r\n"); } switch (r.transfer_encoding) { .chunked => try w.writeAll("transfer-encoding: chunked\r\n"), .content_length => |len| try w.print("content-length: {d}\r\n", .{len}), .none => {}, } if (try emitOverridableHeader("content-type: ", r.headers.content_type, w)) { // The default is to omit content-type if not provided because // "application/octet-stream" is redundant. } for (r.extra_headers) |header| { assert(header.name.len != 0); try w.writeAll(header.name); try w.writeAll(": "); try w.writeAll(header.value); try w.writeAll("\r\n"); } if (connection.proxied) proxy: { const proxy = switch (connection.protocol) { .plain => r.client.http_proxy, .tls => r.client.https_proxy, } orelse break :proxy; const authorization = proxy.authorization orelse break :proxy; try w.writeAll("proxy-authorization: "); try w.writeAll(authorization); try w.writeAll("\r\n"); } try w.writeAll("\r\n"); } pub const ReceiveHeadError = http.Reader.HeadError || ConnectError || error{ /// Server sent headers that did not conform to the HTTP protocol. /// /// To find out more detailed diagnostics, `http.Reader.head_buffer` can be /// passed directly to `Request.Head.parse`. HttpHeadersInvalid, TooManyHttpRedirects, /// This can be avoided by calling `receiveHead` before sending the /// request body. RedirectRequiresResend, HttpRedirectLocationMissing, HttpRedirectLocationOversize, HttpRedirectLocationInvalid, HttpContentEncodingUnsupported, HttpChunkInvalid, HttpChunkTruncated, HttpHeadersOversize, UnsupportedUriScheme, /// Sending the request failed. Error code can be found on the /// `Connection` object. WriteFailed, }; /// If handling redirects and the request has no payload, then this /// function will automatically follow redirects. /// /// If a request payload is present, then this function will error with /// `error.RedirectRequiresResend`. /// /// This function takes an auxiliary buffer to store the arbitrarily large /// URI which may need to be merged with the previous URI, and that data /// needs to survive across different connections, which is where the input /// buffer lives. /// /// `redirect_buffer` must outlive accesses to `Request.uri`. If this /// buffer capacity would be exceeded, `error.HttpRedirectLocationOversize` /// is returned instead. This buffer may be empty if no redirects are to be /// handled. /// /// If this fails with `error.ReadFailed` then the `Connection.getReadError` /// method of `r.connection` can be used to get more detailed information. pub fn receiveHead(r: *Request, redirect_buffer: []u8) ReceiveHeadError!Response { var aux_buf = redirect_buffer; while (true) { const head_buffer = try r.reader.receiveHead(); const response: Response = .{ .request = r, .head = Response.Head.parse(head_buffer) catch return error.HttpHeadersInvalid, }; const head = &response.head; if (head.status == .@"continue") { if (r.handle_continue) continue; r.response_transfer_encoding = head.transfer_encoding; r.response_content_length = head.content_length; return response; // we're not handling the 100-continue } // This while loop is for handling redirects, which means the request's // connection may be different than the previous iteration. However, it // is still guaranteed to be non-null with each iteration of this loop. const connection = r.connection.?; if (r.method == .CONNECT and head.status.class() == .success) { // This connection is no longer doing HTTP. connection.closing = false; r.response_transfer_encoding = head.transfer_encoding; r.response_content_length = head.content_length; return response; } connection.closing = !head.keep_alive or !r.keep_alive; // Any response to a HEAD request and any response with a 1xx // (Informational), 204 (No Content), or 304 (Not Modified) status // code is always terminated by the first empty line after the // header fields, regardless of the header fields present in the // message. if (r.method == .HEAD or head.status.class() == .informational or head.status == .no_content or head.status == .not_modified) { r.response_transfer_encoding = head.transfer_encoding; r.response_content_length = head.content_length; return response; } if (head.status.class() == .redirect and r.redirect_behavior != .unhandled) { if (r.redirect_behavior == .not_allowed) { // Connection can still be reused by skipping the body. const reader = r.reader.bodyReader(&.{}, head.transfer_encoding, head.content_length); _ = reader.discardRemaining() catch |err| switch (err) { error.ReadFailed => connection.closing = true, }; return error.TooManyHttpRedirects; } try r.redirect(head, &aux_buf); try r.sendBodiless(); continue; } if (!r.accept_encoding[@intFromEnum(head.content_encoding)]) return error.HttpContentEncodingUnsupported; r.response_transfer_encoding = head.transfer_encoding; r.response_content_length = head.content_length; return response; } } /// This function takes an auxiliary buffer to store the arbitrarily large /// URI which may need to be merged with the previous URI, and that data /// needs to survive across different connections, which is where the input /// buffer lives. /// /// `aux_buf` must outlive accesses to `Request.uri`. fn redirect(r: *Request, head: *const Response.Head, aux_buf: *[]u8) !void { const new_location = head.location orelse return error.HttpRedirectLocationMissing; if (new_location.len > aux_buf.*.len) return error.HttpRedirectLocationOversize; const location = aux_buf.*[0..new_location.len]; @memcpy(location, new_location); { // Skip the body of the redirect response to leave the connection in // the correct state. This causes `new_location` to be invalidated. const reader = r.reader.bodyReader(&.{}, head.transfer_encoding, head.content_length); _ = reader.discardRemaining() catch |err| switch (err) { error.ReadFailed => return r.reader.body_err.?, }; } const new_uri = r.uri.resolveInPlace(location.len, aux_buf) catch |err| switch (err) { error.UnexpectedCharacter => return error.HttpRedirectLocationInvalid, error.InvalidFormat => return error.HttpRedirectLocationInvalid, error.InvalidPort => return error.HttpRedirectLocationInvalid, error.NoSpaceLeft => return error.HttpRedirectLocationOversize, }; const protocol = Protocol.fromUri(new_uri) orelse return error.UnsupportedUriScheme; const old_connection = r.connection.?; const old_host = old_connection.host(); var new_host_name_buffer: [Uri.host_name_max]u8 = undefined; const new_host = try new_uri.getHost(&new_host_name_buffer); const keep_privileged_headers = std.ascii.eqlIgnoreCase(r.uri.scheme, new_uri.scheme) and sameParentDomain(old_host, new_host); r.client.connection_pool.release(old_connection); r.connection = null; if (!keep_privileged_headers) { // When redirecting to a different domain, strip privileged headers. r.privileged_headers = &.{}; } if (switch (head.status) { .see_other => true, .moved_permanently, .found => r.method == .POST, else => false, }) { // A redirect to a GET must change the method and remove the body. r.method = .GET; r.transfer_encoding = .none; r.headers.content_type = .omit; } if (r.transfer_encoding != .none) { // The request body has already been sent. The request is // still in a valid state, but the redirect must be handled // manually. return error.RedirectRequiresResend; } const new_connection = try r.client.connect(new_host, uriPort(new_uri, protocol), protocol); r.uri = new_uri; r.connection = new_connection; r.reader = .{ .in = new_connection.reader(), .state = .ready, // Populated when `http.Reader.bodyReader` is called. .interface = undefined, .max_head_len = r.client.read_buffer_size, }; r.redirect_behavior.subtractOne(); } /// Returns true if the default behavior is required, otherwise handles /// writing (or not writing) the header. fn emitOverridableHeader(prefix: []const u8, v: Headers.Value, bw: *Writer) Writer.Error!bool { switch (v) { .default => return true, .omit => return false, .override => |x| { var vecs: [3][]const u8 = .{ prefix, x, "\r\n" }; try bw.writeVecAll(&vecs); return false; }, } } }