diff options
| author | Nameless <truemedian@gmail.com> | 2023-12-14 15:52:39 -0600 |
|---|---|---|
| committer | Andrew Kelley <andrew@ziglang.org> | 2024-01-13 18:51:38 -0800 |
| commit | b723296e1fa65b73a43b0790bdddcbfcea7d656d (patch) | |
| tree | 33173696d644f655374e134b73df3d694e4a1a28 /lib/std | |
| parent | 832f6d8f7f7f7b10b86b109a8b26bb5eacc8d13e (diff) | |
| download | zig-b723296e1fa65b73a43b0790bdddcbfcea7d656d.tar.gz zig-b723296e1fa65b73a43b0790bdddcbfcea7d656d.zip | |
std.http: add missing documentation and a few examples
Diffstat (limited to 'lib/std')
| -rw-r--r-- | lib/std/http/Client.zig | 53 | ||||
| -rw-r--r-- | lib/std/http/Headers.zig | 13 | ||||
| -rw-r--r-- | lib/std/http/Server.zig | 86 |
3 files changed, 140 insertions, 12 deletions
diff --git a/lib/std/http/Client.zig b/lib/std/http/Client.zig index 9b66975a09..14bd96a68b 100644 --- a/lib/std/http/Client.zig +++ b/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 }, diff --git a/lib/std/http/Headers.zig b/lib/std/http/Headers.zig index bf0b0755d6..c35775e65b 100644 --- a/lib/std/http/Headers.zig +++ b/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); diff --git a/lib/std/http/Server.zig b/lib/std/http/Server.zig index 6928606b1b..48c4e2cbfb 100644 --- a/lib/std/http/Server.zig +++ b/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); |
