struct Server [src]
Alias for std.http.Server
Blocking HTTP server implementation.
Handles a single connection's lifecycle.
Fields
connection: net.Server.Connection
state: StateKeeps track of whether the Server is ready to accept a new request on the
same connection, and makes invalid API usage cause assertion failures
rather than HTTP protocol violations.
read_buffer: []u8User-provided buffer that must outlive this Server.
Used to store the client's entire HTTP header.
read_buffer_len: usizeAmount of available data inside read_buffer.
next_request_start: usizeIndex into read_buffer of the first byte of the next HTTP request.
Members
- init (Function)
- receiveHead (Function)
- ReceiveHeadError (Error Set)
- Request (struct)
- Response (struct)
- State (enum)
Source
//! Blocking HTTP server implementation.
//! Handles a single connection's lifecycle.
connection: net.Server.Connection,
/// Keeps track of whether the Server is ready to accept a new request on the
/// same connection, and makes invalid API usage cause assertion failures
/// rather than HTTP protocol violations.
state: State,
/// User-provided buffer that must outlive this Server.
/// Used to store the client's entire HTTP header.
read_buffer: []u8,
/// Amount of available data inside read_buffer.
read_buffer_len: usize,
/// Index into `read_buffer` of the first byte of the next HTTP request.
next_request_start: usize,
pub const State = enum {
/// The connection is available to be used for the first time, or reused.
ready,
/// An error occurred in `receiveHead`.
receiving_head,
/// A Request object has been obtained and from there a Response can be
/// opened.
received_head,
/// The client is uploading something to this Server.
receiving_body,
/// The connection is eligible for another HTTP request, however the client
/// and server did not negotiate a persistent connection.
closing,
};
/// Initialize an HTTP server that can respond to multiple requests on the same
/// connection.
/// The returned `Server` is ready for `receiveHead` to be called.
pub fn init(connection: net.Server.Connection, read_buffer: []u8) Server {
return .{
.connection = connection,
.state = .ready,
.read_buffer = read_buffer,
.read_buffer_len = 0,
.next_request_start = 0,
};
}
pub const ReceiveHeadError = error{
/// Client sent too many bytes of HTTP headers.
/// The HTTP specification suggests to respond with a 431 status code
/// before closing the connection.
HttpHeadersOversize,
/// Client sent headers that did not conform to the HTTP protocol.
HttpHeadersInvalid,
/// A low level I/O error occurred trying to read the headers.
HttpHeadersUnreadable,
/// Partial HTTP request was received but the connection was closed before
/// fully receiving the headers.
HttpRequestTruncated,
/// The client sent 0 bytes of headers before closing the stream.
/// In other words, a keep-alive connection was finally closed.
HttpConnectionClosing,
};
/// The header bytes reference the read buffer that Server was initialized with
/// and remain alive until the next call to receiveHead.
pub fn receiveHead(s: *Server) ReceiveHeadError!Request {
assert(s.state == .ready);
s.state = .received_head;
errdefer s.state = .receiving_head;
// In case of a reused connection, move the next request's bytes to the
// beginning of the buffer.
if (s.next_request_start > 0) {
if (s.read_buffer_len > s.next_request_start) {
rebase(s, 0);
} else {
s.read_buffer_len = 0;
}
}
var hp: http.HeadParser = .{};
if (s.read_buffer_len > 0) {
const bytes = s.read_buffer[0..s.read_buffer_len];
const end = hp.feed(bytes);
if (hp.state == .finished)
return finishReceivingHead(s, end);
}
while (true) {
const buf = s.read_buffer[s.read_buffer_len..];
if (buf.len == 0)
return error.HttpHeadersOversize;
const read_n = s.connection.stream.read(buf) catch
return error.HttpHeadersUnreadable;
if (read_n == 0) {
if (s.read_buffer_len > 0) {
return error.HttpRequestTruncated;
} else {
return error.HttpConnectionClosing;
}
}
s.read_buffer_len += read_n;
const bytes = buf[0..read_n];
const end = hp.feed(bytes);
if (hp.state == .finished)
return finishReceivingHead(s, s.read_buffer_len - bytes.len + end);
}
}
fn finishReceivingHead(s: *Server, head_end: usize) ReceiveHeadError!Request {
return .{
.server = s,
.head_end = head_end,
.head = Request.Head.parse(s.read_buffer[0..head_end]) catch
return error.HttpHeadersInvalid,
.reader_state = undefined,
};
}
pub const Request = struct {
server: *Server,
/// Index into Server's read_buffer.
head_end: usize,
head: Head,
reader_state: union {
remaining_content_length: u64,
chunk_parser: http.ChunkParser,
},
pub const Compression = union(enum) {
pub const DeflateDecompressor = std.compress.zlib.Decompressor(std.io.AnyReader);
pub const GzipDecompressor = std.compress.gzip.Decompressor(std.io.AnyReader);
pub const ZstdDecompressor = std.compress.zstd.Decompressor(std.io.AnyReader);
deflate: DeflateDecompressor,
gzip: GzipDecompressor,
zstd: ZstdDecompressor,
none: void,
};
pub const Head = struct {
method: http.Method,
target: []const u8,
version: http.Version,
expect: ?[]const u8,
content_type: ?[]const u8,
content_length: ?u64,
transfer_encoding: http.TransferEncoding,
transfer_compression: http.ContentEncoding,
keep_alive: bool,
compression: Compression,
pub const ParseError = error{
UnknownHttpMethod,
HttpHeadersInvalid,
HttpHeaderContinuationsUnsupported,
HttpTransferEncodingUnsupported,
HttpConnectionHeaderUnsupported,
InvalidContentLength,
CompressionUnsupported,
MissingFinalNewline,
};
pub fn parse(bytes: []const u8) ParseError!Head {
var it = mem.splitSequence(u8, bytes, "\r\n");
const first_line = it.next().?;
if (first_line.len < 10)
return error.HttpHeadersInvalid;
const method_end = mem.indexOfScalar(u8, first_line, ' ') orelse
return error.HttpHeadersInvalid;
if (method_end > 24) return error.HttpHeadersInvalid;
const method_str = first_line[0..method_end];
const method: http.Method = @enumFromInt(http.Method.parse(method_str));
const version_start = mem.lastIndexOfScalar(u8, first_line, ' ') orelse
return error.HttpHeadersInvalid;
if (version_start == method_end) return error.HttpHeadersInvalid;
const version_str = first_line[version_start + 1 ..];
if (version_str.len != 8) return error.HttpHeadersInvalid;
const version: http.Version = switch (int64(version_str[0..8])) {
int64("HTTP/1.0") => .@"HTTP/1.0",
int64("HTTP/1.1") => .@"HTTP/1.1",
else => return error.HttpHeadersInvalid,
};
const target = first_line[method_end + 1 .. version_start];
var head: Head = .{
.method = method,
.target = target,
.version = version,
.expect = null,
.content_type = null,
.content_length = null,
.transfer_encoding = .none,
.transfer_compression = .identity,
.keep_alive = switch (version) {
.@"HTTP/1.0" => false,
.@"HTTP/1.1" => true,
},
.compression = .none,
};
while (it.next()) |line| {
if (line.len == 0) return head;
switch (line[0]) {
' ', '\t' => return error.HttpHeaderContinuationsUnsupported,
else => {},
}
var line_it = mem.splitScalar(u8, line, ':');
const header_name = line_it.next().?;
const header_value = mem.trim(u8, line_it.rest(), " \t");
if (header_name.len == 0) return error.HttpHeadersInvalid;
if (std.ascii.eqlIgnoreCase(header_name, "connection")) {
head.keep_alive = !std.ascii.eqlIgnoreCase(header_value, "close");
} else if (std.ascii.eqlIgnoreCase(header_name, "expect")) {
head.expect = header_value;
} else if (std.ascii.eqlIgnoreCase(header_name, "content-type")) {
head.content_type = header_value;
} else if (std.ascii.eqlIgnoreCase(header_name, "content-length")) {
if (head.content_length != null) return error.HttpHeadersInvalid;
head.content_length = std.fmt.parseInt(u64, header_value, 10) catch
return error.InvalidContentLength;
} else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) {
if (head.transfer_compression != .identity) return error.HttpHeadersInvalid;
const trimmed = mem.trim(u8, header_value, " ");
if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
head.transfer_compression = ce;
} else {
return error.HttpTransferEncodingUnsupported;
}
} else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) {
// Transfer-Encoding: second, first
// Transfer-Encoding: deflate, chunked
var iter = mem.splitBackwardsScalar(u8, header_value, ',');
const first = iter.first();
const trimmed_first = mem.trim(u8, first, " ");
var next: ?[]const u8 = first;
if (std.meta.stringToEnum(http.TransferEncoding, trimmed_first)) |transfer| {
if (head.transfer_encoding != .none)
return error.HttpHeadersInvalid; // we already have a transfer encoding
head.transfer_encoding = transfer;
next = iter.next();
}
if (next) |second| {
const trimmed_second = mem.trim(u8, second, " ");
if (std.meta.stringToEnum(http.ContentEncoding, trimmed_second)) |transfer| {
if (head.transfer_compression != .identity)
return error.HttpHeadersInvalid; // double compression is not supported
head.transfer_compression = transfer;
} else {
return error.HttpTransferEncodingUnsupported;
}
}
if (iter.next()) |_| return error.HttpTransferEncodingUnsupported;
}
}
return error.MissingFinalNewline;
}
test parse {
const request_bytes = "GET /hi HTTP/1.0\r\n" ++
"content-tYpe: text/plain\r\n" ++
"content-Length:10\r\n" ++
"expeCt: 100-continue \r\n" ++
"TRansfer-encoding:\tdeflate, chunked \r\n" ++
"connectioN:\t keep-alive \r\n\r\n";
const req = try parse(request_bytes);
try testing.expectEqual(.GET, req.method);
try testing.expectEqual(.@"HTTP/1.0", req.version);
try testing.expectEqualStrings("/hi", req.target);
try testing.expectEqualStrings("text/plain", req.content_type.?);
try testing.expectEqualStrings("100-continue", req.expect.?);
try testing.expectEqual(true, req.keep_alive);
try testing.expectEqual(10, req.content_length.?);
try testing.expectEqual(.chunked, req.transfer_encoding);
try testing.expectEqual(.deflate, req.transfer_compression);
}
inline fn int64(array: *const [8]u8) u64 {
return @bitCast(array.*);
}
};
pub fn iterateHeaders(r: *Request) http.HeaderIterator {
return http.HeaderIterator.init(r.server.read_buffer[0..r.head_end]);
}
test iterateHeaders {
const request_bytes = "GET /hi HTTP/1.0\r\n" ++
"content-tYpe: text/plain\r\n" ++
"content-Length:10\r\n" ++
"expeCt: 100-continue \r\n" ++
"TRansfer-encoding:\tdeflate, chunked \r\n" ++
"connectioN:\t keep-alive \r\n\r\n";
var read_buffer: [500]u8 = undefined;
@memcpy(read_buffer[0..request_bytes.len], request_bytes);
var server: Server = .{
.connection = undefined,
.state = .ready,
.read_buffer = &read_buffer,
.read_buffer_len = request_bytes.len,
.next_request_start = 0,
};
var request: Request = .{
.server = &server,
.head_end = request_bytes.len,
.head = undefined,
.reader_state = undefined,
};
var it = request.iterateHeaders();
{
const header = it.next().?;
try testing.expectEqualStrings("content-tYpe", header.name);
try testing.expectEqualStrings("text/plain", header.value);
try testing.expect(!it.is_trailer);
}
{
const header = it.next().?;
try testing.expectEqualStrings("content-Length", header.name);
try testing.expectEqualStrings("10", header.value);
try testing.expect(!it.is_trailer);
}
{
const header = it.next().?;
try testing.expectEqualStrings("expeCt", header.name);
try testing.expectEqualStrings("100-continue", header.value);
try testing.expect(!it.is_trailer);
}
{
const header = it.next().?;
try testing.expectEqualStrings("TRansfer-encoding", header.name);
try testing.expectEqualStrings("deflate, chunked", header.value);
try testing.expect(!it.is_trailer);
}
{
const header = it.next().?;
try testing.expectEqualStrings("connectioN", header.name);
try testing.expectEqualStrings("keep-alive", header.value);
try testing.expect(!it.is_trailer);
}
try testing.expectEqual(null, it.next());
}
pub const RespondOptions = struct {
version: http.Version = .@"HTTP/1.1",
status: http.Status = .ok,
reason: ?[]const u8 = null,
keep_alive: bool = true,
extra_headers: []const http.Header = &.{},
transfer_encoding: ?http.TransferEncoding = null,
};
/// Send an entire HTTP response to the client, including headers and body.
///
/// Automatically handles HEAD requests by omitting the body.
///
/// Unless `transfer_encoding` is specified, uses the "content-length"
/// header.
///
/// If the request contains a body and the connection is to be reused,
/// discards the request body, leaving the Server in the `ready` state. If
/// this discarding fails, the connection is marked as not to be reused and
/// no error is surfaced.
///
/// Asserts status is not `continue`.
/// Asserts there are at most 25 extra_headers.
/// Asserts that "\r\n" does not occur in any header name or value.
pub fn respond(
request: *Request,
content: []const u8,
options: RespondOptions,
) Response.WriteError!void {
const max_extra_headers = 25;
assert(options.status != .@"continue");
assert(options.extra_headers.len <= max_extra_headers);
if (std.debug.runtime_safety) {
for (options.extra_headers) |header| {
assert(header.name.len != 0);
assert(std.mem.indexOfScalar(u8, header.name, ':') == null);
assert(std.mem.indexOfPosLinear(u8, header.name, 0, "\r\n") == null);
assert(std.mem.indexOfPosLinear(u8, header.value, 0, "\r\n") == null);
}
}
const transfer_encoding_none = (options.transfer_encoding orelse .chunked) == .none;
const server_keep_alive = !transfer_encoding_none and options.keep_alive;
const keep_alive = request.discardBody(server_keep_alive);
const phrase = options.reason orelse options.status.phrase() orelse "";
var first_buffer: [500]u8 = undefined;
var h = std.ArrayListUnmanaged(u8).initBuffer(&first_buffer);
if (request.head.expect != null) {
// reader() and hence discardBody() above sets expect to null if it
// is handled. So the fact that it is not null here means unhandled.
h.appendSliceAssumeCapacity("HTTP/1.1 417 Expectation Failed\r\n");
if (!keep_alive) h.appendSliceAssumeCapacity("connection: close\r\n");
h.appendSliceAssumeCapacity("content-length: 0\r\n\r\n");
try request.server.connection.stream.writeAll(h.items);
return;
}
h.fixedWriter().print("{s} {d} {s}\r\n", .{
@tagName(options.version), @intFromEnum(options.status), phrase,
}) catch unreachable;
switch (options.version) {
.@"HTTP/1.0" => if (keep_alive) h.appendSliceAssumeCapacity("connection: keep-alive\r\n"),
.@"HTTP/1.1" => if (!keep_alive) h.appendSliceAssumeCapacity("connection: close\r\n"),
}
if (options.transfer_encoding) |transfer_encoding| switch (transfer_encoding) {
.none => {},
.chunked => h.appendSliceAssumeCapacity("transfer-encoding: chunked\r\n"),
} else {
h.fixedWriter().print("content-length: {d}\r\n", .{content.len}) catch unreachable;
}
var chunk_header_buffer: [18]u8 = undefined;
var iovecs: [max_extra_headers * 4 + 3]std.posix.iovec_const = undefined;
var iovecs_len: usize = 0;
iovecs[iovecs_len] = .{
.base = h.items.ptr,
.len = h.items.len,
};
iovecs_len += 1;
for (options.extra_headers) |header| {
iovecs[iovecs_len] = .{
.base = header.name.ptr,
.len = header.name.len,
};
iovecs_len += 1;
iovecs[iovecs_len] = .{
.base = ": ",
.len = 2,
};
iovecs_len += 1;
if (header.value.len != 0) {
iovecs[iovecs_len] = .{
.base = header.value.ptr,
.len = header.value.len,
};
iovecs_len += 1;
}
iovecs[iovecs_len] = .{
.base = "\r\n",
.len = 2,
};
iovecs_len += 1;
}
iovecs[iovecs_len] = .{
.base = "\r\n",
.len = 2,
};
iovecs_len += 1;
if (request.head.method != .HEAD) {
const is_chunked = (options.transfer_encoding orelse .none) == .chunked;
if (is_chunked) {
if (content.len > 0) {
const chunk_header = std.fmt.bufPrint(
&chunk_header_buffer,
"{x}\r\n",
.{content.len},
) catch unreachable;
iovecs[iovecs_len] = .{
.base = chunk_header.ptr,
.len = chunk_header.len,
};
iovecs_len += 1;
iovecs[iovecs_len] = .{
.base = content.ptr,
.len = content.len,
};
iovecs_len += 1;
iovecs[iovecs_len] = .{
.base = "\r\n",
.len = 2,
};
iovecs_len += 1;
}
iovecs[iovecs_len] = .{
.base = "0\r\n\r\n",
.len = 5,
};
iovecs_len += 1;
} else if (content.len > 0) {
iovecs[iovecs_len] = .{
.base = content.ptr,
.len = content.len,
};
iovecs_len += 1;
}
}
try request.server.connection.stream.writevAll(iovecs[0..iovecs_len]);
}
pub const RespondStreamingOptions = struct {
/// An externally managed slice of memory used to batch bytes before
/// sending. `respondStreaming` asserts this is large enough to store
/// the full HTTP response head.
///
/// Must outlive the returned Response.
send_buffer: []u8,
/// If provided, the response will use the content-length header;
/// otherwise it will use transfer-encoding: chunked.
content_length: ?u64 = null,
/// Options that are shared with the `respond` method.
respond_options: RespondOptions = .{},
};
/// The header is buffered but not sent until Response.flush is called.
///
/// If the request contains a body and the connection is to be reused,
/// discards the request body, leaving the Server in the `ready` state. If
/// this discarding fails, the connection is marked as not to be reused and
/// no error is surfaced.
///
/// HEAD requests are handled transparently by setting a flag on the
/// returned Response to omit the body. However it may be worth noticing
/// that flag and skipping any expensive work that would otherwise need to
/// be done to satisfy the request.
///
/// Asserts `send_buffer` is large enough to store the entire response header.
/// Asserts status is not `continue`.
pub fn respondStreaming(request: *Request, options: RespondStreamingOptions) Response {
const o = options.respond_options;
assert(o.status != .@"continue");
const transfer_encoding_none = (o.transfer_encoding orelse .chunked) == .none;
const server_keep_alive = !transfer_encoding_none and o.keep_alive;
const keep_alive = request.discardBody(server_keep_alive);
const phrase = o.reason orelse o.status.phrase() orelse "";
var h = std.ArrayListUnmanaged(u8).initBuffer(options.send_buffer);
const elide_body = if (request.head.expect != null) eb: {
// reader() and hence discardBody() above sets expect to null if it
// is handled. So the fact that it is not null here means unhandled.
h.appendSliceAssumeCapacity("HTTP/1.1 417 Expectation Failed\r\n");
if (!keep_alive) h.appendSliceAssumeCapacity("connection: close\r\n");
h.appendSliceAssumeCapacity("content-length: 0\r\n\r\n");
break :eb true;
} else eb: {
h.fixedWriter().print("{s} {d} {s}\r\n", .{
@tagName(o.version), @intFromEnum(o.status), phrase,
}) catch unreachable;
switch (o.version) {
.@"HTTP/1.0" => if (keep_alive) h.appendSliceAssumeCapacity("connection: keep-alive\r\n"),
.@"HTTP/1.1" => if (!keep_alive) h.appendSliceAssumeCapacity("connection: close\r\n"),
}
if (o.transfer_encoding) |transfer_encoding| switch (transfer_encoding) {
.chunked => h.appendSliceAssumeCapacity("transfer-encoding: chunked\r\n"),
.none => {},
} else if (options.content_length) |len| {
h.fixedWriter().print("content-length: {d}\r\n", .{len}) catch unreachable;
} else {
h.appendSliceAssumeCapacity("transfer-encoding: chunked\r\n");
}
for (o.extra_headers) |header| {
assert(header.name.len != 0);
h.appendSliceAssumeCapacity(header.name);
h.appendSliceAssumeCapacity(": ");
h.appendSliceAssumeCapacity(header.value);
h.appendSliceAssumeCapacity("\r\n");
}
h.appendSliceAssumeCapacity("\r\n");
break :eb request.head.method == .HEAD;
};
return .{
.stream = request.server.connection.stream,
.send_buffer = options.send_buffer,
.send_buffer_start = 0,
.send_buffer_end = h.items.len,
.transfer_encoding = if (o.transfer_encoding) |te| switch (te) {
.chunked => .chunked,
.none => .none,
} else if (options.content_length) |len| .{
.content_length = len,
} else .chunked,
.elide_body = elide_body,
.chunk_len = 0,
};
}
pub const ReadError = net.Stream.ReadError || error{
HttpChunkInvalid,
HttpHeadersOversize,
};
fn read_cl(context: *const anyopaque, buffer: []u8) ReadError!usize {
const request: *Request = @constCast(@alignCast(@ptrCast(context)));
const s = request.server;
const remaining_content_length = &request.reader_state.remaining_content_length;
if (remaining_content_length.* == 0) {
s.state = .ready;
return 0;
}
assert(s.state == .receiving_body);
const available = try fill(s, request.head_end);
const len = @min(remaining_content_length.*, available.len, buffer.len);
@memcpy(buffer[0..len], available[0..len]);
remaining_content_length.* -= len;
s.next_request_start += len;
if (remaining_content_length.* == 0)
s.state = .ready;
return len;
}
fn fill(s: *Server, head_end: usize) ReadError![]u8 {
const available = s.read_buffer[s.next_request_start..s.read_buffer_len];
if (available.len > 0) return available;
s.next_request_start = head_end;
s.read_buffer_len = head_end + try s.connection.stream.read(s.read_buffer[head_end..]);
return s.read_buffer[head_end..s.read_buffer_len];
}
fn read_chunked(context: *const anyopaque, buffer: []u8) ReadError!usize {
const request: *Request = @constCast(@alignCast(@ptrCast(context)));
const s = request.server;
const cp = &request.reader_state.chunk_parser;
const head_end = request.head_end;
// Protect against returning 0 before the end of stream.
var out_end: usize = 0;
while (out_end == 0) {
switch (cp.state) {
.invalid => return 0,
.data => {
assert(s.state == .receiving_body);
const available = try fill(s, head_end);
const len = @min(cp.chunk_len, available.len, buffer.len);
@memcpy(buffer[0..len], available[0..len]);
cp.chunk_len -= len;
if (cp.chunk_len == 0)
cp.state = .data_suffix;
out_end += len;
s.next_request_start += len;
continue;
},
else => {
assert(s.state == .receiving_body);
const available = try fill(s, head_end);
const n = cp.feed(available);
switch (cp.state) {
.invalid => return error.HttpChunkInvalid,
.data => {
if (cp.chunk_len == 0) {
// The next bytes in the stream are trailers,
// or \r\n to indicate end of chunked body.
//
// This function must append the trailers at
// head_end so that headers and trailers are
// together.
//
// Since returning 0 would indicate end of
// stream, this function must read all the
// trailers before returning.
if (s.next_request_start > head_end) rebase(s, head_end);
var hp: http.HeadParser = .{};
{
const bytes = s.read_buffer[head_end..s.read_buffer_len];
const end = hp.feed(bytes);
if (hp.state == .finished) {
cp.state = .invalid;
s.state = .ready;
s.next_request_start = s.read_buffer_len - bytes.len + end;
return out_end;
}
}
while (true) {
const buf = s.read_buffer[s.read_buffer_len..];
if (buf.len == 0)
return error.HttpHeadersOversize;
const read_n = try s.connection.stream.read(buf);
s.read_buffer_len += read_n;
const bytes = buf[0..read_n];
const end = hp.feed(bytes);
if (hp.state == .finished) {
cp.state = .invalid;
s.state = .ready;
s.next_request_start = s.read_buffer_len - bytes.len + end;
return out_end;
}
}
}
const data = available[n..];
const len = @min(cp.chunk_len, data.len, buffer.len);
@memcpy(buffer[0..len], data[0..len]);
cp.chunk_len -= len;
if (cp.chunk_len == 0)
cp.state = .data_suffix;
out_end += len;
s.next_request_start += n + len;
continue;
},
else => continue,
}
},
}
}
return out_end;
}
pub const ReaderError = Response.WriteError || error{
/// The client sent an expect HTTP header value other than
/// "100-continue".
HttpExpectationFailed,
};
/// In the case that the request contains "expect: 100-continue", this
/// function writes the continuation header, which means it can fail with a
/// write error. After sending the continuation header, it sets the
/// request's expect field to `null`.
///
/// Asserts that this function is only called once.
pub fn reader(request: *Request) ReaderError!std.io.AnyReader {
const s = request.server;
assert(s.state == .received_head);
s.state = .receiving_body;
s.next_request_start = request.head_end;
if (request.head.expect) |expect| {
if (mem.eql(u8, expect, "100-continue")) {
try request.server.connection.stream.writeAll("HTTP/1.1 100 Continue\r\n\r\n");
request.head.expect = null;
} else {
return error.HttpExpectationFailed;
}
}
switch (request.head.transfer_encoding) {
.chunked => {
request.reader_state = .{ .chunk_parser = http.ChunkParser.init };
return .{
.readFn = read_chunked,
.context = request,
};
},
.none => {
request.reader_state = .{
.remaining_content_length = request.head.content_length orelse 0,
};
return .{
.readFn = read_cl,
.context = request,
};
},
}
}
/// Returns whether the connection should remain persistent.
/// If it would fail, it instead sets the Server state to `receiving_body`
/// and returns false.
fn discardBody(request: *Request, keep_alive: bool) bool {
// Prepare to receive another request on the same connection.
// There are two factors to consider:
// * Any body the client sent must be discarded.
// * The Server's read_buffer may already have some bytes in it from
// whatever came after the head, which may be the next HTTP request
// or the request body.
// If the connection won't be kept alive, then none of this matters
// because the connection will be severed after the response is sent.
const s = request.server;
if (keep_alive and request.head.keep_alive) switch (s.state) {
.received_head => {
const r = request.reader() catch return false;
_ = r.discard() catch return false;
assert(s.state == .ready);
return true;
},
.receiving_body, .ready => return true,
else => unreachable,
};
// Avoid clobbering the state in case a reading stream already exists.
switch (s.state) {
.received_head => s.state = .closing,
else => {},
}
return false;
}
};
pub const Response = struct {
stream: net.Stream,
send_buffer: []u8,
/// Index of the first byte in `send_buffer`.
/// This is 0 unless a short write happens in `write`.
send_buffer_start: usize,
/// Index of the last byte + 1 in `send_buffer`.
send_buffer_end: usize,
/// `null` means transfer-encoding: chunked.
/// As a debugging utility, counts down to zero as bytes are written.
transfer_encoding: TransferEncoding,
elide_body: bool,
/// Indicates how much of the end of the `send_buffer` corresponds to a
/// chunk. This amount of data will be wrapped by an HTTP chunk header.
chunk_len: usize,
pub const TransferEncoding = union(enum) {
/// End of connection signals the end of the stream.
none,
/// As a debugging utility, counts down to zero as bytes are written.
content_length: u64,
/// Each chunk is wrapped in a header and trailer.
chunked,
};
pub const WriteError = net.Stream.WriteError;
/// When using content-length, asserts that the amount of data sent matches
/// the value sent in the header, then calls `flush`.
/// Otherwise, transfer-encoding: chunked is being used, and it writes the
/// end-of-stream message, then flushes the stream to the system.
/// Respects the value of `elide_body` to omit all data after the headers.
pub fn end(r: *Response) WriteError!void {
switch (r.transfer_encoding) {
.content_length => |len| {
assert(len == 0); // Trips when end() called before all bytes written.
try flush_cl(r);
},
.none => {
try flush_cl(r);
},
.chunked => {
try flush_chunked(r, &.{});
},
}
r.* = undefined;
}
pub const EndChunkedOptions = struct {
trailers: []const http.Header = &.{},
};
/// Asserts that the Response is using transfer-encoding: chunked.
/// Writes the end-of-stream message and any optional trailers, then
/// flushes the stream to the system.
/// Respects the value of `elide_body` to omit all data after the headers.
/// Asserts there are at most 25 trailers.
pub fn endChunked(r: *Response, options: EndChunkedOptions) WriteError!void {
assert(r.transfer_encoding == .chunked);
try flush_chunked(r, options.trailers);
r.* = undefined;
}
/// If using content-length, asserts that writing these bytes to the client
/// would not exceed the content-length value sent in the HTTP header.
/// May return 0, which does not indicate end of stream. The caller decides
/// when the end of stream occurs by calling `end`.
pub fn write(r: *Response, bytes: []const u8) WriteError!usize {
switch (r.transfer_encoding) {
.content_length, .none => return write_cl(r, bytes),
.chunked => return write_chunked(r, bytes),
}
}
fn write_cl(context: *const anyopaque, bytes: []const u8) WriteError!usize {
const r: *Response = @constCast(@alignCast(@ptrCast(context)));
var trash: u64 = std.math.maxInt(u64);
const len = switch (r.transfer_encoding) {
.content_length => |*len| len,
else => &trash,
};
if (r.elide_body) {
len.* -= bytes.len;
return bytes.len;
}
if (bytes.len + r.send_buffer_end > r.send_buffer.len) {
const send_buffer_len = r.send_buffer_end - r.send_buffer_start;
var iovecs: [2]std.posix.iovec_const = .{
.{
.base = r.send_buffer.ptr + r.send_buffer_start,
.len = send_buffer_len,
},
.{
.base = bytes.ptr,
.len = bytes.len,
},
};
const n = try r.stream.writev(&iovecs);
if (n >= send_buffer_len) {
// It was enough to reset the buffer.
r.send_buffer_start = 0;
r.send_buffer_end = 0;
const bytes_n = n - send_buffer_len;
len.* -= bytes_n;
return bytes_n;
}
// It didn't even make it through the existing buffer, let
// alone the new bytes provided.
r.send_buffer_start += n;
return 0;
}
// All bytes can be stored in the remaining space of the buffer.
@memcpy(r.send_buffer[r.send_buffer_end..][0..bytes.len], bytes);
r.send_buffer_end += bytes.len;
len.* -= bytes.len;
return bytes.len;
}
fn write_chunked(context: *const anyopaque, bytes: []const u8) WriteError!usize {
const r: *Response = @constCast(@alignCast(@ptrCast(context)));
assert(r.transfer_encoding == .chunked);
if (r.elide_body)
return bytes.len;
if (bytes.len + r.send_buffer_end > r.send_buffer.len) {
const send_buffer_len = r.send_buffer_end - r.send_buffer_start;
const chunk_len = r.chunk_len + bytes.len;
var header_buf: [18]u8 = undefined;
const chunk_header = std.fmt.bufPrint(&header_buf, "{x}\r\n", .{chunk_len}) catch unreachable;
var iovecs: [5]std.posix.iovec_const = .{
.{
.base = r.send_buffer.ptr + r.send_buffer_start,
.len = send_buffer_len - r.chunk_len,
},
.{
.base = chunk_header.ptr,
.len = chunk_header.len,
},
.{
.base = r.send_buffer.ptr + r.send_buffer_end - r.chunk_len,
.len = r.chunk_len,
},
.{
.base = bytes.ptr,
.len = bytes.len,
},
.{
.base = "\r\n",
.len = 2,
},
};
// TODO make this writev instead of writevAll, which involves
// complicating the logic of this function.
try r.stream.writevAll(&iovecs);
r.send_buffer_start = 0;
r.send_buffer_end = 0;
r.chunk_len = 0;
return bytes.len;
}
// All bytes can be stored in the remaining space of the buffer.
@memcpy(r.send_buffer[r.send_buffer_end..][0..bytes.len], bytes);
r.send_buffer_end += bytes.len;
r.chunk_len += bytes.len;
return bytes.len;
}
/// If using content-length, asserts that writing these bytes to the client
/// would not exceed the content-length value sent in the HTTP header.
pub fn writeAll(r: *Response, bytes: []const u8) WriteError!void {
var index: usize = 0;
while (index < bytes.len) {
index += try write(r, bytes[index..]);
}
}
/// Sends all buffered data to the client.
/// This is redundant after calling `end`.
/// Respects the value of `elide_body` to omit all data after the headers.
pub fn flush(r: *Response) WriteError!void {
switch (r.transfer_encoding) {
.none, .content_length => return flush_cl(r),
.chunked => return flush_chunked(r, null),
}
}
fn flush_cl(r: *Response) WriteError!void {
try r.stream.writeAll(r.send_buffer[r.send_buffer_start..r.send_buffer_end]);
r.send_buffer_start = 0;
r.send_buffer_end = 0;
}
fn flush_chunked(r: *Response, end_trailers: ?[]const http.Header) WriteError!void {
const max_trailers = 25;
if (end_trailers) |trailers| assert(trailers.len <= max_trailers);
assert(r.transfer_encoding == .chunked);
const http_headers = r.send_buffer[r.send_buffer_start .. r.send_buffer_end - r.chunk_len];
if (r.elide_body) {
try r.stream.writeAll(http_headers);
r.send_buffer_start = 0;
r.send_buffer_end = 0;
r.chunk_len = 0;
return;
}
var header_buf: [18]u8 = undefined;
const chunk_header = std.fmt.bufPrint(&header_buf, "{x}\r\n", .{r.chunk_len}) catch unreachable;
var iovecs: [max_trailers * 4 + 5]std.posix.iovec_const = undefined;
var iovecs_len: usize = 0;
iovecs[iovecs_len] = .{
.base = http_headers.ptr,
.len = http_headers.len,
};
iovecs_len += 1;
if (r.chunk_len > 0) {
iovecs[iovecs_len] = .{
.base = chunk_header.ptr,
.len = chunk_header.len,
};
iovecs_len += 1;
iovecs[iovecs_len] = .{
.base = r.send_buffer.ptr + r.send_buffer_end - r.chunk_len,
.len = r.chunk_len,
};
iovecs_len += 1;
iovecs[iovecs_len] = .{
.base = "\r\n",
.len = 2,
};
iovecs_len += 1;
}
if (end_trailers) |trailers| {
iovecs[iovecs_len] = .{
.base = "0\r\n",
.len = 3,
};
iovecs_len += 1;
for (trailers) |trailer| {
iovecs[iovecs_len] = .{
.base = trailer.name.ptr,
.len = trailer.name.len,
};
iovecs_len += 1;
iovecs[iovecs_len] = .{
.base = ": ",
.len = 2,
};
iovecs_len += 1;
if (trailer.value.len != 0) {
iovecs[iovecs_len] = .{
.base = trailer.value.ptr,
.len = trailer.value.len,
};
iovecs_len += 1;
}
iovecs[iovecs_len] = .{
.base = "\r\n",
.len = 2,
};
iovecs_len += 1;
}
iovecs[iovecs_len] = .{
.base = "\r\n",
.len = 2,
};
iovecs_len += 1;
}
try r.stream.writevAll(iovecs[0..iovecs_len]);
r.send_buffer_start = 0;
r.send_buffer_end = 0;
r.chunk_len = 0;
}
pub fn writer(r: *Response) std.io.AnyWriter {
return .{
.writeFn = switch (r.transfer_encoding) {
.none, .content_length => write_cl,
.chunked => write_chunked,
},
.context = r,
};
}
};
fn rebase(s: *Server, index: usize) void {
const leftover = s.read_buffer[s.next_request_start..s.read_buffer_len];
const dest = s.read_buffer[index..][0..leftover.len];
if (leftover.len <= s.next_request_start - index) {
@memcpy(dest, leftover);
} else {
mem.copyBackwards(u8, dest, leftover);
}
s.read_buffer_len = index + leftover.len;
}
const std = @import("../std.zig");
const http = std.http;
const mem = std.mem;
const net = std.net;
const Uri = std.Uri;
const assert = std.debug.assert;
const testing = std.testing;
const Server = @This();