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
- deinit (Function)
- finish (Function)
- FinishError (Error Set)
- Headers (struct)
- read (Function)
- readAll (Function)
- reader (Function)
- Reader (Type)
- ReadError (Error Set)
- RedirectBehavior (enum)
- send (Function)
- SendError (Error Set)
- wait (Function)
- WaitError (Error Set)
- write (Function)
- writeAll (Function)
- WriteError (Error Set)
- writer (Function)
- Writer (Type)
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();
}
}