diff --git a/src/internal/identity/handle_resolver.zig b/src/internal/identity/handle_resolver.zig index 0ebbbb2..451a22b 100644 --- a/src/internal/identity/handle_resolver.zig +++ b/src/internal/identity/handle_resolver.zig @@ -39,7 +39,7 @@ pub const HandleResolver = struct { if (self.resolveHttp(handle)) |did| { return did; } else |err| switch (err) { - error.UnsafeIdentityHost => return err, + error.UnsafeIdentityHost, error.Canceled, error.OutOfMemory => return err, else => return try self.resolveDns(handle), } } @@ -70,7 +70,10 @@ pub const HandleResolver = struct { .max_response_size = max_handle_response_size, .on_redirect = .refuse, .resolved_connection = checked_url.resolvedConnection(), - }) catch return error.HttpResolutionFailed; + }) catch |err| switch (err) { + error.Canceled, error.OutOfMemory => |e| return e, + else => return error.HttpResolutionFailed, + }; defer self.allocator.free(result.body); if (result.status != .ok) { @@ -109,7 +112,10 @@ pub const HandleResolver = struct { .accept = "application/dns-json", .max_response_size = max_dns_txt_response_size, .on_redirect = .refuse, - }) catch return error.DnsResolutionFailed; + }) catch |err| switch (err) { + error.Canceled, error.OutOfMemory => |e| return e, + else => return error.DnsResolutionFailed, + }; defer self.allocator.free(result.body); if (result.status != .ok) { @@ -301,3 +307,59 @@ test "resolve handle - integration" { try std.testing.expect(Did.parse(did) != null); try std.testing.expect(std.mem.startsWith(u8, did, "did:")); } + +test "resolveDns returns error.Canceled when canceled mid-response" { + const io = std.testing.io; + const allocator = std.testing.allocator; + + const Origin = struct { + listener: std.Io.net.Server, + stalled: std.Io.Event = .unset, + + // answers with a head that promises a body, then goes quiet until + // the client hangs up. + fn serve(self: *@This(), server_io: std.Io) void { + var conn = self.listener.accept(server_io) catch return; + defer conn.close(server_io); + var rbuf: [4096]u8 = undefined; + var reader = conn.reader(server_io, &rbuf); + var scratch: [2048]u8 = undefined; + var slices = [_][]u8{&scratch}; + _ = reader.interface.readVec(&slices) catch {}; + var wbuf: [256]u8 = undefined; + var writer = conn.writer(server_io, &wbuf); + writer.interface.writeAll("HTTP/1.1 200 OK\r\nContent-Length: 4096\r\n\r\n{") catch {}; + writer.interface.flush() catch {}; + self.stalled.set(server_io); + var sink: [1]u8 = undefined; + var tail = [_][]u8{&sink}; + _ = reader.interface.readVec(&tail) catch {}; + } + }; + + var origin: Origin = .{ + .listener = try (std.Io.net.IpAddress{ .ip4 = .loopback(0) }).listen(io, .{ .reuse_address = true }), + }; + defer origin.listener.deinit(io); + var server = try io.concurrent(Origin.serve, .{ &origin, io }); + defer server.cancel(io); + + var resolver = HandleResolver.init(io, allocator); + defer resolver.deinit(); + const endpoint = try std.fmt.allocPrint( + allocator, + "http://127.0.0.1:{d}/dns-query", + .{origin.listener.socket.address.getPort()}, + ); + defer allocator.free(endpoint); + resolver.doh_endpoint = endpoint; + + const handle = Handle.parse("example.com") orelse return error.InvalidHandle; + var lookup = try io.concurrent(HandleResolver.resolveDns, .{ &resolver, handle }); + try origin.stalled.wait(io); + // zttp only surfaces a cancel once it is streaming the body, so give the + // client time to get past the response head. + try io.sleep(.fromMilliseconds(200), .awake); + + try std.testing.expectError(error.Canceled, lookup.cancel(io)); +}