Commit b723296e1f

Nameless <truemedian@gmail.com>
2023-12-14 22:52:39
std.http: add missing documentation and a few examples
1 parent 832f6d8
Changed files (4)
lib
test
standalone
lib/std/http/Client.zig
@@ -1,4 +1,8 @@
-//! Connecting and opening requests are threadsafe. Individual requests are not.
+//! HTTP(S) Client implementation.
+//!
+//! Connections are opened in a thread-safe manner, but individual Requests are not.
+//!
+//! TLS support may be disabled via `std.options.http_disable_tls`.
 
 const std = @import("../std.zig");
 const builtin = @import("builtin");
@@ -157,6 +161,9 @@ pub const ConnectionPool = struct {
         pool.free_size = new_size;
     }
 
+    /// Frees the connection pool and closes all connections within. This function is threadsafe.
+    ///
+    /// All future operations on the connection pool will deadlock.
     pub fn deinit(pool: *ConnectionPool, allocator: Allocator) void {
         pool.mutex.lock();
 
@@ -191,11 +198,19 @@ pub const Connection = struct {
     /// undefined unless protocol is tls.
     tls_client: if (!disable_tls) *std.crypto.tls.Client else void,
 
+    /// The protocol that this connection is using.
     protocol: Protocol,
+
+    /// The host that this connection is connected to.
     host: []u8,
+
+    /// The port that this connection is connected to.
     port: u16,
 
+    /// Whether this connection is proxied and is not directly connected.
     proxied: bool = false,
+
+    /// Whether this connection is closing when we're done with it.
     closing: bool = false,
 
     read_start: BufferSize = 0,
@@ -232,6 +247,7 @@ pub const Connection = struct {
         };
     }
 
+    /// Refills the read buffer with data from the connection.
     pub fn fill(conn: *Connection) ReadError!void {
         if (conn.read_end != conn.read_start) return;
 
@@ -244,14 +260,17 @@ pub const Connection = struct {
         conn.read_end = @intCast(nread);
     }
 
+    /// Returns the current slice of buffered data.
     pub fn peek(conn: *Connection) []const u8 {
         return conn.read_buf[conn.read_start..conn.read_end];
     }
 
+    /// Discards the given number of bytes from the read buffer.
     pub fn drop(conn: *Connection, num: BufferSize) void {
         conn.read_start += num;
     }
 
+    /// Reads data from the connection into the given buffer.
     pub fn read(conn: *Connection, buffer: []u8) ReadError!usize {
         const available_read = conn.read_end - conn.read_start;
         const available_buffer = buffer.len;
@@ -318,6 +337,7 @@ pub const Connection = struct {
         };
     }
 
+    /// Writes the given buffer to the connection.
     pub fn write(conn: *Connection, buffer: []const u8) WriteError!usize {
         if (conn.write_end + buffer.len > conn.write_buf.len) {
             try conn.flush();
@@ -334,6 +354,7 @@ pub const Connection = struct {
         return buffer.len;
     }
 
+    /// Flushes the write buffer to the connection.
     pub fn flush(conn: *Connection) WriteError!void {
         if (conn.write_end == 0) return;
 
@@ -352,6 +373,7 @@ pub const Connection = struct {
         return Writer{ .context = conn };
     }
 
+    /// Closes the connection.
     pub fn close(conn: *Connection, allocator: Allocator) void {
         if (conn.protocol == .tls) {
             if (disable_tls) unreachable;
@@ -502,8 +524,13 @@ pub const Response = struct {
         try expectEqual(@as(u10, 999), parseInt3("999"));
     }
 
+    /// The HTTP version this response is using.
     version: http.Version,
+
+    /// The status code of the response.
     status: http.Status,
+
+    /// The reason phrase of the response.
     reason: []const u8,
 
     /// If present, the number of bytes in the response body.
@@ -528,22 +555,36 @@ pub const Response = struct {
 ///
 /// Order of operations: open -> send[ -> write -> finish] -> wait -> read
 pub const Request = struct {
+    /// The uri that this request is being sent to.
     uri: Uri,
+
+    /// The client that this request was created from.
     client: *Client,
-    /// is null when this connection is released
+
+    /// Underlying connection to the server. This is null when the connection is released.
     connection: ?*Connection,
 
     method: http.Method,
     version: http.Version = .@"HTTP/1.1",
+
+    /// The list of HTTP request headers.
     headers: http.Headers,
 
     /// The transfer encoding of the request body.
     transfer_encoding: RequestTransfer = .none,
 
+    /// The redirect quota left for this request.
     redirects_left: u32,
+
+    /// Whether the request should follow redirects.
     handle_redirects: bool,
+
+    /// Whether the request should handle a 100-continue response before sending the request body.
     handle_continue: bool,
 
+    /// The response associated with this request.
+    ///
+    /// This field is undefined until `wait` is called.
     response: Response,
 
     /// Used as a allocator for resolving redirects locations.
@@ -993,6 +1034,7 @@ pub const Request = struct {
     }
 };
 
+/// A HTTP proxy server.
 pub const Proxy = struct {
     allocator: Allocator,
     headers: http.Headers,
@@ -1144,6 +1186,7 @@ pub fn loadDefaultProxies(client: *Client) !void {
 pub const ConnectTcpError = Allocator.Error || error{ ConnectionRefused, NetworkUnreachable, ConnectionTimedOut, ConnectionResetByPeer, TemporaryNameServerFailure, NameServerFailure, UnknownHostName, HostLacksNetworkAddresses, UnexpectedConnectFailure, TlsInitializationFailed };
 
 /// Connect to `host:port` using the specified protocol. This will reuse a connection if one is already open.
+///
 /// This function is threadsafe.
 pub fn connectTcp(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectTcpError!*Connection {
     if (client.connection_pool.findConnection(.{
@@ -1203,6 +1246,7 @@ pub fn connectTcp(client: *Client, host: []const u8, port: u16, protocol: Connec
 pub const ConnectUnixError = Allocator.Error || std.os.SocketError || error{ NameTooLong, Unsupported } || std.os.ConnectError;
 
 /// Connect to `path` as a unix domain socket. This will reuse a connection if one is already open.
+///
 /// This function is threadsafe.
 pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*Connection {
     if (!net.has_unix_sockets) return error.Unsupported;
@@ -1237,6 +1281,7 @@ pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*Connecti
 }
 
 /// Connect to `tunnel_host:tunnel_port` using the specified proxy with HTTP CONNECT. This will reuse a connection if one is already open.
+///
 /// This function is threadsafe.
 pub fn connectTunnel(
     client: *Client,
@@ -1318,7 +1363,6 @@ const ConnectErrorPartial = ConnectTcpError || error{ UnsupportedUrlScheme, Conn
 pub const ConnectError = ConnectErrorPartial || RequestError;
 
 /// Connect to `host:port` using the specified protocol. This will reuse a connection if one is already open.
-///
 /// If a proxy is configured for the client, then the proxy will be used to connect to the host.
 ///
 /// This function is threadsafe.
@@ -1375,7 +1419,10 @@ pub const RequestOptions = struct {
     /// request, then the request *will* deadlock.
     handle_continue: bool = true,
 
+    /// Automatically follow redirects. This will only follow redirects for repeatable requests (ie. with no payload or the server has acknowledged the payload)
     handle_redirects: bool = true,
+
+    /// How many redirects to follow before returning an error.
     max_redirects: u32 = 3,
     header_strategy: StorageStrategy = .{ .dynamic = 16 * 1024 },
 
lib/std/http/Headers.zig
@@ -35,6 +35,7 @@ pub const CaseInsensitiveStringContext = struct {
     }
 };
 
+/// A single HTTP header field.
 pub const Field = struct {
     name: []const u8,
     value: []const u8,
@@ -47,6 +48,7 @@ pub const Field = struct {
     }
 };
 
+/// A list of HTTP header fields.
 pub const Headers = struct {
     allocator: Allocator,
     list: HeaderList = .{},
@@ -56,10 +58,12 @@ pub const Headers = struct {
     /// Use with caution.
     owned: bool = true,
 
+    /// Initialize an empty list of headers.
     pub fn init(allocator: Allocator) Headers {
         return .{ .allocator = allocator };
     }
 
+    /// Initialize a pre-populated list of headers from a list of fields.
     pub fn initList(allocator: Allocator, list: []const Field) !Headers {
         var new = Headers.init(allocator);
 
@@ -72,6 +76,9 @@ pub const Headers = struct {
         return new;
     }
 
+    /// Deallocate all memory associated with the headers.
+    ///
+    /// If the `owned` field is false, this will not free the names and values of the headers.
     pub fn deinit(headers: *Headers) void {
         headers.deallocateIndexListsAndFields();
         headers.index.deinit(headers.allocator);
@@ -80,7 +87,9 @@ pub const Headers = struct {
         headers.* = undefined;
     }
 
-    /// Appends a header to the list. Both name and value are copied.
+    /// Appends a header to the list.
+    ///
+    /// If the `owned` field is true, both name and value will be copied.
     pub fn append(headers: *Headers, name: []const u8, value: []const u8) !void {
         const n = headers.list.items.len;
 
@@ -108,6 +117,7 @@ pub const Headers = struct {
         try headers.list.append(headers.allocator, entry);
     }
 
+    /// Returns true if this list of headers contains the given name.
     pub fn contains(headers: Headers, name: []const u8) bool {
         return headers.index.contains(name);
     }
@@ -285,6 +295,7 @@ pub const Headers = struct {
         headers.list.clearRetainingCapacity();
     }
 
+    /// Creates a copy of the headers using the provided allocator.
     pub fn clone(headers: Headers, allocator: Allocator) !Headers {
         var new = Headers.init(allocator);
 
lib/std/http/Server.zig
@@ -1,3 +1,44 @@
+//! HTTP Server implementation.
+//!
+//! This server assumes *all* clients are well behaved and standard compliant; it can and will deadlock if a client holds a connection open without sending a request.
+//!
+//! Example usage:
+//!
+//! ```zig
+//! var server = Server.init(.{ .reuse_address = true });
+//! defer server.deinit();
+//!
+//! try server.listen(bind_addr);
+//!
+//! while (true) {
+//!     var res = try server.accept(.{ .allocator = gpa });
+//!     defer res.deinit();
+//!
+//!     while (res.reset() != .closing) {
+//!         res.wait() catch |err| switch (err) {
+//!             error.HttpHeadersInvalid => break,
+//!             error.HttpHeadersExceededSizeLimit => {
+//!                 res.status = .request_header_fields_too_large;
+//!                 res.send() catch break;
+//!                 break;
+//!             },
+//!             else => {
+//!                 res.status = .bad_request;
+//!                 res.send() catch break;
+//!                 break;
+//!             },
+//!         }
+//!
+//!         res.status = .ok;
+//!         res.transfer_encoding = .chunked;
+//!
+//!         try res.send();
+//!         try res.writeAll("Hello, World!\n");
+//!         try res.finish();
+//!     }
+//! }
+//! ```
+
 const std = @import("../std.zig");
 const testing = std.testing;
 const http = std.http;
@@ -10,8 +51,7 @@ const assert = std.debug.assert;
 const Server = @This();
 const proto = @import("protocol.zig");
 
-allocator: Allocator,
-
+/// The underlying server socket.
 socket: net.StreamServer,
 
 /// An interface to a plain connection.
@@ -269,8 +309,13 @@ pub const Request = struct {
         return @as(u64, @bitCast(array.*));
     }
 
+    /// The HTTP request method.
     method: http.Method,
+
+    /// The HTTP request target.
     target: []const u8,
+
+    /// The HTTP version of this request.
     version: http.Version,
 
     /// The length of the request body, if known.
@@ -282,16 +327,21 @@ pub const Request = struct {
     /// The compression of the request body, or .identity (no compression) if not present.
     transfer_compression: http.ContentEncoding = .identity,
 
+    /// The list of HTTP request headers
     headers: http.Headers,
+
     parser: proto.HeadersParser,
     compression: Compression = .none,
 };
 
 /// A HTTP response waiting to be sent.
 ///
-///                                  [/ <----------------------------------- \]
-/// Order of operations: accept -> wait -> send  [ -> write -> finish][ -> reset /]
-///                                   \ -> read /
+/// Order of operations:
+/// ```
+///             [/ <--------------------------------------- \]
+/// accept -> wait -> send  [ -> write -> finish][ -> reset /]
+///              \ -> read /
+/// ```
 pub const Response = struct {
     version: http.Version = .@"HTTP/1.1",
     status: http.Status = .ok,
@@ -299,11 +349,21 @@ pub const Response = struct {
 
     transfer_encoding: ResponseTransfer = .none,
 
+    /// The allocator responsible for allocating memory for this response.
     allocator: Allocator,
+
+    /// The peer's address
     address: net.Address,
+
+    /// The underlying connection for this response.
     connection: Connection,
 
+    /// The HTTP response headers
     headers: http.Headers,
+
+    /// The HTTP request that this response is responding to.
+    ///
+    /// This field is only valid after calling `wait`.
     request: Request,
 
     state: State = .first,
@@ -495,6 +555,17 @@ pub const Response = struct {
     pub const WaitError = Connection.ReadError || proto.HeadersParser.CheckCompleteHeadError || Request.ParseError || error{ CompressionInitializationFailed, CompressionNotSupported };
 
     /// Wait for the client to send a complete request head.
+    ///
+    /// For correct behavior, the following rules must be followed:
+    ///
+    /// * If this returns any error in `Connection.ReadError`, you MUST immediately close the connection by calling `deinit`.
+    /// * If this returns `error.HttpHeadersInvalid`, you MAY immediately close the connection by calling `deinit`.
+    /// * If this returns `error.HttpHeadersExceededSizeLimit`, you MUST respond with a 431 status code and then call `deinit`.
+    /// * If this returns any error in `Request.ParseError`, you MUST respond with a 400 status code and then call `deinit`.
+    /// * If this returns any other error, you MUST respond with a 400 status code and then call `deinit`.
+    /// * If the request has an Expect header containing 100-continue, you MUST either:
+    ///   * Respond with a 100 status code, then call `wait` again.
+    ///   * Respond with a 417 status code.
     pub fn wait(res: *Response) WaitError!void {
         switch (res.state) {
             .first, .start => res.state = .waited,
@@ -664,9 +735,8 @@ pub const Response = struct {
 };
 
 /// Create a new HTTP server.
-pub fn init(allocator: Allocator, options: net.StreamServer.Options) Server {
+pub fn init(options: net.StreamServer.Options) Server {
     return .{
-        .allocator = allocator,
         .socket = net.StreamServer.init(options),
     };
 }
@@ -748,7 +818,7 @@ test "HTTP server handles a chunked transfer coding request" {
     const expect = std.testing.expect;
 
     const max_header_size = 8192;
-    var server = std.http.Server.init(allocator, .{ .reuse_address = true });
+    var server = std.http.Server.init(.{ .reuse_address = true });
     defer server.deinit();
 
     const address = try std.net.Address.parseIp("127.0.0.1", 0);
test/standalone/http.zig
@@ -220,7 +220,7 @@ pub fn main() !void {
 
     defer _ = gpa_client.deinit();
 
-    server = Server.init(salloc, .{ .reuse_address = true });
+    server = Server.init(.{ .reuse_address = true });
 
     const addr = std.net.Address.parseIp("127.0.0.1", 0) catch unreachable;
     try server.listen(addr);