mirror of
https://github.com/ziglang/zig.git
synced 2025-12-07 23:03:08 +00:00
* Uncouple std.http.ChunkParser from protocol.zig * Fix receiveHead not passing leftover buffer through the header parser. * Fix content-length read streaming This implementation handles the final chunk length correctly rather than "hoping" that the buffer already contains \r\n.
320 lines
11 KiB
Zig
320 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 ChunkParser = @import("http/ChunkParser.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;
|
|
_ = ChunkParser;
|
|
_ = @import("http/test.zig");
|
|
}
|