From 4a25e9a06a9db281b1ae37c21cc8da25288cbdbc Mon Sep 17 00:00:00 2001 From: noriaki watanabe Date: Fri, 8 May 2026 08:25:27 +0900 Subject: [PATCH 1/3] refactor: make handle resolution methods return non-optional values --- Sources/ATResolve/ATResolver.swift | 9 +++------ Tests/ATResolveTests/ATResolveTests.swift | 8 ++++---- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/Sources/ATResolve/ATResolver.swift b/Sources/ATResolve/ATResolver.swift index c3bfad6..3b0968a 100644 --- a/Sources/ATResolve/ATResolver.swift +++ b/Sources/ATResolve/ATResolver.swift @@ -56,7 +56,7 @@ public struct ATResolver { return didRecord?.txt.components(separatedBy: "=").last } - public func didForHandle(_ handle: String) async throws -> String? { + public func didForHandle(_ handle: String) async throws -> String { if let did = try await didForDomain(handle) { return did } @@ -83,11 +83,8 @@ public struct ATResolver { ) } - public func resolveHandle(_ handle: String) async throws -> ResolvedData? { - guard let did = try await didForHandle(handle) else { - return nil - } - + public func resolveHandle(_ handle: String) async throws -> ResolvedData { + let did = try await didForHandle(handle) print("did: \(did)") let directoryResult = try await plcDirectoryQuery(did) diff --git a/Tests/ATResolveTests/ATResolveTests.swift b/Tests/ATResolveTests/ATResolveTests.swift index a5f39b1..0d75c1d 100644 --- a/Tests/ATResolveTests/ATResolveTests.swift +++ b/Tests/ATResolveTests/ATResolveTests.swift @@ -12,9 +12,9 @@ struct ATResolveTests { let data = try await resolver.resolveHandle("massicotte.org") - #expect(data?.did == "did:plc:klsh7edzj3jmxucibyjqstb3") - #expect(data?.handle == "massicotte.org") - #expect(data?.serviceEndpoint == "https://milkcap.us-west.host.bsky.network") + #expect(data.did == "did:plc:klsh7edzj3jmxucibyjqstb3") + #expect(data.handle == "massicotte.org") + #expect(data.serviceEndpoint == "https://milkcap.us-west.host.bsky.network") } @Test @@ -40,7 +40,7 @@ struct ATResolveTests { let profile = try await resolver.resolveHandle("cjrdev.bsky.social") - #expect(profile != nil) + #expect(profile.did == "did:plc:wlef3srsa3hlyzj2hy6yncrh") } @Test func decodeWithCustomProvider() async throws { From a00cc7fb6d4497518e3913b72e9fb0169db97f64 Mon Sep 17 00:00:00 2001 From: noriaki watanabe Date: Wed, 27 May 2026 07:41:32 +0900 Subject: [PATCH 2/3] fix: return nil instead of throwing for nonexistent handles --- Sources/ATResolve/ATResolver.swift | 34 +++++++++++++++-------- Sources/ATResolve/Networking.swift | 7 +++-- Tests/ATResolveTests/ATResolveTests.swift | 19 +++++++++---- 3 files changed, 42 insertions(+), 18 deletions(-) diff --git a/Sources/ATResolve/ATResolver.swift b/Sources/ATResolve/ATResolver.swift index 3b0968a..86a7261 100644 --- a/Sources/ATResolve/ATResolver.swift +++ b/Sources/ATResolve/ATResolver.swift @@ -5,6 +5,11 @@ enum ATResolverError: Error { case requestFailed } +struct XRPCError: Decodable, Error { + let error: String + let message: String? +} + public struct ResolvedData: Codable, Hashable, Sendable { public let did: String public let handle: String @@ -56,21 +61,26 @@ public struct ATResolver { return didRecord?.txt.components(separatedBy: "=").last } - public func didForHandle(_ handle: String) async throws -> String { + public func didForHandle(_ handle: String) async throws -> String? { if let did = try await didForDomain(handle) { return did } - return try await blueskyGetProfile(handle).did + return try await blueskyGetProfile(handle)?.did } - public func blueskyGetProfile(_ actor: String) async throws -> BlueskyProfile { - try await provider.decodeJSON( - host: "public.api.bsky.app", - path: "/xrpc/app.bsky.actor.getProfile", - headers: ["Accept": "application/json"], - queryItems: [("actor", actor)] - ) + public func blueskyGetProfile(_ actor: String) async throws -> BlueskyProfile? { + do { + return try await provider.decodeJSON( + host: "public.api.bsky.app", + path: "/xrpc/app.bsky.actor.getProfile", + headers: ["Accept": "application/json"], + queryItems: [("actor", actor)] + ) + } catch let error as XRPCError + where error.error == "InvalidRequest" && error.message?.localizedCaseInsensitiveContains("not found") == true { + return nil + } } public func plcDirectoryQuery( @@ -83,8 +93,10 @@ public struct ATResolver { ) } - public func resolveHandle(_ handle: String) async throws -> ResolvedData { - let did = try await didForHandle(handle) + public func resolveHandle(_ handle: String) async throws -> ResolvedData? { + guard let did = try await didForHandle(handle) else { + return nil + } print("did: \(did)") let directoryResult = try await plcDirectoryQuery(did) diff --git a/Sources/ATResolve/Networking.swift b/Sources/ATResolve/Networking.swift index 3b9f823..b7e1356 100644 --- a/Sources/ATResolve/Networking.swift +++ b/Sources/ATResolve/Networking.swift @@ -31,8 +31,11 @@ extension URLSession: ResponseProviding { else { print("data:", String(decoding: data, as: UTF8.self)) print("response:", response) - - throw ATResolverError.requestFailed + if let xrpcError = try? JSONDecoder().decode(XRPCError.self, from: data) { + throw xrpcError + } else { + throw ATResolverError.requestFailed + } } return data } diff --git a/Tests/ATResolveTests/ATResolveTests.swift b/Tests/ATResolveTests/ATResolveTests.swift index 0d75c1d..8fd98e8 100644 --- a/Tests/ATResolveTests/ATResolveTests.swift +++ b/Tests/ATResolveTests/ATResolveTests.swift @@ -12,9 +12,9 @@ struct ATResolveTests { let data = try await resolver.resolveHandle("massicotte.org") - #expect(data.did == "did:plc:klsh7edzj3jmxucibyjqstb3") - #expect(data.handle == "massicotte.org") - #expect(data.serviceEndpoint == "https://milkcap.us-west.host.bsky.network") + #expect(data?.did == "did:plc:klsh7edzj3jmxucibyjqstb3") + #expect(data?.handle == "massicotte.org") + #expect(data?.serviceEndpoint == "https://milkcap.us-west.host.bsky.network") } @Test @@ -32,7 +32,16 @@ struct ATResolveTests { let profile = try await resolver.blueskyGetProfile("massicotte.org") - #expect(profile.did == "did:plc:klsh7edzj3jmxucibyjqstb3") + #expect(profile?.did == "did:plc:klsh7edzj3jmxucibyjqstb3") + } + + @Test + func blueskyGetProfileWithNonexistentHandle() async throws { + let resolver = ATResolver(provider: URLSession.shared) + + let profile = try await resolver.blueskyGetProfile("nonexistent.example.com") + + #expect(profile == nil) } @Test func bskySocialHandle() async throws { @@ -40,7 +49,7 @@ struct ATResolveTests { let profile = try await resolver.resolveHandle("cjrdev.bsky.social") - #expect(profile.did == "did:plc:wlef3srsa3hlyzj2hy6yncrh") + #expect(profile?.did == "did:plc:wlef3srsa3hlyzj2hy6yncrh") } @Test func decodeWithCustomProvider() async throws { From 7190f28aaf8abc5f581c679dab6f826e3c36a401 Mon Sep 17 00:00:00 2001 From: noriaki watanabe Date: Wed, 27 May 2026 08:05:07 +0900 Subject: [PATCH 3/3] fix: return nil instead of throwing for invalid handles --- Sources/ATResolve/ATResolver.swift | 4 +++- Tests/ATResolveTests/ATResolveTests.swift | 9 +++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Sources/ATResolve/ATResolver.swift b/Sources/ATResolve/ATResolver.swift index 86a7261..f6e17cd 100644 --- a/Sources/ATResolve/ATResolver.swift +++ b/Sources/ATResolve/ATResolver.swift @@ -78,7 +78,9 @@ public struct ATResolver { queryItems: [("actor", actor)] ) } catch let error as XRPCError - where error.error == "InvalidRequest" && error.message?.localizedCaseInsensitiveContains("not found") == true { + where error.error == "InvalidRequest" && + (error.message?.localizedCaseInsensitiveContains("not found") == true || + error.message?.localizedCaseInsensitiveContains("invalid app.bsky.actor.getProfile params") == true) { return nil } } diff --git a/Tests/ATResolveTests/ATResolveTests.swift b/Tests/ATResolveTests/ATResolveTests.swift index 8fd98e8..a6849e0 100644 --- a/Tests/ATResolveTests/ATResolveTests.swift +++ b/Tests/ATResolveTests/ATResolveTests.swift @@ -44,6 +44,15 @@ struct ATResolveTests { #expect(profile == nil) } + @Test + func blueskyGetProfileReturnsNilForInvalidFormatHandle() async throws { + let resolver = ATResolver(provider: URLSession.shared) + + let profile = try await resolver.blueskyGetProfile("nonexistent@example.com") + + #expect(profile == nil) + } + @Test func bskySocialHandle() async throws { let resolver = ATResolver(provider: URLSession.shared)