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
- default_accept_encoding (Constant)
- deinit (Function)
- Headers (struct)
- receiveHead (Function)
- ReceiveHeadError (Error Set)
- RedirectBehavior (enum)
- sendBodiless (Function)
- sendBodilessUnflushed (Function)
- sendBody (Function)
- sendBodyComplete (Function)
- sendBodyUnflushed (Function)
- TransferEncoding (union)
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;
},
}
}
}