mirror of
https://github.com/ziglang/zig.git
synced 2025-12-16 03:03:09 +00:00
Mainly, this removes the poorly named `wait`, `send`, `finish`
functions, which all operated on the same "Response" object, which was
actually being used as the request.
Now, it looks like this:
1. std.net.Server.accept() gives you a std.net.Server.Connection
2. std.http.Server.init() with the connection
3. Server.receiveHead() gives you a Request
4. Request.reader() gives you a body reader
5. Request.respond() is a one-shot, or Request.respondStreaming() creates
a Response
6. Response.writer() gives you a body writer
7. Response.end() finishes the response; Response.endChunked() allows
passing response trailers.
In other words, the type system now guides the API user down the correct
path.
receiveHead allows extra bytes to be read into the read buffer, and then
will reuse those bytes for the body or the next request upon connection
reuse.
respond(), the one-shot function, will send the entire response in one
syscall.
Streaming response bodies no longer wastefully wraps every call to write
with a chunk header and trailer; instead it only sends the HTTP chunk
wrapper when flushing. This means the user can still control when it
happens but it also does not add unnecessary chunks.
Empirically, in my example project that uses this API, the usage code is
significantly less noisy, it has less error handling while handling
errors more correctly, it's more obvious what is happening, and it is
syscall-optimal.
Additionally:
* Uncouple std.http.HeadParser from protocol.zig
* Delete std.Server.Connection; use std.net.Server.Connection instead.
- The API user supplies the read buffer when initializing the
http.Server, and it is used for the HTTP head as well as a buffer
for reading the body into.
* Replace and document the State enum. No longer is there both "start"
and "first".
318 lines
11 KiB
Zig
318 lines
11 KiB
Zig
const std = @import("std.zig");
|
|
|
|
pub const Client = @import("http/Client.zig");
|
|
pub const Server = @import("http/Server.zig");
|
|
pub const protocol = @import("http/protocol.zig");
|
|
pub const HeadParser = @import("http/HeadParser.zig");
|
|
|
|
pub const Version = enum {
|
|
@"HTTP/1.0",
|
|
@"HTTP/1.1",
|
|
};
|
|
|
|
/// https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
|
|
///
|
|
/// https://datatracker.ietf.org/doc/html/rfc7231#section-4 Initial definition
|
|
///
|
|
/// https://datatracker.ietf.org/doc/html/rfc5789#section-2 PATCH
|
|
pub const Method = enum(u64) {
|
|
GET = parse("GET"),
|
|
HEAD = parse("HEAD"),
|
|
POST = parse("POST"),
|
|
PUT = parse("PUT"),
|
|
DELETE = parse("DELETE"),
|
|
CONNECT = parse("CONNECT"),
|
|
OPTIONS = parse("OPTIONS"),
|
|
TRACE = parse("TRACE"),
|
|
PATCH = parse("PATCH"),
|
|
|
|
_,
|
|
|
|
/// Converts `s` into a type that may be used as a `Method` field.
|
|
/// Asserts that `s` is 24 or fewer bytes.
|
|
pub fn parse(s: []const u8) u64 {
|
|
var x: u64 = 0;
|
|
const len = @min(s.len, @sizeOf(@TypeOf(x)));
|
|
@memcpy(std.mem.asBytes(&x)[0..len], s[0..len]);
|
|
return x;
|
|
}
|
|
|
|
pub fn write(self: Method, w: anytype) !void {
|
|
const bytes = std.mem.asBytes(&@intFromEnum(self));
|
|
const str = std.mem.sliceTo(bytes, 0);
|
|
try w.writeAll(str);
|
|
}
|
|
|
|
/// Returns true if a request of this method is allowed to have a body
|
|
/// Actual behavior from servers may vary and should still be checked
|
|
pub fn requestHasBody(self: Method) bool {
|
|
return switch (self) {
|
|
.POST, .PUT, .PATCH => true,
|
|
.GET, .HEAD, .DELETE, .CONNECT, .OPTIONS, .TRACE => false,
|
|
else => true,
|
|
};
|
|
}
|
|
|
|
/// Returns true if a response to this method is allowed to have a body
|
|
/// Actual behavior from clients may vary and should still be checked
|
|
pub fn responseHasBody(self: Method) bool {
|
|
return switch (self) {
|
|
.GET, .POST, .DELETE, .CONNECT, .OPTIONS, .PATCH => true,
|
|
.HEAD, .PUT, .TRACE => false,
|
|
else => true,
|
|
};
|
|
}
|
|
|
|
/// An HTTP method is safe if it doesn't alter the state of the server.
|
|
///
|
|
/// https://developer.mozilla.org/en-US/docs/Glossary/Safe/HTTP
|
|
///
|
|
/// https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.1
|
|
pub fn safe(self: Method) bool {
|
|
return switch (self) {
|
|
.GET, .HEAD, .OPTIONS, .TRACE => true,
|
|
.POST, .PUT, .DELETE, .CONNECT, .PATCH => false,
|
|
else => false,
|
|
};
|
|
}
|
|
|
|
/// An HTTP method is idempotent if an identical request can be made once or several times in a row with the same effect while leaving the server in the same state.
|
|
///
|
|
/// https://developer.mozilla.org/en-US/docs/Glossary/Idempotent
|
|
///
|
|
/// https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.2
|
|
pub fn idempotent(self: Method) bool {
|
|
return switch (self) {
|
|
.GET, .HEAD, .PUT, .DELETE, .OPTIONS, .TRACE => true,
|
|
.CONNECT, .POST, .PATCH => false,
|
|
else => false,
|
|
};
|
|
}
|
|
|
|
/// A cacheable response is an HTTP response that can be cached, that is stored to be retrieved and used later, saving a new request to the server.
|
|
///
|
|
/// https://developer.mozilla.org/en-US/docs/Glossary/cacheable
|
|
///
|
|
/// https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.3
|
|
pub fn cacheable(self: Method) bool {
|
|
return switch (self) {
|
|
.GET, .HEAD => true,
|
|
.POST, .PUT, .DELETE, .CONNECT, .OPTIONS, .TRACE, .PATCH => false,
|
|
else => false,
|
|
};
|
|
}
|
|
};
|
|
|
|
/// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
|
|
pub const Status = enum(u10) {
|
|
@"continue" = 100, // RFC7231, Section 6.2.1
|
|
switching_protocols = 101, // RFC7231, Section 6.2.2
|
|
processing = 102, // RFC2518
|
|
early_hints = 103, // RFC8297
|
|
|
|
ok = 200, // RFC7231, Section 6.3.1
|
|
created = 201, // RFC7231, Section 6.3.2
|
|
accepted = 202, // RFC7231, Section 6.3.3
|
|
non_authoritative_info = 203, // RFC7231, Section 6.3.4
|
|
no_content = 204, // RFC7231, Section 6.3.5
|
|
reset_content = 205, // RFC7231, Section 6.3.6
|
|
partial_content = 206, // RFC7233, Section 4.1
|
|
multi_status = 207, // RFC4918
|
|
already_reported = 208, // RFC5842
|
|
im_used = 226, // RFC3229
|
|
|
|
multiple_choice = 300, // RFC7231, Section 6.4.1
|
|
moved_permanently = 301, // RFC7231, Section 6.4.2
|
|
found = 302, // RFC7231, Section 6.4.3
|
|
see_other = 303, // RFC7231, Section 6.4.4
|
|
not_modified = 304, // RFC7232, Section 4.1
|
|
use_proxy = 305, // RFC7231, Section 6.4.5
|
|
temporary_redirect = 307, // RFC7231, Section 6.4.7
|
|
permanent_redirect = 308, // RFC7538
|
|
|
|
bad_request = 400, // RFC7231, Section 6.5.1
|
|
unauthorized = 401, // RFC7235, Section 3.1
|
|
payment_required = 402, // RFC7231, Section 6.5.2
|
|
forbidden = 403, // RFC7231, Section 6.5.3
|
|
not_found = 404, // RFC7231, Section 6.5.4
|
|
method_not_allowed = 405, // RFC7231, Section 6.5.5
|
|
not_acceptable = 406, // RFC7231, Section 6.5.6
|
|
proxy_auth_required = 407, // RFC7235, Section 3.2
|
|
request_timeout = 408, // RFC7231, Section 6.5.7
|
|
conflict = 409, // RFC7231, Section 6.5.8
|
|
gone = 410, // RFC7231, Section 6.5.9
|
|
length_required = 411, // RFC7231, Section 6.5.10
|
|
precondition_failed = 412, // RFC7232, Section 4.2][RFC8144, Section 3.2
|
|
payload_too_large = 413, // RFC7231, Section 6.5.11
|
|
uri_too_long = 414, // RFC7231, Section 6.5.12
|
|
unsupported_media_type = 415, // RFC7231, Section 6.5.13][RFC7694, Section 3
|
|
range_not_satisfiable = 416, // RFC7233, Section 4.4
|
|
expectation_failed = 417, // RFC7231, Section 6.5.14
|
|
teapot = 418, // RFC 7168, 2.3.3
|
|
misdirected_request = 421, // RFC7540, Section 9.1.2
|
|
unprocessable_entity = 422, // RFC4918
|
|
locked = 423, // RFC4918
|
|
failed_dependency = 424, // RFC4918
|
|
too_early = 425, // RFC8470
|
|
upgrade_required = 426, // RFC7231, Section 6.5.15
|
|
precondition_required = 428, // RFC6585
|
|
too_many_requests = 429, // RFC6585
|
|
request_header_fields_too_large = 431, // RFC6585
|
|
unavailable_for_legal_reasons = 451, // RFC7725
|
|
|
|
internal_server_error = 500, // RFC7231, Section 6.6.1
|
|
not_implemented = 501, // RFC7231, Section 6.6.2
|
|
bad_gateway = 502, // RFC7231, Section 6.6.3
|
|
service_unavailable = 503, // RFC7231, Section 6.6.4
|
|
gateway_timeout = 504, // RFC7231, Section 6.6.5
|
|
http_version_not_supported = 505, // RFC7231, Section 6.6.6
|
|
variant_also_negotiates = 506, // RFC2295
|
|
insufficient_storage = 507, // RFC4918
|
|
loop_detected = 508, // RFC5842
|
|
not_extended = 510, // RFC2774
|
|
network_authentication_required = 511, // RFC6585
|
|
|
|
_,
|
|
|
|
pub fn phrase(self: Status) ?[]const u8 {
|
|
return switch (self) {
|
|
// 1xx statuses
|
|
.@"continue" => "Continue",
|
|
.switching_protocols => "Switching Protocols",
|
|
.processing => "Processing",
|
|
.early_hints => "Early Hints",
|
|
|
|
// 2xx statuses
|
|
.ok => "OK",
|
|
.created => "Created",
|
|
.accepted => "Accepted",
|
|
.non_authoritative_info => "Non-Authoritative Information",
|
|
.no_content => "No Content",
|
|
.reset_content => "Reset Content",
|
|
.partial_content => "Partial Content",
|
|
.multi_status => "Multi-Status",
|
|
.already_reported => "Already Reported",
|
|
.im_used => "IM Used",
|
|
|
|
// 3xx statuses
|
|
.multiple_choice => "Multiple Choice",
|
|
.moved_permanently => "Moved Permanently",
|
|
.found => "Found",
|
|
.see_other => "See Other",
|
|
.not_modified => "Not Modified",
|
|
.use_proxy => "Use Proxy",
|
|
.temporary_redirect => "Temporary Redirect",
|
|
.permanent_redirect => "Permanent Redirect",
|
|
|
|
// 4xx statuses
|
|
.bad_request => "Bad Request",
|
|
.unauthorized => "Unauthorized",
|
|
.payment_required => "Payment Required",
|
|
.forbidden => "Forbidden",
|
|
.not_found => "Not Found",
|
|
.method_not_allowed => "Method Not Allowed",
|
|
.not_acceptable => "Not Acceptable",
|
|
.proxy_auth_required => "Proxy Authentication Required",
|
|
.request_timeout => "Request Timeout",
|
|
.conflict => "Conflict",
|
|
.gone => "Gone",
|
|
.length_required => "Length Required",
|
|
.precondition_failed => "Precondition Failed",
|
|
.payload_too_large => "Payload Too Large",
|
|
.uri_too_long => "URI Too Long",
|
|
.unsupported_media_type => "Unsupported Media Type",
|
|
.range_not_satisfiable => "Range Not Satisfiable",
|
|
.expectation_failed => "Expectation Failed",
|
|
.teapot => "I'm a teapot",
|
|
.misdirected_request => "Misdirected Request",
|
|
.unprocessable_entity => "Unprocessable Entity",
|
|
.locked => "Locked",
|
|
.failed_dependency => "Failed Dependency",
|
|
.too_early => "Too Early",
|
|
.upgrade_required => "Upgrade Required",
|
|
.precondition_required => "Precondition Required",
|
|
.too_many_requests => "Too Many Requests",
|
|
.request_header_fields_too_large => "Request Header Fields Too Large",
|
|
.unavailable_for_legal_reasons => "Unavailable For Legal Reasons",
|
|
|
|
// 5xx statuses
|
|
.internal_server_error => "Internal Server Error",
|
|
.not_implemented => "Not Implemented",
|
|
.bad_gateway => "Bad Gateway",
|
|
.service_unavailable => "Service Unavailable",
|
|
.gateway_timeout => "Gateway Timeout",
|
|
.http_version_not_supported => "HTTP Version Not Supported",
|
|
.variant_also_negotiates => "Variant Also Negotiates",
|
|
.insufficient_storage => "Insufficient Storage",
|
|
.loop_detected => "Loop Detected",
|
|
.not_extended => "Not Extended",
|
|
.network_authentication_required => "Network Authentication Required",
|
|
|
|
else => return null,
|
|
};
|
|
}
|
|
|
|
pub const Class = enum {
|
|
informational,
|
|
success,
|
|
redirect,
|
|
client_error,
|
|
server_error,
|
|
};
|
|
|
|
pub fn class(self: Status) Class {
|
|
return switch (@intFromEnum(self)) {
|
|
100...199 => .informational,
|
|
200...299 => .success,
|
|
300...399 => .redirect,
|
|
400...499 => .client_error,
|
|
else => .server_error,
|
|
};
|
|
}
|
|
|
|
test {
|
|
try std.testing.expectEqualStrings("OK", Status.ok.phrase().?);
|
|
try std.testing.expectEqualStrings("Not Found", Status.not_found.phrase().?);
|
|
}
|
|
|
|
test {
|
|
try std.testing.expectEqual(Status.Class.success, Status.ok.class());
|
|
try std.testing.expectEqual(Status.Class.client_error, Status.not_found.class());
|
|
}
|
|
};
|
|
|
|
pub const TransferEncoding = enum {
|
|
chunked,
|
|
none,
|
|
// compression is intentionally omitted here, as std.http.Client stores it as content-encoding
|
|
};
|
|
|
|
pub const ContentEncoding = enum {
|
|
identity,
|
|
compress,
|
|
@"x-compress",
|
|
deflate,
|
|
gzip,
|
|
@"x-gzip",
|
|
zstd,
|
|
};
|
|
|
|
pub const Connection = enum {
|
|
keep_alive,
|
|
close,
|
|
};
|
|
|
|
pub const Header = struct {
|
|
name: []const u8,
|
|
value: []const u8,
|
|
};
|
|
|
|
test {
|
|
_ = Client;
|
|
_ = Method;
|
|
_ = Server;
|
|
_ = Status;
|
|
_ = HeadParser;
|
|
_ = @import("http/test.zig");
|
|
}
|