726 lines
18 KiB
Markdown
726 lines
18 KiB
Markdown
# std.http Reference (Zig 0.16.0)
|
|
|
|
HTTP client and server implementation with TLS, connection pooling, compression, and WebSocket support.
|
|
|
|
Primary Zig 0.16 release-note source: https://ziglang.org/download/0.16.0/release-notes.html
|
|
|
|
HTTP uses the new `std.Io` interface in Zig 0.16. Construct clients with `.io = io` and pass `io` through APIs that perform networking.
|
|
|
|
```zig
|
|
var client: std.http.Client = .{
|
|
.allocator = allocator,
|
|
.io = io,
|
|
};
|
|
defer client.deinit();
|
|
```
|
|
|
|
The client borrows both `allocator` and `io`; keep them valid through `client.deinit()`.
|
|
|
|
## Table of Contents
|
|
- [HTTP Client](#http-client)
|
|
- [HTTP Server](#http-server)
|
|
- [WebSocket](#websocket)
|
|
- [Core Types](#core-types)
|
|
- [Common Patterns](#common-patterns)
|
|
|
|
## HTTP Client
|
|
|
|
### Quick Fetch (Simple Requests)
|
|
|
|
```zig
|
|
const std = @import("std");
|
|
|
|
pub fn main(init: std.process.Init) !void {
|
|
var gpa: std.heap.DebugAllocator(.{}) = .init;
|
|
defer _ = gpa.deinit();
|
|
const allocator = gpa.allocator();
|
|
|
|
const io = init.io;
|
|
var client: std.http.Client = .{ .allocator = allocator, .io = io };
|
|
defer client.deinit();
|
|
|
|
// Simple GET - response body discarded
|
|
const result = try client.fetch(.{
|
|
.location = .{ .url = "https://example.com/api" },
|
|
});
|
|
std.debug.print("Status: {d}\n", .{@intFromEnum(result.status)});
|
|
}
|
|
```
|
|
|
|
### Fetch with Response Body
|
|
|
|
```zig
|
|
var client: std.http.Client = .{ .allocator = allocator, .io = io };
|
|
defer client.deinit();
|
|
|
|
// Create writer to capture response
|
|
var body_buf: [65536]u8 = undefined;
|
|
var body_writer: std.Io.Writer = .fixed(&body_buf);
|
|
|
|
const result = try client.fetch(.{
|
|
.location = .{ .url = "https://api.example.com/data" },
|
|
.response_writer = &body_writer,
|
|
});
|
|
|
|
const body = body_writer.buffered();
|
|
std.debug.print("Response ({d}): {s}\n", .{@intFromEnum(result.status), body});
|
|
```
|
|
|
|
### Fetch with POST Body
|
|
|
|
```zig
|
|
const result = try client.fetch(.{
|
|
.location = .{ .url = "https://api.example.com/submit" },
|
|
.method = .POST,
|
|
.payload = "{\"key\": \"value\"}",
|
|
.headers = .{
|
|
.content_type = .{ .override = "application/json" },
|
|
},
|
|
.response_writer = &body_writer,
|
|
});
|
|
```
|
|
|
|
### Full Request Control
|
|
|
|
For more control over the request lifecycle:
|
|
|
|
```zig
|
|
var client: std.http.Client = .{ .allocator = allocator, .io = io };
|
|
defer client.deinit();
|
|
|
|
const uri = try std.Uri.parse("https://api.example.com/resource");
|
|
|
|
var req = try client.request(.GET, uri, .{
|
|
.keep_alive = true,
|
|
.headers = .{
|
|
.authorization = .{ .override = "Bearer token123" },
|
|
},
|
|
.extra_headers = &.{
|
|
.{ .name = "X-Custom-Header", .value = "custom-value" },
|
|
},
|
|
});
|
|
defer req.deinit();
|
|
|
|
// Send request (no body for GET)
|
|
try req.sendBodiless();
|
|
|
|
// Receive response headers
|
|
var redirect_buf: [8192]u8 = undefined;
|
|
var response = try req.receiveHead(&redirect_buf);
|
|
|
|
std.debug.print("Status: {d} {s}\n", .{
|
|
@intFromEnum(response.head.status),
|
|
response.head.reason,
|
|
});
|
|
|
|
// Read response body
|
|
var reader_buf: [4096]u8 = undefined;
|
|
const body_reader = response.reader(&reader_buf);
|
|
|
|
while (true) {
|
|
const chunk = body_reader.take(4096) catch |err| switch (err) {
|
|
error.EndOfStream => break,
|
|
else => return err,
|
|
};
|
|
// process chunk...
|
|
}
|
|
```
|
|
|
|
### POST with Request Body
|
|
|
|
```zig
|
|
var req = try client.request(.POST, uri, .{});
|
|
defer req.deinit();
|
|
|
|
// Set content length and send body
|
|
const body = "request body content";
|
|
try req.sendBodyComplete(@constCast(body));
|
|
|
|
```
|
|
|
|
Streaming is a separate request lifecycle; do not call both body-send forms on one request:
|
|
|
|
```zig
|
|
var req = try client.request(.POST, uri, .{});
|
|
defer req.deinit();
|
|
req.transfer_encoding = .{ .content_length = body.len };
|
|
var body_writer_buf: [1024]u8 = undefined;
|
|
var bw = try req.sendBody(&body_writer_buf);
|
|
try bw.writer.writeAll(body);
|
|
try bw.end();
|
|
|
|
var response = try req.receiveHead(&.{});
|
|
```
|
|
|
|
### Chunked Transfer Encoding
|
|
|
|
```zig
|
|
var req = try client.request(.POST, uri, .{});
|
|
defer req.deinit();
|
|
|
|
req.transfer_encoding = .chunked;
|
|
var body_writer_buf: [1024]u8 = undefined;
|
|
var bw = try req.sendBody(&body_writer_buf);
|
|
|
|
// Write chunks
|
|
try bw.writer.writeAll("first chunk");
|
|
try bw.writer.writeAll("second chunk");
|
|
try bw.end(); // Sends final chunk marker
|
|
|
|
var response = try req.receiveHead(&.{});
|
|
```
|
|
|
|
### Decompressing Response Bodies
|
|
|
|
```zig
|
|
var response = try req.receiveHead(&redirect_buf);
|
|
|
|
var transfer_buf: [64]u8 = undefined;
|
|
var decompress: std.http.Decompress = undefined;
|
|
|
|
// Allocate decompression buffer based on content encoding
|
|
const decompress_buf = switch (response.head.content_encoding) {
|
|
.identity => &.{},
|
|
.zstd => try allocator.alloc(u8, std.compress.zstd.default_window_len),
|
|
.deflate, .gzip => try allocator.alloc(u8, std.compress.flate.max_window_len),
|
|
.compress => return error.UnsupportedCompression,
|
|
};
|
|
defer if (decompress_buf.len > 0) allocator.free(decompress_buf);
|
|
|
|
const reader = response.readerDecompressing(&transfer_buf, &decompress, decompress_buf);
|
|
// reader now returns decompressed bytes
|
|
```
|
|
|
|
### Redirect Handling
|
|
|
|
```zig
|
|
var req = try client.request(.GET, uri, .{
|
|
// Follow up to 5 redirects (default is 3)
|
|
.redirect_behavior = .init(5),
|
|
// Or disable redirects:
|
|
// .redirect_behavior = .not_allowed,
|
|
// Or handle manually:
|
|
// .redirect_behavior = .unhandled,
|
|
});
|
|
defer req.deinit();
|
|
|
|
try req.sendBodiless();
|
|
|
|
// redirect_buf stores the redirect location URI
|
|
var redirect_buf: [8192]u8 = undefined;
|
|
var response = try req.receiveHead(&redirect_buf);
|
|
|
|
// After redirects, req.uri contains the final URI
|
|
std.debug.print("Final URL: {s}\n", .{req.uri.path.raw});
|
|
```
|
|
|
|
### Connection Pooling
|
|
|
|
Connections are automatically pooled and reused:
|
|
|
|
```zig
|
|
var client: std.http.Client = .{ .allocator = allocator, .io = io };
|
|
defer client.deinit();
|
|
|
|
// Configure pool size (default 32)
|
|
client.connection_pool.free_size = 64;
|
|
|
|
// Configure buffer sizes
|
|
client.read_buffer_size = 16384; // default 8192
|
|
client.write_buffer_size = 2048; // default 1024
|
|
|
|
// Connections are reused when host/port/protocol match
|
|
for (0..10) |_| {
|
|
var req = try client.request(.GET, uri, .{ .keep_alive = true });
|
|
defer req.deinit();
|
|
try req.sendBodiless();
|
|
var response = try req.receiveHead(&.{});
|
|
var discard_buf: [1024]u8 = undefined;
|
|
_ = try response.reader(&discard_buf).discardRemaining();
|
|
// req.deinit() returns an eligible keep-alive connection to the pool.
|
|
}
|
|
```
|
|
|
|
### Proxy Configuration
|
|
|
|
```zig
|
|
var client: std.http.Client = .{ .allocator = allocator, .io = io };
|
|
defer client.deinit();
|
|
|
|
// Load from environment (HTTP_PROXY, HTTPS_PROXY, etc.)
|
|
var arena = std.heap.ArenaAllocator.init(allocator);
|
|
defer arena.deinit();
|
|
// `environ_map` and the arena-backed proxy strings must outlive the client.
|
|
try client.initDefaultProxies(arena.allocator(), &environ_map);
|
|
|
|
// Or configure manually:
|
|
var proxy: std.http.Proxy = .{
|
|
.protocol = .plain,
|
|
.host = "proxy.example.com",
|
|
.port = 8080,
|
|
.authorization = null, // or "Basic base64credentials"
|
|
.supports_connect = true,
|
|
};
|
|
client.http_proxy = &proxy;
|
|
```
|
|
|
|
### TLS Configuration
|
|
|
|
```zig
|
|
var client: std.http.Client = .{ .allocator = allocator, .io = io };
|
|
defer client.deinit();
|
|
|
|
// TLS is enabled by default for https://
|
|
// Configure TLS buffer size (affects memory usage)
|
|
client.tls_buffer_size = std.crypto.tls.Client.min_buffer_len;
|
|
|
|
// Force time/root-certificate freshness to be reconsidered on the next HTTPS request.
|
|
client.now = null;
|
|
|
|
// Disable TLS at compile time via std.options.http_disable_tls
|
|
```
|
|
|
|
## HTTP Server
|
|
|
|
### Basic Server
|
|
|
|
```zig
|
|
const std = @import("std");
|
|
const net = std.Io.net;
|
|
const http = std.http;
|
|
|
|
pub fn main(init: std.process.Init) !void {
|
|
const io = init.io;
|
|
const address = try net.IpAddress.parseIp4("127.0.0.1", 8080);
|
|
var tcp_server = try address.listen(io, .{});
|
|
defer tcp_server.deinit(io);
|
|
|
|
while (true) {
|
|
const conn = try tcp_server.accept(io);
|
|
defer conn.close(io);
|
|
|
|
var read_buf: [8192]u8 = undefined;
|
|
var write_buf: [4096]u8 = undefined;
|
|
|
|
var reader = conn.reader(io, &read_buf);
|
|
var writer = conn.writer(io, &write_buf);
|
|
|
|
var server = http.Server.init(&reader.interface, &writer.interface);
|
|
|
|
const request = server.receiveHead() catch |err| {
|
|
std.debug.print("Failed to receive: {}\n", .{err});
|
|
continue;
|
|
};
|
|
|
|
try handleRequest(&request);
|
|
}
|
|
}
|
|
|
|
fn handleRequest(request: *http.Server.Request) !void {
|
|
const head = request.head;
|
|
std.debug.print("{s} {s}\n", .{@tagName(head.method), head.target});
|
|
|
|
// Simple response
|
|
try request.respond("Hello, World!", .{
|
|
.status = .ok,
|
|
.extra_headers = &.{
|
|
.{ .name = "Content-Type", .value = "text/plain" },
|
|
},
|
|
});
|
|
}
|
|
```
|
|
|
|
### Reading Request Body
|
|
|
|
```zig
|
|
fn handleRequest(request: *http.Server.Request) !void {
|
|
// Handle Expect: 100-continue
|
|
var body_buf: [4096]u8 = undefined;
|
|
const body_reader = try request.readerExpectContinue(&body_buf);
|
|
|
|
// Read entire body
|
|
var body: std.ArrayList(u8) = .empty;
|
|
defer body.deinit(allocator);
|
|
|
|
while (true) {
|
|
const chunk = body_reader.take(1024) catch |err| switch (err) {
|
|
error.EndOfStream => break,
|
|
else => return err,
|
|
};
|
|
try body.appendSlice(allocator, chunk);
|
|
}
|
|
|
|
try request.respond("Received", .{});
|
|
}
|
|
```
|
|
|
|
### Streaming Response
|
|
|
|
```zig
|
|
fn handleRequest(request: *http.Server.Request) !void {
|
|
var response_buf: [4096]u8 = undefined;
|
|
|
|
// Start streaming response (uses chunked transfer encoding by default)
|
|
var body = try request.respondStreaming(&response_buf, .{
|
|
.respond_options = .{
|
|
.status = .ok,
|
|
.extra_headers = &.{
|
|
.{ .name = "Content-Type", .value = "text/event-stream" },
|
|
},
|
|
},
|
|
});
|
|
|
|
// Write chunks
|
|
try body.writer.writeAll("data: chunk 1\n\n");
|
|
try body.flush();
|
|
|
|
try body.writer.writeAll("data: chunk 2\n\n");
|
|
try body.end(); // Finishes chunked response
|
|
}
|
|
```
|
|
|
|
### Response with Known Length
|
|
|
|
```zig
|
|
fn handleRequest(request: *http.Server.Request) !void {
|
|
const content = "Fixed length response";
|
|
|
|
var response_buf: [1024]u8 = undefined;
|
|
var body = try request.respondStreaming(&response_buf, .{
|
|
.content_length = content.len, // Uses Content-Length instead of chunked
|
|
.respond_options = .{ .status = .ok },
|
|
});
|
|
|
|
try body.writer.writeAll(content);
|
|
try body.end();
|
|
}
|
|
```
|
|
|
|
### Iterating Headers
|
|
|
|
```zig
|
|
fn handleRequest(request: *http.Server.Request) !void {
|
|
var it = request.iterateHeaders();
|
|
while (it.next()) |header| {
|
|
std.debug.print("{s}: {s}\n", .{header.name, header.value});
|
|
}
|
|
|
|
try request.respond("OK", .{});
|
|
}
|
|
```
|
|
|
|
## WebSocket
|
|
|
|
### Server-Side WebSocket Upgrade
|
|
|
|
```zig
|
|
fn handleRequest(request: *http.Server.Request) !void {
|
|
const upgrade = request.upgradeRequested();
|
|
|
|
switch (upgrade) {
|
|
.websocket => |key| {
|
|
if (key) |k| {
|
|
var ws = try request.respondWebSocket(.{ .key = k });
|
|
try handleWebSocket(&ws);
|
|
} else {
|
|
try request.respond("Missing key", .{ .status = .bad_request });
|
|
}
|
|
},
|
|
.other => |name| {
|
|
std.debug.print("Unknown upgrade: {s}\n", .{name});
|
|
try request.respond("Not supported", .{ .status = .bad_request });
|
|
},
|
|
.none => {
|
|
try request.respond("Expected WebSocket", .{ .status = .bad_request });
|
|
},
|
|
}
|
|
}
|
|
|
|
fn handleWebSocket(ws: *http.Server.WebSocket) !void {
|
|
try ws.flush(); // Send upgrade response
|
|
|
|
while (true) {
|
|
const msg = ws.readSmallMessage() catch |err| switch (err) {
|
|
error.ConnectionClose => break,
|
|
else => return err,
|
|
};
|
|
|
|
switch (msg.opcode) {
|
|
.text, .binary => {
|
|
// Echo back
|
|
try ws.writeMessage(msg.data, msg.opcode);
|
|
},
|
|
.ping => {
|
|
try ws.writeMessage(msg.data, .pong);
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### WebSocket Message Types
|
|
|
|
```zig
|
|
// Opcodes
|
|
const Opcode = enum(u4) {
|
|
continuation = 0,
|
|
text = 1,
|
|
binary = 2,
|
|
connection_close = 8,
|
|
ping = 9,
|
|
pong = 10,
|
|
};
|
|
|
|
// Write different message types
|
|
try ws.writeMessage("Hello", .text);
|
|
try ws.writeMessage(&binary_data, .binary);
|
|
try ws.writeMessage("", .ping);
|
|
|
|
// Unflushed writes (batch multiple messages)
|
|
try ws.writeMessageUnflushed("msg1", .text);
|
|
try ws.writeMessageUnflushed("msg2", .text);
|
|
try ws.flush();
|
|
```
|
|
|
|
## Core Types
|
|
|
|
### HTTP Methods
|
|
|
|
```zig
|
|
const Method = enum {
|
|
GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH,
|
|
|
|
pub fn requestHasBody(m: Method) bool; // POST, PUT, PATCH
|
|
pub fn responseHasBody(m: Method) bool; // GET, POST, PUT, DELETE, CONNECT, OPTIONS, PATCH
|
|
pub fn safe(m: Method) bool; // GET, HEAD, OPTIONS, TRACE
|
|
pub fn idempotent(m: Method) bool; // GET, HEAD, PUT, DELETE, OPTIONS, TRACE
|
|
pub fn cacheable(m: Method) bool; // GET, HEAD
|
|
};
|
|
```
|
|
|
|
### HTTP Status Codes
|
|
|
|
```zig
|
|
const Status = enum(u10) {
|
|
// 1xx Informational
|
|
@"continue" = 100,
|
|
switching_protocols = 101,
|
|
|
|
// 2xx Success
|
|
ok = 200,
|
|
created = 201,
|
|
accepted = 202,
|
|
no_content = 204,
|
|
|
|
// 3xx Redirection
|
|
moved_permanently = 301,
|
|
found = 302,
|
|
see_other = 303,
|
|
not_modified = 304,
|
|
temporary_redirect = 307,
|
|
permanent_redirect = 308,
|
|
|
|
// 4xx Client Error
|
|
bad_request = 400,
|
|
unauthorized = 401,
|
|
forbidden = 403,
|
|
not_found = 404,
|
|
method_not_allowed = 405,
|
|
too_many_requests = 429,
|
|
|
|
// 5xx Server Error
|
|
internal_server_error = 500,
|
|
not_implemented = 501,
|
|
bad_gateway = 502,
|
|
service_unavailable = 503,
|
|
|
|
_, // Non-exhaustive for custom codes
|
|
|
|
pub fn phrase(self: Status) ?[]const u8;
|
|
pub fn class(self: Status) Class;
|
|
};
|
|
|
|
const Class = enum { informational, success, redirect, client_error, server_error };
|
|
```
|
|
|
|
### Content Encoding
|
|
|
|
```zig
|
|
const ContentEncoding = enum {
|
|
zstd,
|
|
gzip,
|
|
deflate,
|
|
compress,
|
|
identity,
|
|
|
|
pub fn fromString(s: []const u8) ?ContentEncoding;
|
|
pub fn minBufferCapacity(ce: ContentEncoding) usize;
|
|
};
|
|
```
|
|
|
|
### Transfer Encoding
|
|
|
|
```zig
|
|
const TransferEncoding = enum {
|
|
chunked,
|
|
none,
|
|
};
|
|
```
|
|
|
|
### Header Struct
|
|
|
|
```zig
|
|
const Header = struct {
|
|
name: []const u8,
|
|
value: []const u8,
|
|
};
|
|
```
|
|
|
|
## Common Patterns
|
|
|
|
### JSON API Client
|
|
|
|
```zig
|
|
fn fetchJson(comptime T: type, io: std.Io, allocator: Allocator, url: []const u8) !std.json.Parsed(T) {
|
|
var client: std.http.Client = .{ .allocator = allocator, .io = io };
|
|
defer client.deinit();
|
|
|
|
var body_buf: [65536]u8 = undefined;
|
|
var body_writer: std.Io.Writer = .fixed(&body_buf);
|
|
|
|
const result = try client.fetch(.{
|
|
.location = .{ .url = url },
|
|
.headers = .{
|
|
.content_type = .{ .override = "application/json" },
|
|
},
|
|
.response_writer = &body_writer,
|
|
});
|
|
|
|
if (result.status != .ok) return error.HttpError;
|
|
|
|
// alloc_always prevents returned strings from borrowing body_buf. The
|
|
// caller owns the returned Parsed(T) and must call deinit().
|
|
return std.json.parseFromSlice(T, allocator, body_writer.buffered(), .{
|
|
.allocate = .alloc_always,
|
|
});
|
|
}
|
|
```
|
|
|
|
### POST JSON Data
|
|
|
|
```zig
|
|
fn postJson(io: std.Io, allocator: Allocator, url: []const u8, data: anytype) !void {
|
|
const json = try std.json.stringifyAlloc(allocator, data, .{});
|
|
defer allocator.free(json);
|
|
|
|
var client: std.http.Client = .{ .allocator = allocator, .io = io };
|
|
defer client.deinit();
|
|
|
|
const result = try client.fetch(.{
|
|
.location = .{ .url = url },
|
|
.method = .POST,
|
|
.payload = json,
|
|
.headers = .{
|
|
.content_type = .{ .override = "application/json" },
|
|
},
|
|
});
|
|
|
|
if (result.status.class() != .success) return error.HttpError;
|
|
}
|
|
```
|
|
|
|
### Download File
|
|
|
|
```zig
|
|
fn downloadFile(io: std.Io, allocator: Allocator, url: []const u8, path: []const u8) !void {
|
|
var client: std.http.Client = .{ .allocator = allocator, .io = io };
|
|
defer client.deinit();
|
|
|
|
const uri = try std.Uri.parse(url);
|
|
var req = try client.request(.GET, uri, .{});
|
|
defer req.deinit();
|
|
|
|
try req.sendBodiless();
|
|
|
|
var redirect_buf: [8192]u8 = undefined;
|
|
var response = try req.receiveHead(&redirect_buf);
|
|
|
|
if (response.head.status != .ok) return error.HttpError;
|
|
|
|
const file = try std.Io.Dir.cwd().createFile(io, path, .{});
|
|
defer file.close(io);
|
|
|
|
var file_buf: [4096]u8 = undefined;
|
|
var file_writer = file.writer(io, &file_buf);
|
|
|
|
var reader_buf: [4096]u8 = undefined;
|
|
const body_reader = response.reader(&reader_buf);
|
|
|
|
_ = body_reader.streamRemaining(&file_writer.interface) catch |err| switch (err) {
|
|
error.ReadFailed => return response.bodyErr().?,
|
|
else => return err,
|
|
};
|
|
|
|
try file_writer.interface.flush();
|
|
}
|
|
```
|
|
|
|
### Simple REST Server
|
|
|
|
```zig
|
|
fn handleApi(request: *http.Server.Request) !void {
|
|
const head = request.head;
|
|
|
|
if (std.mem.eql(u8, head.target, "/api/health")) {
|
|
try request.respond("{\"status\":\"ok\"}", .{
|
|
.extra_headers = &.{
|
|
.{ .name = "Content-Type", .value = "application/json" },
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (std.mem.startsWith(u8, head.target, "/api/users")) {
|
|
switch (head.method) {
|
|
.GET => try handleGetUsers(request),
|
|
.POST => try handleCreateUser(request),
|
|
else => try request.respond("", .{ .status = .method_not_allowed }),
|
|
}
|
|
return;
|
|
}
|
|
|
|
try request.respond("Not Found", .{ .status = .not_found });
|
|
}
|
|
```
|
|
|
|
### Error Handling
|
|
|
|
```zig
|
|
fn makeRequest(client: *std.http.Client, uri: std.Uri) ![]const u8 {
|
|
var req = client.request(.GET, uri, .{}) catch |err| switch (err) {
|
|
error.ConnectionRefused => return error.ServerDown,
|
|
error.TlsInitializationFailed => return error.TlsError,
|
|
error.UnknownHostName => return error.DnsError,
|
|
else => return err,
|
|
};
|
|
defer req.deinit();
|
|
|
|
try req.sendBodiless();
|
|
|
|
var buf: [8192]u8 = undefined;
|
|
var response = req.receiveHead(&buf) catch |err| switch (err) {
|
|
error.HttpHeadersOversize => return error.ResponseTooLarge,
|
|
error.HttpHeadersInvalid => return error.MalformedResponse,
|
|
error.TooManyHttpRedirects => return error.RedirectLoop,
|
|
else => return err,
|
|
};
|
|
|
|
if (response.head.status.class() != .success) {
|
|
return error.HttpError;
|
|
}
|
|
|
|
// ...
|
|
}
|
|
```
|