1const builtin = @import("builtin");
2const std = @import("std.zig");
3const assert = std.debug.assert;
4const Writer = std.Io.Writer;
5const File = std.Io.File;
6
7pub const Client = @import("http/Client.zig");
8pub const Server = @import("http/Server.zig");
9pub const HeadParser = @import("http/HeadParser.zig");
10pub const ChunkParser = @import("http/ChunkParser.zig");
11pub const HeaderIterator = @import("http/HeaderIterator.zig");
12
13pub 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
25pub 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
96pub 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`.
274pub const TransferEncoding = enum {
275 chunked,
276 none,
277};
278
279pub 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
308pub const Connection = enum {
309 keep_alive,
310 close,
311};
312
313pub const Header = struct {
314 name: []const u8,
315 value: []const u8,
316};
317
318pub 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
709pub 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.
743pub 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
1040test {
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}