struct Request [src]

A HTTP request that has been sent. Order of operations: open -> send[ -> write -> finish] -> wait -> read

Fields

uri: Uri
client: *Client
connection: ?*ConnectionThis is null when the connection is released.
keep_alive: bool
method: http.Method
version: http.Version = .@"HTTP/1.1"
transfer_encoding: RequestTransfer
redirect_behavior: RedirectBehavior
handle_continue: boolWhether the request should handle a 100-continue response before sending the request body.
response: ResponseThe response associated with this request. This field is undefined until wait is called.
headers: HeadersStandard headers that have default, but overridable, behavior.
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 { uri: Uri, client: *Client, /// This is null when the connection is released. connection: ?*Connection, keep_alive: bool, method: http.Method, version: http.Version = .@"HTTP/1.1", transfer_encoding: RequestTransfer, redirect_behavior: RedirectBehavior, /// Whether the request should handle a 100-continue response before sending the request body. handle_continue: bool, /// The response associated with this request. /// /// This field is undefined until `wait` is called. response: Response, /// Standard headers that have default, but overridable, behavior. headers: Headers, /// 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 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 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); } }; /// Frees all resources associated with the request. pub fn deinit(req: *Request) void { if (req.connection) |connection| { if (!req.response.parser.done) { // If the response wasn't fully read, then we need to close the connection. connection.closing = true; } req.client.connection_pool.release(req.client.allocator, connection); } req.* = undefined; } // This function must deallocate all resources associated with the request, // or keep those which will be used. // This needs to be kept in sync with deinit and request. fn redirect(req: *Request, uri: Uri) !void { assert(req.response.parser.done); req.client.connection_pool.release(req.client.allocator, req.connection.?); req.connection = null; var server_header: std.heap.FixedBufferAllocator = .init(req.response.parser.header_bytes_buffer); defer req.response.parser.header_bytes_buffer = server_header.buffer[server_header.end_index..]; const protocol, const valid_uri = try validateUri(uri, server_header.allocator()); const new_host = valid_uri.host.?.raw; const prev_host = req.uri.host.?.raw; const keep_privileged_headers = std.ascii.eqlIgnoreCase(valid_uri.scheme, req.uri.scheme) and std.ascii.endsWithIgnoreCase(new_host, prev_host) and (new_host.len == prev_host.len or new_host[new_host.len - prev_host.len - 1] == '.'); if (!keep_privileged_headers) { // When redirecting to a different domain, strip privileged headers. req.privileged_headers = &.{}; } if (switch (req.response.status) { .see_other => true, .moved_permanently, .found => req.method == .POST, else => false, }) { // A redirect to a GET must change the method and remove the body. req.method = .GET; req.transfer_encoding = .none; req.headers.content_type = .omit; } if (req.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; } req.uri = valid_uri; req.connection = try req.client.connect(new_host, uriPort(valid_uri, protocol), protocol); req.redirect_behavior.subtractOne(); req.response.parser.reset(); req.response = .{ .version = undefined, .status = undefined, .reason = undefined, .keep_alive = undefined, .parser = req.response.parser, }; } pub const SendError = Connection.WriteError || error{ InvalidContentLength, UnsupportedTransferEncoding }; /// Send the HTTP request headers to the server. pub fn send(req: *Request) SendError!void { if (!req.method.requestHasBody() and req.transfer_encoding != .none) return error.UnsupportedTransferEncoding; const connection = req.connection.?; const w = connection.writer(); try req.method.write(w); try w.writeByte(' '); if (req.method == .CONNECT) { try req.uri.writeToStream(.{ .authority = true }, w); } else { try req.uri.writeToStream(.{ .scheme = connection.proxied, .authentication = connection.proxied, .authority = connection.proxied, .path = true, .query = true, }, w); } try w.writeByte(' '); try w.writeAll(@tagName(req.version)); try w.writeAll("\r\n"); if (try emitOverridableHeader("host: ", req.headers.host, w)) { try w.writeAll("host: "); try req.uri.writeToStream(.{ .authority = true }, w); try w.writeAll("\r\n"); } if (try emitOverridableHeader("authorization: ", req.headers.authorization, w)) { if (req.uri.user != null or req.uri.password != null) { try w.writeAll("authorization: "); const authorization = try connection.allocWriteBuffer( @intCast(basic_authorization.valueLengthFromUri(req.uri)), ); assert(basic_authorization.value(req.uri, authorization).len == authorization.len); try w.writeAll("\r\n"); } } if (try emitOverridableHeader("user-agent: ", req.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: ", req.headers.connection, w)) { if (req.keep_alive) { try w.writeAll("connection: keep-alive\r\n"); } else { try w.writeAll("connection: close\r\n"); } } if (try emitOverridableHeader("accept-encoding: ", req.headers.accept_encoding, w)) { // https://github.com/ziglang/zig/issues/18937 //try w.writeAll("accept-encoding: gzip, deflate, zstd\r\n"); try w.writeAll("accept-encoding: gzip, deflate\r\n"); } switch (req.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: ", req.headers.content_type, w)) { // The default is to omit content-type if not provided because // "application/octet-stream" is redundant. } for (req.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 => req.client.http_proxy, .tls => req.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"); try connection.flush(); } /// Returns true if the default behavior is required, otherwise handles /// writing (or not writing) the header. fn emitOverridableHeader(prefix: []const u8, v: Headers.Value, w: anytype) !bool { switch (v) { .default => return true, .omit => return false, .override => |x| { try w.writeAll(prefix); try w.writeAll(x); try w.writeAll("\r\n"); return false; }, } } const TransferReadError = Connection.ReadError || proto.HeadersParser.ReadError; const TransferReader = std.io.Reader(*Request, TransferReadError, transferRead); fn transferReader(req: *Request) TransferReader { return .{ .context = req }; } fn transferRead(req: *Request, buf: []u8) TransferReadError!usize { if (req.response.parser.done) return 0; var index: usize = 0; while (index == 0) { const amt = try req.response.parser.read(req.connection.?, buf[index..], req.response.skip); if (amt == 0 and req.response.parser.done) break; index += amt; } return index; } pub const WaitError = RequestError || SendError || TransferReadError || proto.HeadersParser.CheckCompleteHeadError || Response.ParseError || error{ TooManyHttpRedirects, RedirectRequiresResend, HttpRedirectLocationMissing, HttpRedirectLocationInvalid, CompressionInitializationFailed, CompressionUnsupported, }; /// Waits for a response from the server and parses any headers that are sent. /// This function will block until the final response is received. /// /// 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. /// /// Must be called after `send` and, if any data was written to the request /// body, then also after `finish`. pub fn wait(req: *Request) WaitError!void { while (true) { // 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 = req.connection.?; while (true) { // read headers try connection.fill(); const nchecked = try req.response.parser.checkCompleteHead(connection.peek()); connection.drop(@intCast(nchecked)); if (req.response.parser.state.isContent()) break; } try req.response.parse(req.response.parser.get()); if (req.response.status == .@"continue") { // We're done parsing the continue response; reset to prepare // for the real response. req.response.parser.done = true; req.response.parser.reset(); if (req.handle_continue) continue; return; // we're not handling the 100-continue } // we're switching protocols, so this connection is no longer doing http if (req.method == .CONNECT and req.response.status.class() == .success) { connection.closing = false; req.response.parser.done = true; return; // the connection is not HTTP past this point } connection.closing = !req.response.keep_alive or !req.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 (req.method == .HEAD or req.response.status.class() == .informational or req.response.status == .no_content or req.response.status == .not_modified) { req.response.parser.done = true; return; // The response is empty; no further setup or redirection is necessary. } switch (req.response.transfer_encoding) { .none => { if (req.response.content_length) |cl| { req.response.parser.next_chunk_length = cl; if (cl == 0) req.response.parser.done = true; } else { // read until the connection is closed req.response.parser.next_chunk_length = std.math.maxInt(u64); } }, .chunked => { req.response.parser.next_chunk_length = 0; req.response.parser.state = .chunk_head_size; }, } if (req.response.status.class() == .redirect and req.redirect_behavior != .unhandled) { // skip the body of the redirect response, this will at least // leave the connection in a known good state. req.response.skip = true; assert(try req.transferRead(&.{}) == 0); // we're skipping, no buffer is necessary if (req.redirect_behavior == .not_allowed) return error.TooManyHttpRedirects; const location = req.response.location orelse return error.HttpRedirectLocationMissing; // This mutates the beginning of header_bytes_buffer and uses that // for the backing memory of the returned Uri. try req.redirect(req.uri.resolve_inplace( location, &req.response.parser.header_bytes_buffer, ) catch |err| switch (err) { error.UnexpectedCharacter, error.InvalidFormat, error.InvalidPort, => return error.HttpRedirectLocationInvalid, error.NoSpaceLeft => return error.HttpHeadersOversize, }); try req.send(); } else { req.response.skip = false; if (!req.response.parser.done) { switch (req.response.transfer_compression) { .identity => req.response.compression = .none, .compress, .@"x-compress" => return error.CompressionUnsupported, .deflate => req.response.compression = .{ .deflate = std.compress.zlib.decompressor(req.transferReader()), }, .gzip, .@"x-gzip" => req.response.compression = .{ .gzip = std.compress.gzip.decompressor(req.transferReader()), }, // https://github.com/ziglang/zig/issues/18937 //.zstd => req.response.compression = .{ // .zstd = std.compress.zstd.decompressStream(req.client.allocator, req.transferReader()), //}, .zstd => return error.CompressionUnsupported, } } break; } } } pub const ReadError = TransferReadError || proto.HeadersParser.CheckCompleteHeadError || error{ DecompressionFailure, InvalidTrailers }; pub const Reader = std.io.Reader(*Request, ReadError, read); pub fn reader(req: *Request) Reader { return .{ .context = req }; } /// Reads data from the response body. Must be called after `wait`. pub fn read(req: *Request, buffer: []u8) ReadError!usize { const out_index = switch (req.response.compression) { .deflate => |*deflate| deflate.read(buffer) catch return error.DecompressionFailure, .gzip => |*gzip| gzip.read(buffer) catch return error.DecompressionFailure, // https://github.com/ziglang/zig/issues/18937 //.zstd => |*zstd| zstd.read(buffer) catch return error.DecompressionFailure, else => try req.transferRead(buffer), }; if (out_index > 0) return out_index; while (!req.response.parser.state.isContent()) { // read trailing headers try req.connection.?.fill(); const nchecked = try req.response.parser.checkCompleteHead(req.connection.?.peek()); req.connection.?.drop(@intCast(nchecked)); } return 0; } /// Reads data from the response body. Must be called after `wait`. pub fn readAll(req: *Request, buffer: []u8) !usize { var index: usize = 0; while (index < buffer.len) { const amt = try read(req, buffer[index..]); if (amt == 0) break; index += amt; } return index; } pub const WriteError = Connection.WriteError || error{ NotWriteable, MessageTooLong }; pub const Writer = std.io.Writer(*Request, WriteError, write); pub fn writer(req: *Request) Writer { return .{ .context = req }; } /// Write `bytes` to the server. The `transfer_encoding` field determines how data will be sent. /// Must be called after `send` and before `finish`. pub fn write(req: *Request, bytes: []const u8) WriteError!usize { switch (req.transfer_encoding) { .chunked => { if (bytes.len > 0) { try req.connection.?.writer().print("{x}\r\n", .{bytes.len}); try req.connection.?.writer().writeAll(bytes); try req.connection.?.writer().writeAll("\r\n"); } return bytes.len; }, .content_length => |*len| { if (len.* < bytes.len) return error.MessageTooLong; const amt = try req.connection.?.write(bytes); len.* -= amt; return amt; }, .none => return error.NotWriteable, } } /// Write `bytes` to the server. The `transfer_encoding` field determines how data will be sent. /// Must be called after `send` and before `finish`. pub fn writeAll(req: *Request, bytes: []const u8) WriteError!void { var index: usize = 0; while (index < bytes.len) { index += try write(req, bytes[index..]); } } pub const FinishError = WriteError || error{MessageNotCompleted}; /// Finish the body of a request. This notifies the server that you have no more data to send. /// Must be called after `send`. pub fn finish(req: *Request) FinishError!void { switch (req.transfer_encoding) { .chunked => try req.connection.?.writer().writeAll("0\r\n\r\n"), .content_length => |len| if (len != 0) return error.MessageNotCompleted, .none => {}, } try req.connection.?.flush(); } }