diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d69dab..24b4eb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - **breaking**: zig 0.16 — all networking APIs take `io: std.Io` as first parameter - **breaking**: streaming clients use `subscribe(handler)` pattern instead of `connect()` + `next()` loop - **breaking**: websocket.zig bumped — Io-native server accept loop, client write lock, TLS stream support +- **fix**: `DidResolver.resolve` and `HttpTransport.fetch` propagate the underlying `std.http.Client.fetch` error instead of collapsing every transport-layer failure (DNS, connect, TLS) to `error.DidResolutionFailed` / `error.RequestFailed` — callers can distinguish failure modes via `@errorName(err)`. soft-breaking: the inferred error set widens - **feat**: `Io.Timestamp` replaces libc `gettimeofday` in JWT/OAuth - **feat**: `io.sleep()` replaces libc `nanosleep` in reconnect backoff (cancellation-aware) - **docs**: [devlog 008](devlog/008-the-io-migration.md) — the 0.16 migration diff --git a/build.zig.zon b/build.zig.zon index d9aee99..277bde4 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = .zat, - .version = "0.3.0-alpha.22", + .version = "0.3.0-alpha.23", .fingerprint = 0x8da9db57ee82fbe4, .minimum_zig_version = "0.16.0-dev.3070+b22eb176b", .dependencies = .{ diff --git a/src/internal/identity/did_resolver.zig b/src/internal/identity/did_resolver.zig index a9c2e06..3b6cbe5 100644 --- a/src/internal/identity/did_resolver.zig +++ b/src/internal/identity/did_resolver.zig @@ -95,7 +95,7 @@ pub const DidResolver = struct { /// fetch and parse a did document from url fn fetchDidDocument(self: *DidResolver, url: []const u8) !DidDocument { - const result = self.transport.fetch(.{ .url = url }) catch return error.DidResolutionFailed; + const result = try self.transport.fetch(.{ .url = url }); defer self.allocator.free(result.body); if (result.status != .ok) { @@ -141,6 +141,39 @@ test "resolve did:plc - leak check (no arena)" { try std.testing.expectEqualStrings("did:plc:z72i7hdynmk6r22z27h6tvur", doc.id); } +test "regression: transport errors propagate distinct kinds" { + // before this fix, transport.fetch had `catch return error.RequestFailed` + // and fetchDidDocument had `catch return error.DidResolutionFailed`, so + // every transport-layer failure (DNS, TCP, TLS) collapsed to one + // indistinguishable error and callers had no way to see what was wrong. + // this regression test asserts the underlying error kind survives the + // resolver layer for at least one common transport failure mode. + // + // history: zlay 2026-04-08, where the host_authority pool failed at 100% + // and we had no production telemetry on which transport error fired + // because both layers had been swallowed. see relay docs/zlay-external- + // review-2026-04-09.md. + var resolver = DidResolver.init(std.Options.debug_io, std.testing.allocator); + defer resolver.deinit(); + + // 127.0.0.1:443 is almost certainly not listening on a test machine. + // did:web:127.0.0.1 → https://127.0.0.1/.well-known/did.json → connect refused. + const did = Did.parse("did:web:127.0.0.1") orelse return error.SkipZigTest; + if (resolver.resolve(did)) |doc| { + // someone is actually serving a DID doc on 127.0.0.1:443 — skip rather + // than fail, since the assertion below assumes a transport failure + var d = doc; + d.deinit(); + return error.SkipZigTest; + } else |err| { + // exact error name varies by platform (ConnectionRefused on linux/darwin, + // possibly different elsewhere). just assert it's not the catch-all that + // the pre-fix code returned for everything. + try std.testing.expect(err != error.DidResolutionFailed); + try std.testing.expect(err != error.RequestFailed); + } +} + test "did:web url construction" { // test url building without network var resolver = DidResolver.init(std.Options.debug_io, std.testing.allocator); diff --git a/src/internal/xrpc/transport.zig b/src/internal/xrpc/transport.zig index 5bcaa00..210b199 100644 --- a/src/internal/xrpc/transport.zig +++ b/src/internal/xrpc/transport.zig @@ -59,7 +59,7 @@ pub const HttpTransport = struct { } } - const result = self.http_client.fetch(.{ + const result = try self.http_client.fetch(.{ .location = .{ .url = options.url }, .response_writer = &aw.writer, .method = options.method, @@ -67,7 +67,7 @@ pub const HttpTransport = struct { .headers = headers, .extra_headers = extra_buf[0..extra_count], .keep_alive = self.keep_alive, - }) catch return error.RequestFailed; + }); return .{ .status = result.status,