╭ http.zig ─────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭ find file ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╮ ╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │ │ │http│ ssh │ │ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╯ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const builtin = @import("builtin"); 2 const std = @import("std.zig"); 3 const assert = std.debug.assert; 4 const Writer = std.Io.Writer; 5 const File = std.Io.File; 6 7 pub const Client = @import("http/Client.zig"); 8 pub const Server = @import("http/Server.zig"); 9 pub const HeadParser = @import("http/HeadParser.zig"); 10 pub const ChunkParser = @import("http/ChunkParser.zig"); 11 pub const HeaderIterator = @import("http/HeaderIterator.zig"); 12 13 pub const Version = enum { 14 @"HTTP/1.0", 15 @"HTTP/1.1", 16 }; 17 18 /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods 19 /// 20 /// https://datatracker.ietf.org/doc/html/rfc7231#section-4 Initial definition 21 /// 22 /// https://datatracker.ietf.org/doc/html/rfc5789#section-2 PATCH 23 /// 24 /// https://datatracker.ietf.org/doc/html/rfc10008#name-query-method QUERY 25 pub const Method = enum { 26 GET, 27 HEAD, 28 POST, 29 PUT, 30 DELETE, 31 CONNECT, 32 OPTIONS, 33 TRACE, 34 PATCH, 35 QUERY, 36 37 /// Returns true if a request of this method is allowed to have a body 38 /// Actual behavior from servers may vary and should still be checked 39 pub fn requestHasBody(m: Method) bool { 40 return switch (m) { 41 .POST, .PUT, .PATCH, .QUERY => true, 42 .GET, .HEAD, .DELETE, .CONNECT, .OPTIONS, .TRACE => false, 43 }; 44 } 45 46 /// Returns true if a response to this method is allowed to have a body 47 /// Actual behavior from clients may vary and should still be checked 48 pub fn responseHasBody(m: Method) bool { 49 return switch (m) { 50 .GET, .POST, .PUT, .DELETE, .CONNECT, .OPTIONS, .PATCH, .QUERY => true, 51 .HEAD, .TRACE => false, 52 }; 53 } 54 55 /// An HTTP method is safe if it doesn't alter the state of the server. 56 /// 57 /// https://developer.mozilla.org/en-US/docs/Glossary/Safe/HTTP 58 /// 59 /// https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.1 60 pub fn safe(m: Method) bool { 61 return switch (m) { 62 .GET, .HEAD, .OPTIONS, .TRACE, .QUERY => true, 63 .POST, .PUT, .DELETE, .CONNECT, .PATCH => false, 64 }; 65 } 66 67 /// An HTTP method is idempotent if an identical request can be made once 68 /// or several times in a row with the same effect while leaving the server 69 /// in the same state. 70 /// 71 /// https://developer.mozilla.org/en-US/docs/Glossary/Idempotent 72 /// 73 /// https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.2 74 pub fn idempotent(m: Method) bool { 75 return switch (m) { 76 .GET, .HEAD, .PUT, .DELETE, .OPTIONS, .TRACE, .QUERY => true, 77 .CONNECT, .POST, .PATCH => false, 78 }; 79 } 80 81 /// A cacheable response can be stored to be retrieved and used later, 82 /// saving a new request to the server. 83 /// 84 /// https://developer.mozilla.org/en-US/docs/Glossary/cacheable 85 /// 86 /// https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.3 87 pub fn cacheable(m: Method) bool { 88 return switch (m) { 89 .GET, .HEAD, .QUERY => true, 90 .POST, .PUT, .DELETE, .CONNECT, .OPTIONS, .TRACE, .PATCH => false, 91 }; 92 } 93 }; 94 95 /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status 96 pub const Status = enum(u10) { 97 @"continue" = 100, // RFC7231, Section 6.2.1 98 switching_protocols = 101, // RFC7231, Section 6.2.2 99 processing = 102, // RFC2518 100 early_hints = 103, // RFC8297 101 102 ok = 200, // RFC7231, Section 6.3.1 103 created = 201, // RFC7231, Section 6.3.2 104 accepted = 202, // RFC7231, Section 6.3.3 105 non_authoritative_info = 203, // RFC7231, Section 6.3.4 106 no_content = 204, // RFC7231, Section 6.3.5 107 reset_content = 205, // RFC7231, Section 6.3.6 108 partial_content = 206, // RFC7233, Section 4.1 109 multi_status = 207, // RFC4918 110 already_reported = 208, // RFC5842 111 im_used = 226, // RFC3229 112 113 multiple_choice = 300, // RFC7231, Section 6.4.1 114 moved_permanently = 301, // RFC7231, Section 6.4.2 115 found = 302, // RFC7231, Section 6.4.3 116 see_other = 303, // RFC7231, Section 6.4.4 117 not_modified = 304, // RFC7232, Section 4.1 118 use_proxy = 305, // RFC7231, Section 6.4.5 119 temporary_redirect = 307, // RFC7231, Section 6.4.7 120 permanent_redirect = 308, // RFC7538 121 122 bad_request = 400, // RFC7231, Section 6.5.1 123 unauthorized = 401, // RFC7235, Section 3.1 124 payment_required = 402, // RFC7231, Section 6.5.2 125 forbidden = 403, // RFC7231, Section 6.5.3 126 not_found = 404, // RFC7231, Section 6.5.4 127 method_not_allowed = 405, // RFC7231, Section 6.5.5 128 not_acceptable = 406, // RFC7231, Section 6.5.6 129 proxy_auth_required = 407, // RFC7235, Section 3.2 130 request_timeout = 408, // RFC7231, Section 6.5.7 131 conflict = 409, // RFC7231, Section 6.5.8 132 gone = 410, // RFC7231, Section 6.5.9 133 length_required = 411, // RFC7231, Section 6.5.10 134 precondition_failed = 412, // RFC7232, Section 4.2][RFC8144, Section 3.2 135 payload_too_large = 413, // RFC7231, Section 6.5.11 136 uri_too_long = 414, // RFC7231, Section 6.5.12 137 unsupported_media_type = 415, // RFC7231, Section 6.5.13][RFC7694, Section 3 138 range_not_satisfiable = 416, // RFC7233, Section 4.4 139 expectation_failed = 417, // RFC7231, Section 6.5.14 140 teapot = 418, // RFC 7168, 2.3.3 141 misdirected_request = 421, // RFC7540, Section 9.1.2 142 unprocessable_entity = 422, // RFC4918 143 locked = 423, // RFC4918 144 failed_dependency = 424, // RFC4918 145 too_early = 425, // RFC8470 146 upgrade_required = 426, // RFC7231, Section 6.5.15 147 precondition_required = 428, // RFC6585 148 too_many_requests = 429, // RFC6585 149 request_header_fields_too_large = 431, // RFC6585 150 unavailable_for_legal_reasons = 451, // RFC7725 151 152 internal_server_error = 500, // RFC7231, Section 6.6.1 153 not_implemented = 501, // RFC7231, Section 6.6.2 154 bad_gateway = 502, // RFC7231, Section 6.6.3 155 service_unavailable = 503, // RFC7231, Section 6.6.4 156 gateway_timeout = 504, // RFC7231, Section 6.6.5 157 http_version_not_supported = 505, // RFC7231, Section 6.6.6 158 variant_also_negotiates = 506, // RFC2295 159 insufficient_storage = 507, // RFC4918 160 loop_detected = 508, // RFC5842 161 not_extended = 510, // RFC2774 162 network_authentication_required = 511, // RFC6585 163 164 _, 165 166 pub fn phrase(self: Status) ?[]const u8 { 167 return switch (self) { 168 // 1xx statuses 169 .@"continue" => "Continue", 170 .switching_protocols => "Switching Protocols", 171 .processing => "Processing", 172 .early_hints => "Early Hints", 173 174 // 2xx statuses 175 .ok => "OK", 176 .created => "Created", 177 .accepted => "Accepted", 178 .non_authoritative_info => "Non-Authoritative Information", 179 .no_content => "No Content", 180 .reset_content => "Reset Content", 181 .partial_content => "Partial Content", 182 .multi_status => "Multi-Status", 183 .already_reported => "Already Reported", 184 .im_used => "IM Used", 185 186 // 3xx statuses 187 .multiple_choice => "Multiple Choice", 188 .moved_permanently => "Moved Permanently", 189 .found => "Found", 190 .see_other => "See Other", 191 .not_modified => "Not Modified", 192 .use_proxy => "Use Proxy", 193 .temporary_redirect => "Temporary Redirect", 194 .permanent_redirect => "Permanent Redirect", 195 196 // 4xx statuses 197 .bad_request => "Bad Request", 198 .unauthorized => "Unauthorized", 199 .payment_required => "Payment Required", 200 .forbidden => "Forbidden", 201 .not_found => "Not Found", 202 .method_not_allowed => "Method Not Allowed", 203 .not_acceptable => "Not Acceptable", 204 .proxy_auth_required => "Proxy Authentication Required", 205 .request_timeout => "Request Timeout", 206 .conflict => "Conflict", 207 .gone => "Gone", 208 .length_required => "Length Required", 209 .precondition_failed => "Precondition Failed", 210 .payload_too_large => "Payload Too Large", 211 .uri_too_long => "URI Too Long", 212 .unsupported_media_type => "Unsupported Media Type", 213 .range_not_satisfiable => "Range Not Satisfiable", 214 .expectation_failed => "Expectation Failed", 215 .teapot => "I'm a teapot", 216 .misdirected_request => "Misdirected Request", 217 .unprocessable_entity => "Unprocessable Entity", 218 .locked => "Locked", 219 .failed_dependency => "Failed Dependency", 220 .too_early => "Too Early", 221 .upgrade_required => "Upgrade Required", 222 .precondition_required => "Precondition Required", 223 .too_many_requests => "Too Many Requests", 224 .request_header_fields_too_large => "Request Header Fields Too Large", 225 .unavailable_for_legal_reasons => "Unavailable For Legal Reasons", 226 227 // 5xx statuses 228 .internal_server_error => "Internal Server Error", 229 .not_implemented => "Not Implemented", 230 .bad_gateway => "Bad Gateway", 231 .service_unavailable => "Service Unavailable", 232 .gateway_timeout => "Gateway Timeout", 233 .http_version_not_supported => "HTTP Version Not Supported", 234 .variant_also_negotiates => "Variant Also Negotiates", 235 .insufficient_storage => "Insufficient Storage", 236 .loop_detected => "Loop Detected", 237 .not_extended => "Not Extended", 238 .network_authentication_required => "Network Authentication Required", 239 240 else => return null, 241 }; 242 } 243 244 pub const Class = enum { 245 informational, 246 success, 247 redirect, 248 client_error, 249 server_error, 250 }; 251 252 pub fn class(self: Status) Class { 253 return switch (@backingInt(self)) { 254 100...199 => .informational, 255 200...299 => .success, 256 300...399 => .redirect, 257 400...499 => .client_error, 258 else => .server_error, 259 }; 260 } 261 262 test { 263 try std.testing.expectEqualStrings("OK", Status.ok.phrase().?); 264 try std.testing.expectEqualStrings("Not Found", Status.not_found.phrase().?); 265 } 266 267 test { 268 try std.testing.expectEqual(Status.Class.success, Status.ok.class()); 269 try std.testing.expectEqual(Status.Class.client_error, Status.not_found.class()); 270 } 271 }; 272 273 /// compression is intentionally omitted here since it is handled in `ContentEncoding`. 274 pub const TransferEncoding = enum { 275 chunked, 276 none, 277 }; 278 279 pub const ContentEncoding = enum { 280 zstd, 281 gzip, 282 deflate, 283 compress, 284 identity, 285 286 pub fn fromString(s: []const u8) ?ContentEncoding { 287 const map = std.StaticStringMap(ContentEncoding).initComptime(.{ 288 .{ "zstd", .zstd }, 289 .{ "gzip", .gzip }, 290 .{ "x-gzip", .gzip }, 291 .{ "deflate", .deflate }, 292 .{ "compress", .compress }, 293 .{ "x-compress", .compress }, 294 .{ "identity", .identity }, 295 }); 296 return map.get(s); 297 } 298 299 pub fn minBufferCapacity(ce: ContentEncoding) usize { 300 return switch (ce) { 301 .zstd => std.compress.zstd.default_window_len, 302 .gzip, .deflate => std.compress.flate.max_window_len, 303 .compress, .identity => 0, 304 }; 305 } 306 }; 307 308 pub const Connection = enum { 309 keep_alive, 310 close, 311 }; 312 313 pub const Header = struct { 314 name: []const u8, 315 value: []const u8, 316 }; 317 318 pub const Reader = struct { 319 in: *std.Io.Reader, 320 /// This is preallocated memory that might be used by `bodyReader`. That 321 /// function might return a pointer to this field, or a different 322 /// `*std.Io.Reader`. Advisable to not access this field directly. 323 interface: std.Io.Reader, 324 /// Keeps track of whether the stream is ready to accept a new request, 325 /// making invalid API usage cause assertion failures rather than HTTP 326 /// protocol violations. 327 state: State, 328 /// HTTP trailer bytes. These are at the end of a transfer-encoding: 329 /// chunked message. This data is available only after calling one of the 330 /// "end" functions and points to data inside the buffer of `in`, and is 331 /// therefore invalidated on the next call to `receiveHead`, or any other 332 /// read from `in`. 333 trailers: []const u8 = &.{}, 334 body_err: ?BodyError = null, 335 max_head_len: usize, 336 337 pub const RemainingChunkLen = enum(u64) { 338 head = 0, 339 n = 1, 340 rn = 2, 341 _, 342 343 pub fn init(integer: u64) RemainingChunkLen { 344 return @fromBackingInt(@intCast(integer)); 345 } 346 347 pub fn int(rcl: RemainingChunkLen) u64 { 348 return @backingInt(rcl); 349 } 350 }; 351 352 pub const State = union(enum) { 353 /// The stream is available to be used for the first time, or reused. 354 ready, 355 received_head, 356 /// The stream goes until the connection is closed. 357 body_none, 358 body_remaining_content_length: u64, 359 body_remaining_chunk_len: RemainingChunkLen, 360 /// The stream would be eligible for another HTTP request, however the 361 /// client and server did not negotiate a persistent connection. 362 closing, 363 }; 364 365 pub const BodyError = error{ 366 HttpChunkInvalid, 367 HttpChunkTruncated, 368 HttpHeadersOversize, 369 }; 370 371 pub const HeadError = error{ 372 /// Too many bytes of HTTP headers. 373 /// 374 /// The HTTP specification suggests to respond with a 431 status code 375 /// before closing the connection. 376 HttpHeadersOversize, 377 /// Partial HTTP request was received but the connection was closed 378 /// before fully receiving the headers. 379 HttpRequestTruncated, 380 /// The client sent 0 bytes of headers before closing the stream. This 381 /// happens when a keep-alive connection is finally closed. 382 HttpConnectionClosing, 383 /// Transitive error occurred reading from `in`. 384 ReadFailed, 385 }; 386 387 /// Buffers the entire head inside `in`. 388 /// 389 /// The resulting memory is invalidated by any subsequent consumption of 390 /// the input stream. 391 pub fn receiveHead(reader: *Reader) HeadError![]const u8 { 392 reader.trailers = &.{}; 393 const in = reader.in; 394 const max_head_len = reader.max_head_len; 395 var hp: HeadParser = .{}; 396 var head_len: usize = 0; 397 while (true) { 398 if (head_len >= max_head_len) return error.HttpHeadersOversize; 399 const remaining = in.buffered()[head_len..]; 400 if (remaining.len == 0) { 401 in.fillMore() catch |err| switch (err) { 402 error.EndOfStream => switch (head_len) { 403 0 => return error.HttpConnectionClosing, 404 else => return error.HttpRequestTruncated, 405 }, 406 error.ReadFailed => |e| return e, 407 }; 408 continue; 409 } 410 head_len += hp.feed(remaining); 411 if (hp.state == .finished) { 412 reader.state = .received_head; 413 const head_buffer = in.buffered()[0..head_len]; 414 in.toss(head_len); 415 return head_buffer; 416 } 417 } 418 } 419 420 /// If compressed body has been negotiated this will return compressed bytes. 421 /// 422 /// Asserts only called once and after `receiveHead`. 423 /// 424 /// See also: 425 /// * `interfaceDecompressing` 426 pub fn bodyReader( 427 reader: *Reader, 428 transfer_buffer: []u8, 429 transfer_encoding: TransferEncoding, 430 content_length: ?u64, 431 ) *std.Io.Reader { 432 assert(reader.state == .received_head); 433 switch (transfer_encoding) { 434 .chunked => { 435 reader.state = .{ .body_remaining_chunk_len = .head }; 436 reader.interface = .{ 437 .buffer = transfer_buffer, 438 .seek = 0, 439 .end = 0, 440 .vtable = &.{ 441 .stream = chunkedStream, 442 .discard = chunkedDiscard, 443 }, 444 }; 445 return &reader.interface; 446 }, 447 .none => { 448 if (content_length) |len| { 449 reader.state = if (len == 0) .ready else .{ .body_remaining_content_length = len }; 450 reader.interface = .{ 451 .buffer = transfer_buffer, 452 .seek = 0, 453 .end = 0, 454 .vtable = &.{ 455 .stream = contentLengthStream, 456 .discard = contentLengthDiscard, 457 }, 458 }; 459 return &reader.interface; 460 } else { 461 reader.state = .body_none; 462 return reader.in; 463 } 464 }, 465 } 466 } 467 468 /// If compressed body has been negotiated this will return decompressed bytes. 469 /// 470 /// Asserts only called once and after `receiveHead`. 471 /// 472 /// See also: 473 /// * `interface` 474 pub fn bodyReaderDecompressing( 475 reader: *Reader, 476 transfer_buffer: []u8, 477 transfer_encoding: TransferEncoding, 478 content_length: ?u64, 479 content_encoding: ContentEncoding, 480 decompress: *Decompress, 481 decompress_buffer: []u8, 482 ) *std.Io.Reader { 483 if (transfer_encoding == .none and content_length == null) { 484 assert(reader.state == .received_head); 485 reader.state = .body_none; 486 switch (content_encoding) { 487 .identity => { 488 return reader.in; 489 }, 490 .deflate => { 491 decompress.* = .{ .flate = .init(reader.in, .zlib, decompress_buffer) }; 492 return &decompress.flate.reader; 493 }, 494 .gzip => { 495 decompress.* = .{ .flate = .init(reader.in, .gzip, decompress_buffer) }; 496 return &decompress.flate.reader; 497 }, 498 .zstd => { 499 decompress.* = .{ .zstd = .init(reader.in, decompress_buffer, .{ .verify_checksum = false }) }; 500 return &decompress.zstd.reader; 501 }, 502 .compress => unreachable, 503 } 504 } 505 const transfer_reader = bodyReader(reader, transfer_buffer, transfer_encoding, content_length); 506 return decompress.init(transfer_reader, decompress_buffer, content_encoding); 507 } 508 509 fn contentLengthStream( 510 io_r: *std.Io.Reader, 511 w: *Writer, 512 limit: std.Io.Limit, 513 ) std.Io.Reader.StreamError!usize { 514 const reader: *Reader = @alignCast(@fieldParentPtr("interface", io_r)); 515 if (reader.state == .ready) return error.EndOfStream; 516 const remaining_content_length = &reader.state.body_remaining_content_length; 517 const remaining = remaining_content_length.*; 518 const n = try reader.in.stream(w, limit.min(.limited64(remaining))); 519 if (n == remaining) { 520 reader.state = .ready; 521 } else { 522 remaining_content_length.* = remaining - n; 523 } 524 return n; 525 } 526 527 fn contentLengthDiscard(io_r: *std.Io.Reader, limit: std.Io.Limit) std.Io.Reader.Error!usize { 528 const reader: *Reader = @alignCast(@fieldParentPtr("interface", io_r)); 529 if (reader.state == .ready) return error.EndOfStream; 530 const remaining_content_length = &reader.state.body_remaining_content_length; 531 const remaining = remaining_content_length.*; 532 const n = try reader.in.discard(limit.min(.limited64(remaining))); 533 if (n == remaining) { 534 reader.state = .ready; 535 } else { 536 remaining_content_length.* = remaining - n; 537 } 538 return n; 539 } 540 541 fn chunkedStream(io_r: *std.Io.Reader, w: *Writer, limit: std.Io.Limit) std.Io.Reader.StreamError!usize { 542 const reader: *Reader = @alignCast(@fieldParentPtr("interface", io_r)); 543 const chunk_len_ptr = switch (reader.state) { 544 .ready => return error.EndOfStream, 545 .body_remaining_chunk_len => |*x| x, 546 else => unreachable, 547 }; 548 return chunkedReadEndless(reader, w, limit, chunk_len_ptr) catch |err| switch (err) { 549 error.ReadFailed, error.WriteFailed => |e| return e, 550 error.EndOfStream => { 551 reader.body_err = error.HttpChunkTruncated; 552 return error.ReadFailed; 553 }, 554 else => |e| { 555 reader.body_err = e; 556 return error.ReadFailed; 557 }, 558 }; 559 } 560 561 fn chunkedReadEndless( 562 reader: *Reader, 563 w: *Writer, 564 limit: std.Io.Limit, 565 chunk_len_ptr: *RemainingChunkLen, 566 ) (BodyError || std.Io.Reader.StreamError)!usize { 567 const in = reader.in; 568 len: switch (chunk_len_ptr.*) { 569 .head => { 570 var cp: ChunkParser = .init; 571 while (true) { 572 const i = cp.feed(in.buffered()); 573 switch (cp.state) { 574 .invalid => return error.HttpChunkInvalid, 575 .data => { 576 in.toss(i); 577 break; 578 }, 579 else => { 580 in.toss(i); 581 try in.fillMore(); 582 continue; 583 }, 584 } 585 } 586 if (cp.chunk_len == 0) return parseTrailers(reader, 0); 587 const n = try in.stream(w, limit.min(.limited64(cp.chunk_len))); 588 chunk_len_ptr.* = .init(cp.chunk_len + 2 - n); 589 return n; 590 }, 591 .n => { 592 if ((try in.peekByte()) != '\n') return error.HttpChunkInvalid; 593 in.toss(1); 594 continue :len .head; 595 }, 596 .rn => { 597 const rn = try in.peekArray(2); 598 if (rn[0] != '\r' or rn[1] != '\n') return error.HttpChunkInvalid; 599 in.toss(2); 600 continue :len .head; 601 }, 602 else => |remaining_chunk_len| { 603 const n = try in.stream(w, limit.min(.limited64(@backingInt(remaining_chunk_len) - 2))); 604 chunk_len_ptr.* = .init(@backingInt(remaining_chunk_len) - n); 605 return n; 606 }, 607 } 608 } 609 610 fn chunkedDiscard(io_r: *std.Io.Reader, limit: std.Io.Limit) std.Io.Reader.Error!usize { 611 const reader: *Reader = @alignCast(@fieldParentPtr("interface", io_r)); 612 const chunk_len_ptr = switch (reader.state) { 613 .ready => return error.EndOfStream, 614 .body_remaining_chunk_len => |*x| x, 615 else => unreachable, 616 }; 617 return chunkedDiscardEndless(reader, limit, chunk_len_ptr) catch |err| switch (err) { 618 error.ReadFailed => |e| return e, 619 error.EndOfStream => { 620 reader.body_err = error.HttpChunkTruncated; 621 return error.ReadFailed; 622 }, 623 else => |e| { 624 reader.body_err = e; 625 return error.ReadFailed; 626 }, 627 }; 628 } 629 630 fn chunkedDiscardEndless( 631 reader: *Reader, 632 limit: std.Io.Limit, 633 chunk_len_ptr: *RemainingChunkLen, 634 ) (BodyError || std.Io.Reader.Error)!usize { 635 const in = reader.in; 636 len: switch (chunk_len_ptr.*) { 637 .head => { 638 var cp: ChunkParser = .init; 639 while (true) { 640 const i = cp.feed(in.buffered()); 641 switch (cp.state) { 642 .invalid => return error.HttpChunkInvalid, 643 .data => { 644 in.toss(i); 645 break; 646 }, 647 else => { 648 in.toss(i); 649 try in.fillMore(); 650 continue; 651 }, 652 } 653 } 654 if (cp.chunk_len == 0) return parseTrailers(reader, 0); 655 const n = try in.discard(limit.min(.limited64(cp.chunk_len))); 656 chunk_len_ptr.* = .init(cp.chunk_len + 2 - n); 657 return n; 658 }, 659 .n => { 660 if ((try in.peekByte()) != '\n') return error.HttpChunkInvalid; 661 in.toss(1); 662 continue :len .head; 663 }, 664 .rn => { 665 const rn = try in.peekArray(2); 666 if (rn[0] != '\r' or rn[1] != '\n') return error.HttpChunkInvalid; 667 in.toss(2); 668 continue :len .head; 669 }, 670 else => |remaining_chunk_len| { 671 const n = try in.discard(limit.min(.limited64(remaining_chunk_len.int() - 2))); 672 chunk_len_ptr.* = .init(remaining_chunk_len.int() - n); 673 return n; 674 }, 675 } 676 } 677 678 /// Called when next bytes in the stream are trailers, or "\r\n" to indicate 679 /// end of chunked body. 680 fn parseTrailers(reader: *Reader, amt_read: usize) (BodyError || std.Io.Reader.Error)!usize { 681 const in = reader.in; 682 const rn = try in.peekArray(2); 683 if (rn[0] == '\r' and rn[1] == '\n') { 684 in.toss(2); 685 reader.state = .ready; 686 assert(reader.trailers.len == 0); 687 return amt_read; 688 } 689 var hp: HeadParser = .{ .state = .seen_rn }; 690 var trailers_len: usize = 2; 691 while (true) { 692 if (in.buffer.len - trailers_len == 0) return error.HttpHeadersOversize; 693 const remaining = in.buffered()[trailers_len..]; 694 if (remaining.len == 0) { 695 try in.fillMore(); 696 continue; 697 } 698 trailers_len += hp.feed(remaining); 699 if (hp.state == .finished) { 700 reader.state = .ready; 701 reader.trailers = in.buffered()[0..trailers_len]; 702 in.toss(trailers_len); 703 return amt_read; 704 } 705 } 706 } 707 }; 708 709 pub const Decompress = union(enum) { 710 flate: std.compress.flate.Decompress, 711 zstd: std.compress.zstd.Decompress, 712 none: *std.Io.Reader, 713 714 pub fn init( 715 decompress: *Decompress, 716 transfer_reader: *std.Io.Reader, 717 buffer: []u8, 718 content_encoding: ContentEncoding, 719 ) *std.Io.Reader { 720 switch (content_encoding) { 721 .identity => { 722 decompress.* = .{ .none = transfer_reader }; 723 return transfer_reader; 724 }, 725 .deflate => { 726 decompress.* = .{ .flate = .init(transfer_reader, .zlib, buffer) }; 727 return &decompress.flate.reader; 728 }, 729 .gzip => { 730 decompress.* = .{ .flate = .init(transfer_reader, .gzip, buffer) }; 731 return &decompress.flate.reader; 732 }, 733 .zstd => { 734 decompress.* = .{ .zstd = .init(transfer_reader, buffer, .{ .verify_checksum = false }) }; 735 return &decompress.zstd.reader; 736 }, 737 .compress => unreachable, 738 } 739 } 740 }; 741 742 /// Request or response body. 743 pub const BodyWriter = struct { 744 /// Until the lifetime of `BodyWriter` ends, it is illegal to modify the 745 /// state of this other than via methods of `BodyWriter`. 746 http_protocol_output: *Writer, 747 state: State, 748 writer: Writer, 749 750 pub const Error = Writer.Error; 751 752 /// How many zeroes to reserve for hex-encoded chunk length. 753 const chunk_len_digits = 8; 754 const max_chunk_len: usize = std.math.pow(u64, 16, chunk_len_digits) - 1; 755 const chunk_header_template = @as([chunk_len_digits]u8, @splat('0')) ++ "\r\n"; 756 757 comptime { 758 assert(max_chunk_len == std.math.maxInt(u32)); 759 } 760 761 pub const State = union(enum) { 762 /// End of connection signals the end of the stream. 763 none, 764 /// As a debugging utility, counts down to zero as bytes are written. 765 content_length: u64, 766 /// Each chunk is wrapped in a header and trailer. 767 /// This length is the the number of bytes to be written before the 768 /// next header. This includes +2 for the `\r\n` trailer and is zero 769 /// for the beginning of the stream. 770 chunk_len: usize, 771 /// Cleanly finished stream; connection can be reused. 772 end, 773 774 pub const init_chunked: State = .{ .chunk_len = 0 }; 775 }; 776 777 pub fn isEliding(w: *const BodyWriter) bool { 778 return w.writer.vtable.drain == elidingDrain; 779 } 780 781 /// Sends all buffered data across `BodyWriter.http_protocol_output`. 782 pub fn flush(w: *BodyWriter) Error!void { 783 const out = w.http_protocol_output; 784 switch (w.state) { 785 .end, .none, .content_length, .chunk_len => return out.flush(), 786 } 787 } 788 789 /// When using content-length, asserts that the amount of data sent matches 790 /// the value sent in the header, then flushes `http_protocol_output`. 791 /// 792 /// When using transfer-encoding: chunked, writes the end-of-stream message 793 /// with empty trailers, then flushes the stream to the system. Asserts any 794 /// started chunk has been completely finished. 795 /// 796 /// Respects the value of `isEliding` to omit all data after the headers. 797 /// 798 /// See also: 799 /// * `endUnflushed` 800 /// * `endChunked` 801 pub fn end(w: *BodyWriter) Error!void { 802 try endUnflushed(w); 803 try w.http_protocol_output.flush(); 804 } 805 806 /// When using content-length, asserts that the amount of data sent matches 807 /// the value sent in the header. 808 /// 809 /// Otherwise, transfer-encoding: chunked is being used, and it writes the 810 /// end-of-stream message with empty trailers. 811 /// 812 /// Respects the value of `isEliding` to omit all data after the headers. 813 /// 814 /// Does not flush `http_protocol_output`, but does flush `writer`. 815 /// 816 /// See also: 817 /// * `end` 818 /// * `endChunked` 819 pub fn endUnflushed(w: *BodyWriter) Error!void { 820 try w.writer.flush(); 821 switch (w.state) { 822 .end => unreachable, 823 .content_length => |len| { 824 assert(len == 0); // Trips when end() called before all bytes written. 825 w.state = .end; 826 }, 827 .none => {}, 828 .chunk_len => return endChunkedUnflushed(w, .{}), 829 } 830 } 831 832 pub const EndChunkedOptions = struct { 833 trailers: []const Header = &.{}, 834 }; 835 836 /// Writes the end-of-stream message and any optional trailers, flushing 837 /// the underlying stream. 838 /// 839 /// Asserts that the BodyWriter is using transfer-encoding: chunked. 840 /// 841 /// Respects the value of `isEliding` to omit all data after the headers. 842 /// 843 /// See also: 844 /// * `endChunkedUnflushed` 845 /// * `end` 846 pub fn endChunked(w: *BodyWriter, options: EndChunkedOptions) Error!void { 847 try endChunkedUnflushed(w, options); 848 try w.http_protocol_output.flush(); 849 } 850 851 /// Writes the end-of-stream message and any optional trailers. 852 /// 853 /// Does not flush. 854 /// 855 /// Asserts that the BodyWriter is using transfer-encoding: chunked. 856 /// 857 /// Respects the value of `isEliding` to omit all data after the headers. 858 /// 859 /// See also: 860 /// * `endChunked` 861 /// * `endUnflushed` 862 /// * `end` 863 pub fn endChunkedUnflushed(w: *BodyWriter, options: EndChunkedOptions) Error!void { 864 if (w.isEliding()) { 865 w.state = .end; 866 return; 867 } 868 const bw = w.http_protocol_output; 869 switch (w.state.chunk_len) { 870 0 => {}, 871 1 => unreachable, // Wrote more data than specified in chunk header. 872 2 => try bw.writeAll("\r\n"), 873 else => unreachable, // An earlier write call indicated more data would follow. 874 } 875 try bw.writeAll("0\r\n"); 876 for (options.trailers) |trailer| { 877 try bw.writeAll(trailer.name); 878 try bw.writeAll(": "); 879 try bw.writeAll(trailer.value); 880 try bw.writeAll("\r\n"); 881 } 882 try bw.writeAll("\r\n"); 883 w.state = .end; 884 } 885 886 pub fn contentLengthDrain(w: *Writer, data: []const []const u8, splat: usize) Error!usize { 887 const bw: *BodyWriter = @alignCast(@fieldParentPtr("writer", w)); 888 assert(!bw.isEliding()); 889 const out = bw.http_protocol_output; 890 const n = try out.writeSplatHeader(w.buffered(), data, splat); 891 bw.state.content_length -= n; 892 return w.consume(n); 893 } 894 895 pub fn noneDrain(w: *Writer, data: []const []const u8, splat: usize) Error!usize { 896 const bw: *BodyWriter = @alignCast(@fieldParentPtr("writer", w)); 897 assert(!bw.isEliding()); 898 const out = bw.http_protocol_output; 899 const n = try out.writeSplatHeader(w.buffered(), data, splat); 900 return w.consume(n); 901 } 902 903 pub fn elidingDrain(w: *Writer, data: []const []const u8, splat: usize) Error!usize { 904 const bw: *BodyWriter = @alignCast(@fieldParentPtr("writer", w)); 905 const slice = data[0 .. data.len - 1]; 906 const pattern = data[slice.len]; 907 var written: usize = pattern.len * splat; 908 for (slice) |bytes| written += bytes.len; 909 switch (bw.state) { 910 .content_length => |*len| len.* -= written + w.end, 911 else => {}, 912 } 913 w.end = 0; 914 return written; 915 } 916 917 pub fn elidingSendFile(w: *Writer, file_reader: *File.Reader, limit: std.Io.Limit) Writer.FileError!usize { 918 const bw: *BodyWriter = @alignCast(@fieldParentPtr("writer", w)); 919 if (File.Handle == void) return error.Unimplemented; 920 if (builtin.zig_backend == .stage2_aarch64) return error.Unimplemented; 921 switch (bw.state) { 922 .content_length => |*len| len.* -= w.end, 923 else => {}, 924 } 925 w.end = 0; 926 if (limit == .nothing) return 0; 927 if (file_reader.getSize()) |size| { 928 const n = limit.minInt64(size - file_reader.pos); 929 if (n == 0) return error.EndOfStream; 930 file_reader.seekBy(@intCast(n)) catch return error.Unimplemented; 931 switch (bw.state) { 932 .content_length => |*len| len.* -= n, 933 else => {}, 934 } 935 return n; 936 } else |_| { 937 // Error is observable on `file_reader` instance, and it is better to 938 // treat the file as a pipe. 939 return error.Unimplemented; 940 } 941 } 942 943 /// Returns `null` if size cannot be computed without making any syscalls. 944 pub fn noneSendFile(w: *Writer, file_reader: *File.Reader, limit: std.Io.Limit) Writer.FileError!usize { 945 const bw: *BodyWriter = @alignCast(@fieldParentPtr("writer", w)); 946 assert(!bw.isEliding()); 947 const out = bw.http_protocol_output; 948 const n = try out.sendFileHeader(w.buffered(), file_reader, limit); 949 return w.consume(n); 950 } 951 952 pub fn contentLengthSendFile(w: *Writer, file_reader: *File.Reader, limit: std.Io.Limit) Writer.FileError!usize { 953 const bw: *BodyWriter = @alignCast(@fieldParentPtr("writer", w)); 954 assert(!bw.isEliding()); 955 const out = bw.http_protocol_output; 956 const n = try out.sendFileHeader(w.buffered(), file_reader, limit); 957 bw.state.content_length -= n; 958 return w.consume(n); 959 } 960 961 pub fn chunkedSendFile(w: *Writer, file_reader: *File.Reader, limit: std.Io.Limit) Writer.FileError!usize { 962 const bw: *BodyWriter = @alignCast(@fieldParentPtr("writer", w)); 963 assert(!bw.isEliding()); 964 const data_len = Writer.countSendFileLowerBound(w.end, file_reader, limit) orelse { 965 // If the file size is unknown, we cannot lower to a `sendFile` since we would 966 // have to flush the chunk header before knowing the chunk length. 967 return error.Unimplemented; 968 }; 969 if (data_len == 0) return error.EndOfStream; 970 const out = bw.http_protocol_output; 971 l: switch (bw.state.chunk_len) { 972 0 => { 973 const header_buf = try out.writableArray(chunk_header_template.len); 974 @memcpy(header_buf, chunk_header_template); 975 writeHex(header_buf[0..chunk_len_digits], data_len); 976 bw.state.chunk_len = data_len + 2; 977 continue :l bw.state.chunk_len; 978 }, 979 1 => unreachable, // Wrote more data than specified in chunk header. 980 2 => { 981 try out.writeAll("\r\n"); 982 bw.state.chunk_len = 0; 983 continue :l 0; 984 }, 985 else => { 986 const chunk_limit: std.Io.Limit = .limited(bw.state.chunk_len - 2); 987 const n = if (chunk_limit.subtract(w.buffered().len)) |sendfile_limit| 988 try out.sendFileHeader(w.buffered(), file_reader, sendfile_limit.min(limit)) 989 else 990 try out.write(chunk_limit.slice(w.buffered())); 991 bw.state.chunk_len -= n; 992 return w.consume(n); 993 }, 994 } 995 } 996 997 pub fn chunkedDrain(w: *Writer, data: []const []const u8, splat: usize) Error!usize { 998 const bw: *BodyWriter = @alignCast(@fieldParentPtr("writer", w)); 999 assert(!bw.isEliding()); 1000 const out = bw.http_protocol_output; 1001 const data_len = w.end + Writer.countSplat(data, splat); 1002 l: switch (bw.state.chunk_len) { 1003 0 => { 1004 const header_buf = try out.writableArray(chunk_header_template.len); 1005 @memcpy(header_buf, chunk_header_template); 1006 writeHex(header_buf[0..chunk_len_digits], data_len); 1007 bw.state.chunk_len = data_len + 2; 1008 continue :l bw.state.chunk_len; 1009 }, 1010 1 => unreachable, // Wrote more data than specified in chunk header. 1011 2 => { 1012 try out.writeAll("\r\n"); 1013 bw.state.chunk_len = 0; 1014 continue :l 0; 1015 }, 1016 else => { 1017 const n = try out.writeSplatHeaderLimit(w.buffered(), data, splat, .limited(bw.state.chunk_len - 2)); 1018 bw.state.chunk_len -= n; 1019 return w.consume(n); 1020 }, 1021 } 1022 } 1023 1024 /// Writes an integer as base 16 to `buf`, right-aligned, assuming the 1025 /// buffer has already been filled with zeroes. 1026 fn writeHex(buf: []u8, x: usize) void { 1027 assert(std.mem.allEqual(u8, buf, '0')); 1028 const base = 16; 1029 var index: usize = buf.len; 1030 var a = x; 1031 while (a > 0) { 1032 const digit = a % base; 1033 index -= 1; 1034 buf[index] = std.fmt.digitToChar(@intCast(digit), .lower); 1035 a /= base; 1036 } 1037 } 1038 }; 1039 1040 test { 1041 _ = Server; 1042 _ = Status; 1043 _ = Method; 1044 _ = ChunkParser; 1045 _ = HeadParser; 1046 1047 if (builtin.os.tag != .wasi) { 1048 _ = Client; 1049 _ = @import("http/test.zig"); 1050 } 1051 } 1052