Skip to content

Commit 732de76

Browse files
pepicrftclaude
andcommitted
Handle 400 Bad Request for invalid source control URLs in registry lookup
When the registry server returns 400 Bad Request for the identity lookup endpoint, throw `RegistryError.invalidSourceControlURL` to provide a clear error message to the user. This handles cases where malformed URLs (e.g., containing git credential error messages) are sent to the registry. The server validates the URL and returns 400, which the client now handles appropriately instead of treating it as an unexpected error. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 5547e32 commit 732de76

File tree

2 files changed

+43
-1
lines changed

2 files changed

+43
-1
lines changed

Sources/PackageRegistry/RegistryClient.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1066,14 +1066,19 @@ public final class RegistryClient: AsyncCancellable {
10661066
)
10671067
observabilityScope.emit(debug: "matched identities for \(scmURL): \(packageIdentities)")
10681068
return Set(packageIdentities.identifiers.map(PackageIdentity.plain))
1069+
case 400:
1070+
// 400 indicates the server rejected the URL as invalid.
1071+
// This can happen when malformed URLs (e.g., containing git credential error messages)
1072+
// are passed to the registry.
1073+
throw RegistryError.invalidSourceControlURL(scmURL)
10691074
case 404:
10701075
// 404 is valid, no identities mapped
10711076
return []
10721077
default:
10731078
throw RegistryError.failedIdentityLookup(
10741079
registry: registry,
10751080
scmURL: scmURL,
1076-
error: self.unexpectedStatusError(response, expectedStatus: [200, 404])
1081+
error: self.unexpectedStatusError(response, expectedStatus: [200, 400, 404])
10771082
)
10781083
}
10791084
}
@@ -1503,6 +1508,7 @@ public enum RegistryError: Error, CustomStringConvertible {
15031508
case registryNotConfigured(scope: PackageIdentity.Scope?)
15041509
case invalidPackageIdentity(PackageIdentity)
15051510
case invalidURL(URL)
1511+
case invalidSourceControlURL(SourceControlURL)
15061512
case invalidResponseStatus(expected: [Int], actual: Int)
15071513
case invalidContentVersion(expected: String, actual: String?)
15081514
case invalidContentType(expected: String, actual: String?)
@@ -1580,6 +1586,8 @@ public enum RegistryError: Error, CustomStringConvertible {
15801586
return "invalid package identifier '\(packageIdentity)'"
15811587
case .invalidURL(let url):
15821588
return "invalid URL '\(url)'"
1589+
case .invalidSourceControlURL(let scmURL):
1590+
return "invalid source control URL '\(scmURL)'"
15831591
case .invalidResponseStatus(let expected, let actual):
15841592
return "invalid registry response status '\(actual)', expected '\(expected)'"
15851593
case .invalidContentVersion(let expected, let actual):

Tests/PackageRegistryTests/RegistryClientTests.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3148,6 +3148,40 @@ fileprivate var availabilityURL = URL("\(registryURL)/availability")
31483148
let identities = try await registryClient.lookupIdentities(scmURL: packageURL)
31493149
#expect([PackageIdentity.plain("mona.LinkedList")] == identities)
31503150
}
3151+
3152+
@Test func invalidSourceControlURL() async throws {
3153+
// Test that when the server returns 400 Bad Request for a malformed URL,
3154+
// the client throws invalidSourceControlURL error
3155+
let malformedURL = SourceControlURL("https://github.com/owner/repo.git': failed looking up identity")
3156+
3157+
let handler: HTTPClient.Implementation = { request, _ in
3158+
// Server returns 400 Bad Request for invalid URLs
3159+
let data = #"{"message": "Invalid repository URL"}"#.data(using: .utf8)!
3160+
return .init(
3161+
statusCode: 400,
3162+
headers: .init([
3163+
.init(name: "Content-Length", value: "\(data.count)"),
3164+
.init(name: "Content-Type", value: "application/json"),
3165+
.init(name: "Content-Version", value: "1"),
3166+
]),
3167+
body: data
3168+
)
3169+
}
3170+
3171+
let httpClient = HTTPClient(implementation: handler)
3172+
var configuration = RegistryConfiguration()
3173+
configuration.defaultRegistry = Registry(url: registryURL, supportsAvailability: false)
3174+
3175+
let registryClient = makeRegistryClient(configuration: configuration, httpClient: httpClient)
3176+
await #expect {
3177+
try await registryClient.lookupIdentities(scmURL: malformedURL)
3178+
} throws: { error in
3179+
if case RegistryError.invalidSourceControlURL(malformedURL) = error {
3180+
return true
3181+
}
3182+
return false
3183+
}
3184+
}
31513185
}
31523186

31533187
@Suite("Login") struct Login {

0 commit comments

Comments
 (0)