authorgravatar for truemedian@gmail.comNameless <truemedian@gmail.com> 2023-04-04 20:29:00-05:00
committergravatar for truemedian@gmail.comNameless <truemedian@gmail.com> 2023-04-08 09:59:36-05:00
logef6d58ed3b03b2a5d08489daee0c232d5e113d55
tree57026931ce164f75ece056989d224eaa58fc9901
parent8250a925444254c6c0c84af3006d04b7102a3343
signaturelock-open Commit is signed but in an unrecognized format.

std.http: add documentation


3 files changed, 73 insertions(+), 30 deletions(-)

lib/std/http/Client.zig+19-9
...@@ -15,8 +15,6 @@ const proto = @import("protocol.zig");...@@ -15,8 +15,6 @@ const proto = @import("protocol.zig");
15pub const default_connection_pool_size = 32;15pub const default_connection_pool_size = 32;
16pub const connection_pool_size = std.options.http_connection_pool_size;16pub const connection_pool_size = std.options.http_connection_pool_size;
1717
18/// Used for tcpConnectToHost and storing HTTP headers when an externally
19/// managed buffer is not provided.
20allocator: Allocator,18allocator: Allocator,
21ca_bundle: std.crypto.Certificate.Bundle = .{},19ca_bundle: std.crypto.Certificate.Bundle = .{},
22ca_bundle_mutex: std.Thread.Mutex = .{},20ca_bundle_mutex: std.Thread.Mutex = .{},
...@@ -24,8 +22,10 @@ ca_bundle_mutex: std.Thread.Mutex = .{},...@@ -24,8 +22,10 @@ ca_bundle_mutex: std.Thread.Mutex = .{},
24/// it will first rescan the system for root certificates.22/// it will first rescan the system for root certificates.
25next_https_rescan_certs: bool = true,23next_https_rescan_certs: bool = true,
2624
25/// The pool of connections that can be reused (and currently in use).
27connection_pool: ConnectionPool = .{},26connection_pool: ConnectionPool = .{},
2827
28/// The last error that occurred on this client. This is not threadsafe, do not expect it to be completely accurate.
29last_error: ?ExtraError = null,29last_error: ?ExtraError = null,
3030
31pub const ExtraError = union(enum) {31pub const ExtraError = union(enum) {
...@@ -68,7 +68,9 @@ pub const ExtraError = union(enum) {...@@ -68,7 +68,9 @@ pub const ExtraError = union(enum) {
68 decompress: DecompressError, // error.ReadFailed68 decompress: DecompressError, // error.ReadFailed
69};69};
7070
71/// A set of linked lists of connections that can be reused.
71pub const ConnectionPool = struct {72pub const ConnectionPool = struct {
73 /// The criteria for a connection to be considered a match.
72 pub const Criteria = struct {74 pub const Criteria = struct {
73 host: []const u8,75 host: []const u8,
74 port: u16,76 port: u16,
...@@ -92,7 +94,9 @@ pub const ConnectionPool = struct {...@@ -92,7 +94,9 @@ pub const ConnectionPool = struct {
92 pub const Node = Queue.Node;94 pub const Node = Queue.Node;
9395
94 mutex: std.Thread.Mutex = .{},96 mutex: std.Thread.Mutex = .{},
97 /// Open connections that are currently in use.
95 used: Queue = .{},98 used: Queue = .{},
99 /// Open connections that are not currently in use.
96 free: Queue = .{},100 free: Queue = .{},
97 free_len: usize = 0,101 free_len: usize = 0,
98 free_size: usize = connection_pool_size,102 free_size: usize = connection_pool_size,
...@@ -189,6 +193,7 @@ pub const ConnectionPool = struct {...@@ -189,6 +193,7 @@ pub const ConnectionPool = struct {
189 }193 }
190};194};
191195
196/// An interface to either a plain or TLS connection.
192pub const Connection = struct {197pub const Connection = struct {
193 stream: net.Stream,198 stream: net.Stream,
194 /// undefined unless protocol is tls.199 /// undefined unless protocol is tls.
...@@ -261,6 +266,7 @@ pub const Connection = struct {...@@ -261,6 +266,7 @@ pub const Connection = struct {
261 }266 }
262};267};
263268
269/// A buffered (and peekable) Connection.
264pub const BufferedConnection = struct {270pub const BufferedConnection = struct {
265 pub const buffer_size = 0x2000;271 pub const buffer_size = 0x2000;
266272
...@@ -344,12 +350,14 @@ pub const BufferedConnection = struct {...@@ -344,12 +350,14 @@ pub const BufferedConnection = struct {
344 }350 }
345};351};
346352
353/// The mode of transport for requests.
347pub const RequestTransfer = union(enum) {354pub const RequestTransfer = union(enum) {
348 content_length: u64,355 content_length: u64,
349 chunked: void,356 chunked: void,
350 none: void,357 none: void,
351};358};
352359
360/// The decompressor for response messages.
353pub const Compression = union(enum) {361pub const Compression = union(enum) {
354 pub const DeflateDecompressor = std.compress.zlib.ZlibStream(Request.TransferReader);362 pub const DeflateDecompressor = std.compress.zlib.ZlibStream(Request.TransferReader);
355 pub const GzipDecompressor = std.compress.gzip.Decompress(Request.TransferReader);363 pub const GzipDecompressor = std.compress.gzip.Decompress(Request.TransferReader);
...@@ -361,6 +369,7 @@ pub const Compression = union(enum) {...@@ -361,6 +369,7 @@ pub const Compression = union(enum) {
361 none: void,369 none: void,
362};370};
363371
372/// A HTTP response originating from a server.
364pub const Response = struct {373pub const Response = struct {
365 pub const Headers = struct {374 pub const Headers = struct {
366 status: http.Status,375 status: http.Status,
...@@ -501,14 +510,9 @@ pub const Response = struct {...@@ -501,14 +510,9 @@ pub const Response = struct {
501 skip: bool = false,510 skip: bool = false,
502};511};
503512
504/// A HTTP request.513/// A HTTP request that has been sent.
505///514///
506/// Order of operations:515/// Order of operations: request[ -> write -> finish] -> do -> read
507/// - request
508/// - write
509/// - finish
510/// - do
511/// - read
512pub const Request = struct {516pub const Request = struct {
513 pub const Headers = struct {517 pub const Headers = struct {
514 version: http.Version = .@"HTTP/1.1",518 version: http.Version = .@"HTTP/1.1",
...@@ -862,6 +866,8 @@ pub const Request = struct {...@@ -862,6 +866,8 @@ pub const Request = struct {
862 }866 }
863};867};
864868
869/// Release all associated resources with the client.
870/// TODO: currently leaks all request allocated data
865pub fn deinit(client: *Client) void {871pub fn deinit(client: *Client) void {
866 client.connection_pool.deinit(client);872 client.connection_pool.deinit(client);
867873
...@@ -871,6 +877,8 @@ pub fn deinit(client: *Client) void {...@@ -871,6 +877,8 @@ pub fn deinit(client: *Client) void {
871877
872pub const ConnectError = Allocator.Error || error{ ConnectionFailed, TlsInitializationFailed };878pub const ConnectError = Allocator.Error || error{ ConnectionFailed, TlsInitializationFailed };
873879
880/// Connect to `host:port` using the specified protocol. This will reuse a connection if one is already open.
881/// This function is threadsafe.
874pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectError!*ConnectionPool.Node {882pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectError!*ConnectionPool.Node {
875 if (client.connection_pool.findConnection(.{883 if (client.connection_pool.findConnection(.{
876 .host = host,884 .host = host,
...@@ -955,6 +963,8 @@ pub const protocol_map = std.ComptimeStringMap(Connection.Protocol, .{...@@ -955,6 +963,8 @@ pub const protocol_map = std.ComptimeStringMap(Connection.Protocol, .{
955 .{ "wss", .tls },963 .{ "wss", .tls },
956});964});
957965
966/// Form and send a http request to a server.
967/// This function is threadsafe.
958pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Options) RequestError!Request {968pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Options) RequestError!Request {
959 const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme;969 const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme;
960970
lib/std/http/Server.zig+22-7
...@@ -14,10 +14,7 @@ allocator: Allocator,...@@ -14,10 +14,7 @@ allocator: Allocator,
1414
15socket: net.StreamServer,15socket: net.StreamServer,
1616
17pub const DeflateDecompressor = std.compress.zlib.ZlibStream(Response.TransferReader);17/// An interface to either a plain or TLS connection.
18pub const GzipDecompressor = std.compress.gzip.Decompress(Response.TransferReader);
19pub const ZstdDecompressor = std.compress.zstd.DecompressStream(Response.TransferReader, .{});
20
21pub const Connection = struct {18pub const Connection = struct {
22 stream: net.Stream,19 stream: net.Stream,
23 protocol: Protocol,20 protocol: Protocol,
...@@ -74,6 +71,7 @@ pub const Connection = struct {...@@ -74,6 +71,7 @@ pub const Connection = struct {
74 }71 }
75};72};
7673
74/// A buffered (and peekable) Connection.
77pub const BufferedConnection = struct {75pub const BufferedConnection = struct {
78 pub const buffer_size = 0x2000;76 pub const buffer_size = 0x2000;
7977
...@@ -157,6 +155,7 @@ pub const BufferedConnection = struct {...@@ -157,6 +155,7 @@ pub const BufferedConnection = struct {
157 }155 }
158};156};
159157
158/// A HTTP request originating from a client.
160pub const Request = struct {159pub const Request = struct {
161 pub const Headers = struct {160 pub const Headers = struct {
162 method: http.Method,161 method: http.Method,
...@@ -290,6 +289,11 @@ pub const Request = struct {...@@ -290,6 +289,11 @@ pub const Request = struct {
290 compression: Compression = .none,289 compression: Compression = .none,
291};290};
292291
292/// A HTTP response waiting to be sent.
293///
294/// [/ <----------------------------------- \]
295/// Order of operations: accept -> wait -> do [ -> write -> finish][ -> reset /]
296/// \ -> read /
293pub const Response = struct {297pub const Response = struct {
294 pub const Headers = struct {298 pub const Headers = struct {
295 version: http.Version = .@"HTTP/1.1",299 version: http.Version = .@"HTTP/1.1",
...@@ -310,6 +314,7 @@ pub const Response = struct {...@@ -310,6 +314,7 @@ pub const Response = struct {
310 headers: Headers = .{},314 headers: Headers = .{},
311 request: Request,315 request: Request,
312316
317 /// Reset this response to its initial state. This must be called before handling a second request on the same connection.
313 pub fn reset(res: *Response) void {318 pub fn reset(res: *Response) void {
314 switch (res.request.compression) {319 switch (res.request.compression) {
315 .none => {},320 .none => {},
...@@ -336,7 +341,8 @@ pub const Response = struct {...@@ -336,7 +341,8 @@ pub const Response = struct {
336 }341 }
337 }342 }
338343
339 pub fn sendResponseHead(res: *Response) !void {344 /// Send the response headers.
345 pub fn do(res: *Response) !void {
340 var buffered = std.io.bufferedWriter(res.connection.writer());346 var buffered = std.io.bufferedWriter(res.connection.writer());
341 const w = buffered.writer();347 const w = buffered.writer();
342348
...@@ -402,7 +408,8 @@ pub const Response = struct {...@@ -402,7 +408,8 @@ pub const Response = struct {
402408
403 pub const WaitForCompleteHeadError = BufferedConnection.ReadError || proto.HeadersParser.WaitForCompleteHeadError || Request.Headers.ParseError || error{ BadHeader, InvalidCompression, StreamTooLong, InvalidWindowSize } || error{CompressionNotSupported};409 pub const WaitForCompleteHeadError = BufferedConnection.ReadError || proto.HeadersParser.WaitForCompleteHeadError || Request.Headers.ParseError || error{ BadHeader, InvalidCompression, StreamTooLong, InvalidWindowSize } || error{CompressionNotSupported};
404410
405 pub fn waitForCompleteHead(res: *Response) !void {411 /// Wait for the client to send a complete request head.
412 pub fn wait(res: *Response) !void {
406 while (true) {413 while (true) {
407 try res.connection.fill();414 try res.connection.fill();
408415
...@@ -451,7 +458,7 @@ pub const Response = struct {...@@ -451,7 +458,7 @@ pub const Response = struct {
451 }458 }
452 }459 }
453460
454 pub const ReadError = DeflateDecompressor.Error || GzipDecompressor.Error || ZstdDecompressor.Error || WaitForCompleteHeadError;461 pub const ReadError = Compression.DeflateDecompressor.Error || Compression.GzipDecompressor.Error || Compression.ZstdDecompressor.Error || WaitForCompleteHeadError;
455462
456 pub const Reader = std.io.Reader(*Response, ReadError, read);463 pub const Reader = std.io.Reader(*Response, ReadError, read);
457464
...@@ -517,13 +524,19 @@ pub const Response = struct {...@@ -517,13 +524,19 @@ pub const Response = struct {
517 }524 }
518};525};
519526
527/// The mode of transport for responses.
520pub const RequestTransfer = union(enum) {528pub const RequestTransfer = union(enum) {
521 content_length: u64,529 content_length: u64,
522 chunked: void,530 chunked: void,
523 none: void,531 none: void,
524};532};
525533
534/// The decompressor for request messages.
526pub const Compression = union(enum) {535pub const Compression = union(enum) {
536 pub const DeflateDecompressor = std.compress.zlib.ZlibStream(Response.TransferReader);
537 pub const GzipDecompressor = std.compress.gzip.Decompress(Response.TransferReader);
538 pub const ZstdDecompressor = std.compress.zstd.DecompressStream(Response.TransferReader, .{});
539
527 deflate: DeflateDecompressor,540 deflate: DeflateDecompressor,
528 gzip: GzipDecompressor,541 gzip: GzipDecompressor,
529 zstd: ZstdDecompressor,542 zstd: ZstdDecompressor,
...@@ -543,6 +556,7 @@ pub fn deinit(server: *Server) void {...@@ -543,6 +556,7 @@ pub fn deinit(server: *Server) void {
543556
544pub const ListenError = std.os.SocketError || std.os.BindError || std.os.ListenError || std.os.SetSockOptError || std.os.GetSockNameError;557pub const ListenError = std.os.SocketError || std.os.BindError || std.os.ListenError || std.os.SetSockOptError || std.os.GetSockNameError;
545558
559/// Start the HTTP server listening on the given address.
546pub fn listen(server: *Server, address: net.Address) !void {560pub fn listen(server: *Server, address: net.Address) !void {
547 try server.socket.listen(address);561 try server.socket.listen(address);
548}562}
...@@ -562,6 +576,7 @@ pub const HeaderStrategy = union(enum) {...@@ -562,6 +576,7 @@ pub const HeaderStrategy = union(enum) {
562 static: []u8,576 static: []u8,
563};577};
564578
579/// Accept a new connection and allocate a Response for it.
565pub fn accept(server: *Server, options: HeaderStrategy) AcceptError!*Response {580pub fn accept(server: *Server, options: HeaderStrategy) AcceptError!*Response {
566 const in = try server.socket.accept();581 const in = try server.socket.accept();
567582
lib/std/http/protocol.zig+32-14
...@@ -21,6 +21,7 @@ pub const State = enum {...@@ -21,6 +21,7 @@ pub const State = enum {
21 chunk_data_suffix,21 chunk_data_suffix,
22 chunk_data_suffix_r,22 chunk_data_suffix_r,
2323
24 /// Returns true if the parser is in a content state (ie. not waiting for more headers).
24 pub fn isContent(self: State) bool {25 pub fn isContent(self: State) bool {
25 return switch (self) {26 return switch (self) {
26 .invalid, .start, .seen_n, .seen_r, .seen_rn, .seen_rnr => false,27 .invalid, .start, .seen_n, .seen_r, .seen_rn, .seen_rnr => false,
...@@ -31,7 +32,7 @@ pub const State = enum {...@@ -31,7 +32,7 @@ pub const State = enum {
3132
32pub const HeadersParser = struct {33pub const HeadersParser = struct {
33 state: State = .start,34 state: State = .start,
34 /// Wether or not `header_bytes` is allocated or was provided as a fixed buffer.35 /// Whether or not `header_bytes` is allocated or was provided as a fixed buffer.
35 header_bytes_owned: bool,36 header_bytes_owned: bool,
36 /// Either a fixed buffer of len `max_header_bytes` or a dynamic buffer that can grow up to `max_header_bytes`.37 /// Either a fixed buffer of len `max_header_bytes` or a dynamic buffer that can grow up to `max_header_bytes`.
37 /// Pointers into this buffer are not stable until after a message is complete.38 /// Pointers into this buffer are not stable until after a message is complete.
...@@ -39,10 +40,11 @@ pub const HeadersParser = struct {...@@ -39,10 +40,11 @@ pub const HeadersParser = struct {
39 /// The maximum allowed size of `header_bytes`.40 /// The maximum allowed size of `header_bytes`.
40 max_header_bytes: usize,41 max_header_bytes: usize,
41 next_chunk_length: u64 = 0,42 next_chunk_length: u64 = 0,
42 /// Wether this parser is done parsing a complete message.43 /// Whether this parser is done parsing a complete message.
43 /// A message is only done when the entire payload has been read44 /// A message is only done when the entire payload has been read.
44 done: bool = false,45 done: bool = false,
4546
47 /// Initializes the parser with a dynamically growing header buffer of up to `max` bytes.
46 pub fn initDynamic(max: usize) HeadersParser {48 pub fn initDynamic(max: usize) HeadersParser {
47 return .{49 return .{
48 .header_bytes = .{},50 .header_bytes = .{},
...@@ -51,6 +53,7 @@ pub const HeadersParser = struct {...@@ -51,6 +53,7 @@ pub const HeadersParser = struct {
51 };53 };
52 }54 }
5355
56 /// Initializes the parser with a provided buffer `buf`.
54 pub fn initStatic(buf: []u8) HeadersParser {57 pub fn initStatic(buf: []u8) HeadersParser {
55 return .{58 return .{
56 .header_bytes = .{ .items = buf[0..0], .capacity = buf.len },59 .header_bytes = .{ .items = buf[0..0], .capacity = buf.len },
...@@ -59,7 +62,11 @@ pub const HeadersParser = struct {...@@ -59,7 +62,11 @@ pub const HeadersParser = struct {
59 };62 };
60 }63 }
6164
65 /// Completely resets the parser to it's initial state.
66 /// This must be called after a message is complete.
62 pub fn reset(r: *HeadersParser) void {67 pub fn reset(r: *HeadersParser) void {
68 assert(r.done); // The message must be completely read before reset, otherwise the parser is in an invalid state.
69
63 r.header_bytes.clearRetainingCapacity();70 r.header_bytes.clearRetainingCapacity();
6471
65 r.* = .{72 r.* = .{
...@@ -69,13 +76,14 @@ pub const HeadersParser = struct {...@@ -69,13 +76,14 @@ pub const HeadersParser = struct {
69 };76 };
70 }77 }
7178
72 /// Returns how many bytes are part of HTTP headers. Always less than or79 /// Returns the number of bytes consumed by headers. This is always less than or equal to `bytes.len`.
73 /// equal to bytes.len. If the amount returned is less than bytes.len, it80 /// You should check `r.state.isContent()` after this to check if the headers are done.
74 /// means the headers ended and the first byte after the double \r\n\r\n is81 ///
75 /// located at `bytes[result]`.82 /// If the amount returned is less than `bytes.len`, you may assume that the parser is in a content state and the
83 /// first byte of content is located at `bytes[result]`.
76 pub fn findHeadersEnd(r: *HeadersParser, bytes: []const u8) u32 {84 pub fn findHeadersEnd(r: *HeadersParser, bytes: []const u8) u32 {
77 const vector_len = 16;85 const vector_len: comptime_int = comptime std.simd.suggestVectorSize(u8) orelse 8;
78 const len = @truncate(u32, bytes.len);86 const len = @intCast(u32, bytes.len);
79 var index: u32 = 0;87 var index: u32 = 0;
8088
81 while (true) {89 while (true) {
...@@ -390,8 +398,13 @@ pub const HeadersParser = struct {...@@ -390,8 +398,13 @@ pub const HeadersParser = struct {
390 }398 }
391 }399 }
392400
401 /// Returns the number of bytes consumed by the chunk size. This is always less than or equal to `bytes.len`.
402 /// You should check `r.state == .chunk_data` after this to check if the chunk size has been fully parsed.
403 ///
404 /// If the amount returned is less than `bytes.len`, you may assume that the parser is in the `chunk_data` state
405 /// and that the first byte of the chunk is at `bytes[result]`.
393 pub fn findChunkedLen(r: *HeadersParser, bytes: []const u8) u32 {406 pub fn findChunkedLen(r: *HeadersParser, bytes: []const u8) u32 {
394 const len = @truncate(u32, bytes.len);407 const len = @intCast(u32, bytes.len);
395408
396 for (bytes[0..], 0..) |c, i| {409 for (bytes[0..], 0..) |c, i| {
397 const index = @intCast(u32, i);410 const index = @intCast(u32, i);
...@@ -471,8 +484,10 @@ pub const HeadersParser = struct {...@@ -471,8 +484,10 @@ pub const HeadersParser = struct {
471484
472 pub const CheckCompleteHeadError = mem.Allocator.Error || error{HttpHeadersExceededSizeLimit};485 pub const CheckCompleteHeadError = mem.Allocator.Error || error{HttpHeadersExceededSizeLimit};
473486
474 /// Pumps `in` bytes into the parser. Returns the number of bytes consumed. This function will return 0 if the parser487 /// Pushes `in` into the parser. Returns the number of bytes consumed by the header. Any header bytes are appended
475 /// is not in a state to parse more headers.488 /// to the `header_bytes` buffer.
489 ///
490 /// This function only uses `allocator` if `r.header_bytes_owned` is true, and may be undefined otherwise.
476 pub fn checkCompleteHead(r: *HeadersParser, allocator: std.mem.Allocator, in: []const u8) CheckCompleteHeadError!u32 {491 pub fn checkCompleteHead(r: *HeadersParser, allocator: std.mem.Allocator, in: []const u8) CheckCompleteHeadError!u32 {
477 if (r.state.isContent()) return 0;492 if (r.state.isContent()) return 0;
478493
...@@ -493,8 +508,11 @@ pub const HeadersParser = struct {...@@ -493,8 +508,11 @@ pub const HeadersParser = struct {
493 HttpChunkInvalid,508 HttpChunkInvalid,
494 };509 };
495510
496 /// Reads the body of the message into `buffer`. If `skip` is true, the buffer will be unused and the body will be511 /// Reads the body of the message into `buffer`. Returns the number of bytes placed in the buffer.
497 /// skipped. Returns the number of bytes placed in the buffer.512 ///
513 /// If `skip` is true, the buffer will be unused and the body will be skipped.
514 ///
515 /// See `std.http.Client.BufferedConnection for an example of `bconn`.
498 pub fn read(r: *HeadersParser, bconn: anytype, buffer: []u8, skip: bool) !usize {516 pub fn read(r: *HeadersParser, bconn: anytype, buffer: []u8, skip: bool) !usize {
499 assert(r.state.isContent());517 assert(r.state.isContent());
500 if (r.done) return 0;518 if (r.done) return 0;