feature. See also
. The project being documented here (as the example) is the Zig library itself.
Client.Request
pub const Request = struct
File
Code
pub const Request = struct {
uri: Uri,
client: *Client,
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,
handle_continue: bool,
headers: Headers,
response_content_length: ?u64 = null,
response_transfer_encoding: http.TransferEncoding = .none,
extra_headers: []const http.Header,
privileged_headers: []const http.Header,
pub const default_accept_encoding: [@typeInfo(http.ContentEncoding).@"enum".field_names.len]bool = b: {
var result: [@typeInfo(http.ContentEncoding).@"enum".field_names.len]bool = @splat(false);
result[@backingInt(http.ContentEncoding.gzip)] = true;
result[@backingInt(http.ContentEncoding.deflate)] = true;
result[@backingInt(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,
};
};
pub const RedirectBehavior = enum(u16) {
not_allowed = 0,
unhandled = std.math.maxInt(u16),
_,
pub fn init(n: u16) RedirectBehavior {
assert(n != std.math.maxInt(u16));
return @fromBackingInt(@intCast(n));
}
pub fn subtractOne(rb: *RedirectBehavior) void {
switch (rb.*) {
.not_allowed => unreachable,
.unhandled => unreachable,
_ => rb.* = @fromBackingInt(@intCast(@backingInt(rb.*) - 1)),
}
}
pub fn remaining(rb: RedirectBehavior) u16 {
assert(rb != .unhandled);
return @backingInt(rb);
}
};
pub fn deinit(r: *Request) void {
const io = r.client.io;
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, io);
}
r.* = undefined;
}
pub fn sendBodiless(r: *Request) Writer.Error!void {
try sendBodilessUnflushed(r);
try r.connection.?.flush();
}
pub fn sendBodilessUnflushed(r: *Request) Writer.Error!void {
assert(r.transfer_encoding == .none);
assert(!r.method.requestHasBody());
try sendHead(r);
}
pub fn sendBody(r: *Request, buffer: []u8) Writer.Error!http.BodyWriter {
const result = try sendBodyUnflushed(r, buffer);
try r.connection.?.flush();
return result;
}
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();
}
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,
},
},
},
};
}
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 = @fromBackingInt(@intCast(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)) {
// "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{
HttpHeadersInvalid,
TooManyHttpRedirects,
RedirectRequiresResend,
HttpRedirectLocationMissing,
HttpRedirectLocationOversize,
HttpRedirectLocationInvalid,
HttpContentEncodingUnsupported,
HttpChunkInvalid,
HttpChunkTruncated,
HttpHeadersOversize,
UnsupportedUriScheme,
WriteFailed,
};
pub fn receiveHead(r: *Request, redirect_buffer: []u8) ReceiveHeadError!Response {
var aux_buf = redirect_buffer;
while (true) {
// 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.?;
const head_buffer = r.reader.receiveHead() catch |err| {
connection.closing = true;
return err;
};
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;
}
if (r.method == .CONNECT and head.status.class() == .success) {
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;
// (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) {
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[@backingInt(head.content_encoding)])
return error.HttpContentEncodingUnsupported;
r.response_transfer_encoding = head.transfer_encoding;
r.response_content_length = head.content_length;
return response;
}
}
fn redirect(r: *Request, head: *const Response.Head, aux_buf: *[]u8) !void {
const io = r.client.io;
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);
{
// 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.InvalidHostName => 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: [HostName.max_len]u8 = undefined;
const new_host = HostName.fromUri(new_uri, &new_host_name_buffer) catch |err| switch (err) {
error.UriMissingHost => return error.HttpRedirectLocationInvalid,
error.InvalidHostName => return error.HttpRedirectLocationInvalid,
error.NameTooLong => return error.HttpRedirectLocationOversize,
};
const keep_privileged_headers =
std.ascii.eqlIgnoreCase(r.uri.scheme, new_uri.scheme) and
old_host.sameParentDomain(new_host);
r.client.connection_pool.release(old_connection, io);
r.connection = null;
if (!keep_privileged_headers) {
r.privileged_headers = &.{};
}
if (switch (head.status) {
.see_other => true,
.moved_permanently, .found => r.method == .POST,
else => false,
}) {
r.method = .GET;
r.transfer_encoding = .none;
r.headers.content_type = .omit;
}
if (r.transfer_encoding != .none) {
// 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,
.interface = undefined,
.max_head_len = r.client.read_buffer_size,
};
r.redirect_behavior.subtractOne();
}
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;
},
}
}
}