atproto utils for zig zat.dev
atproto sdk zig
26
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: isolate HTTP I/O behind HttpTransport for 0.16 prep

zig 0.16 moves std.http to std.Io interface. this isolates all HTTP
client usage behind a single transport layer so migration is one file.

- add src/internal/transport.zig with HttpTransport wrapper
- migrate XrpcClient, DidResolver, HandleResolver to use it
- no public API changes, downstream consumers unaffected

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

zzstoatzz 89fc63ec 4788708e

+146 -57
+7 -11
src/internal/did_resolver.zig
··· 7 7 const std = @import("std"); 8 8 const Did = @import("did.zig").Did; 9 9 const DidDocument = @import("did_document.zig").DidDocument; 10 + const HttpTransport = @import("transport.zig").HttpTransport; 10 11 11 12 pub const DidResolver = struct { 12 13 allocator: std.mem.Allocator, 13 - http_client: std.http.Client, 14 + transport: HttpTransport, 14 15 15 16 /// plc directory url (default: https://plc.directory) 16 17 plc_url: []const u8 = "https://plc.directory", ··· 18 19 pub fn init(allocator: std.mem.Allocator) DidResolver { 19 20 return .{ 20 21 .allocator = allocator, 21 - .http_client = .{ .allocator = allocator }, 22 + .transport = HttpTransport.init(allocator), 22 23 }; 23 24 } 24 25 25 26 pub fn deinit(self: *DidResolver) void { 26 - self.http_client.deinit(); 27 + self.transport.deinit(); 27 28 } 28 29 29 30 /// resolve a did to its document ··· 84 85 85 86 /// fetch and parse a did document from url 86 87 fn fetchDidDocument(self: *DidResolver, url: []const u8) !DidDocument { 87 - var aw: std.Io.Writer.Allocating = .init(self.allocator); 88 - defer aw.deinit(); 89 - 90 - const result = self.http_client.fetch(.{ 91 - .location = .{ .url = url }, 92 - .response_writer = &aw.writer, 93 - }) catch return error.DidResolutionFailed; 88 + const result = self.transport.fetch(.{ .url = url }) catch return error.DidResolutionFailed; 89 + defer self.allocator.free(result.body); 94 90 95 91 if (result.status != .ok) { 96 92 return error.DidResolutionFailed; 97 93 } 98 94 99 - return try DidDocument.parse(self.allocator, aw.toArrayList().items); 95 + return try DidDocument.parse(self.allocator, result.body); 100 96 } 101 97 }; 102 98
+12 -22
src/internal/handle_resolver.zig
··· 11 11 const std = @import("std"); 12 12 const Handle = @import("handle.zig").Handle; 13 13 const Did = @import("did.zig").Did; 14 + const HttpTransport = @import("transport.zig").HttpTransport; 14 15 15 16 pub const HandleResolver = struct { 16 17 allocator: std.mem.Allocator, 17 - http_client: std.http.Client, 18 + transport: HttpTransport, 18 19 doh_endpoint: []const u8, 19 20 20 21 pub fn init(allocator: std.mem.Allocator) HandleResolver { 21 22 return .{ 22 23 .allocator = allocator, 23 - .http_client = .{ .allocator = allocator }, 24 + .transport = HttpTransport.init(allocator), 24 25 .doh_endpoint = "https://cloudflare-dns.com/dns-query", 25 26 }; 26 27 } 27 28 28 29 pub fn deinit(self: *HandleResolver) void { 29 - self.http_client.deinit(); 30 + self.transport.deinit(); 30 31 } 31 32 32 33 /// resolve a handle to a DID via HTTP well-known ··· 47 48 ); 48 49 defer self.allocator.free(url); 49 50 50 - var aw: std.Io.Writer.Allocating = .init(self.allocator); 51 - defer aw.deinit(); 52 - 53 - const result = self.http_client.fetch(.{ 54 - .location = .{ .url = url }, 55 - .response_writer = &aw.writer, 56 - }) catch return error.HttpResolutionFailed; 51 + const result = self.transport.fetch(.{ .url = url }) catch return error.HttpResolutionFailed; 52 + defer self.allocator.free(result.body); 57 53 58 54 if (result.status != .ok) { 59 55 return error.HttpResolutionFailed; 60 56 } 61 57 62 58 // response body should be the DID as plain text 63 - const did_str = std.mem.trim(u8, aw.toArrayList().items, &std.ascii.whitespace); 59 + const did_str = std.mem.trim(u8, result.body, &std.ascii.whitespace); 64 60 65 61 // validate it's a proper DID 66 62 if (Did.parse(did_str) == null) { ··· 86 82 ); 87 83 defer self.allocator.free(url); 88 84 89 - var aw: std.io.Writer.Allocating = .init(self.allocator); 90 - defer aw.deinit(); 91 - 92 - const result = self.http_client.fetch(.{ 93 - .location = .{ .url = url }, 94 - .extra_headers = &.{ 95 - .{ .name = "accept", .value = "application/dns-json" }, 96 - }, 97 - .response_writer = &aw.writer, 85 + const result = self.transport.fetch(.{ 86 + .url = url, 87 + .accept = "application/dns-json", 98 88 }) catch return error.DnsResolutionFailed; 89 + defer self.allocator.free(result.body); 99 90 100 91 if (result.status != .ok) { 101 92 return error.DnsResolutionFailed; 102 93 } 103 94 104 - const response_body = aw.toArrayList().items; 105 95 const parsed = std.json.parseFromSlice( 106 96 DnsResponse, 107 97 self.allocator, 108 - response_body, 98 + result.body, 109 99 .{}, 110 100 ) catch return error.InvalidDnsResponse; 111 101 defer parsed.deinit();
+114
src/internal/transport.zig
··· 1 + //! HTTP Transport - isolates HTTP client for 0.16 migration 2 + //! 3 + //! wraps std.http.Client to provide a single point of change 4 + //! when zig 0.16 moves HTTP to std.Io interface. 5 + //! 6 + //! 0.16 migration plan: 7 + //! - add `io: std.Io` field 8 + //! - add `initWithIo(io: std.Io, allocator: Allocator)` constructor 9 + //! - update fetch() to use io.http or equivalent 10 + 11 + const std = @import("std"); 12 + 13 + pub const HttpTransport = struct { 14 + allocator: std.mem.Allocator, 15 + http_client: std.http.Client, 16 + 17 + // 0.16: will add 18 + // io: ?std.Io = null, 19 + 20 + pub fn init(allocator: std.mem.Allocator) HttpTransport { 21 + return .{ 22 + .allocator = allocator, 23 + .http_client = .{ .allocator = allocator }, 24 + }; 25 + } 26 + 27 + // 0.16: will add 28 + // pub fn initWithIo(io: std.Io, allocator: std.mem.Allocator) HttpTransport { 29 + // return .{ 30 + // .allocator = allocator, 31 + // .http_client = .{ .allocator = allocator }, 32 + // .io = io, 33 + // }; 34 + // } 35 + 36 + pub fn deinit(self: *HttpTransport) void { 37 + self.http_client.deinit(); 38 + } 39 + 40 + /// fetch a URL and write response to provided writer 41 + pub fn fetch(self: *HttpTransport, options: FetchOptions) !FetchResult { 42 + var aw: std.Io.Writer.Allocating = .init(self.allocator); 43 + defer aw.deinit(); 44 + 45 + var headers: std.http.Client.Request.Headers = .{ 46 + .accept_encoding = .{ .override = "identity" }, // disable gzip - zig stdlib issue 47 + .content_type = if (options.payload != null) .{ .override = "application/json" } else .default, 48 + }; 49 + 50 + // apply custom headers 51 + if (options.authorization) |auth| { 52 + headers.authorization = .{ .override = auth }; 53 + } 54 + if (options.content_type) |ct| { 55 + headers.content_type = .{ .override = ct }; 56 + } 57 + 58 + // build extra headers array for accept and any custom headers 59 + var extra_buf: [8]std.http.Header = undefined; 60 + var extra_count: usize = 0; 61 + 62 + if (options.accept) |accept| { 63 + extra_buf[extra_count] = .{ .name = "accept", .value = accept }; 64 + extra_count += 1; 65 + } 66 + 67 + if (options.extra_headers) |hdrs| { 68 + for (hdrs) |h| { 69 + if (extra_count < extra_buf.len) { 70 + extra_buf[extra_count] = h; 71 + extra_count += 1; 72 + } 73 + } 74 + } 75 + 76 + const result = self.http_client.fetch(.{ 77 + .location = .{ .url = options.url }, 78 + .response_writer = &aw.writer, 79 + .method = options.method, 80 + .payload = options.payload, 81 + .headers = headers, 82 + .extra_headers = extra_buf[0..extra_count], 83 + }) catch return error.RequestFailed; 84 + 85 + const body = aw.toArrayList().items; 86 + 87 + return .{ 88 + .status = result.status, 89 + .body = try self.allocator.dupe(u8, body), 90 + }; 91 + } 92 + 93 + pub const FetchOptions = struct { 94 + url: []const u8, 95 + method: std.http.Method = .GET, 96 + payload: ?[]const u8 = null, 97 + authorization: ?[]const u8 = null, 98 + accept: ?[]const u8 = null, 99 + content_type: ?[]const u8 = null, 100 + extra_headers: ?[]const std.http.Header = null, 101 + }; 102 + 103 + pub const FetchResult = struct { 104 + status: std.http.Status, 105 + body: []u8, 106 + }; 107 + }; 108 + 109 + // === tests === 110 + 111 + test "transport init/deinit" { 112 + var transport = HttpTransport.init(std.testing.allocator); 113 + defer transport.deinit(); 114 + }
+13 -24
src/internal/xrpc.zig
··· 7 7 8 8 const std = @import("std"); 9 9 const Nsid = @import("nsid.zig").Nsid; 10 + const HttpTransport = @import("transport.zig").HttpTransport; 10 11 11 12 pub const XrpcClient = struct { 12 13 allocator: std.mem.Allocator, 13 - http_client: std.http.Client, 14 + transport: HttpTransport, 14 15 15 16 /// pds or appview host (e.g., "https://bsky.social") 16 17 host: []const u8, ··· 24 25 pub fn init(allocator: std.mem.Allocator, host: []const u8) XrpcClient { 25 26 return .{ 26 27 .allocator = allocator, 27 - .http_client = .{ .allocator = allocator }, 28 + .transport = HttpTransport.init(allocator), 28 29 .host = host, 29 30 }; 30 31 } 31 32 32 33 pub fn deinit(self: *XrpcClient) void { 33 - self.http_client.deinit(); 34 + self.transport.deinit(); 34 35 } 35 36 36 37 /// set bearer token for authenticated requests ··· 85 86 } 86 87 87 88 fn doRequest(self: *XrpcClient, url: []const u8, body: ?[]const u8) !Response { 88 - var aw: std.Io.Writer.Allocating = .init(self.allocator); 89 - defer aw.deinit(); 90 - 91 - // disable gzip: zig stdlib flate.Decompress panics on certain streams 92 - // https://github.com/ziglang/zig/issues/25021 93 - var extra_headers: std.http.Client.Request.Headers = .{ 94 - .accept_encoding = .{ .override = "identity" }, 95 - .content_type = if (body != null) .{ .override = "application/json" } else .default, 96 - }; 97 89 var auth_header_buf: [max_auth_header_len]u8 = undefined; 98 - if (self.access_token) |token| { 99 - const auth_value = try std.fmt.bufPrint(&auth_header_buf, "Bearer {s}", .{token}); 100 - extra_headers.authorization = .{ .override = auth_value }; 101 - } 90 + const auth_value: ?[]const u8 = if (self.access_token) |token| 91 + std.fmt.bufPrint(&auth_header_buf, "Bearer {s}", .{token}) catch null 92 + else 93 + null; 102 94 103 - const result = self.http_client.fetch(.{ 104 - .location = .{ .url = url }, 105 - .response_writer = &aw.writer, 95 + const result = try self.transport.fetch(.{ 96 + .url = url, 106 97 .method = if (body != null) .POST else .GET, 107 98 .payload = body, 108 - .headers = extra_headers, 109 - }) catch return error.RequestFailed; 110 - 111 - const response_body = aw.toArrayList().items; 99 + .authorization = auth_value, 100 + }); 112 101 113 102 return .{ 114 103 .allocator = self.allocator, 115 104 .status = result.status, 116 - .body = try self.allocator.dupe(u8, response_body), 105 + .body = result.body, 117 106 }; 118 107 } 119 108