Skip to content

Commit ffe130a

Browse files
authored
[Collections] Improve error handling (#3471) (#3474)
Make sure errors returned by `JSONPackageCollectionProvider` have good string representation. rdar://77530631
1 parent 739f7c4 commit ffe130a

File tree

2 files changed

+46
-45
lines changed

2 files changed

+46
-45
lines changed

Sources/PackageCollections/Providers/JSONPackageCollectionProvider.swift

Lines changed: 36 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ struct JSONPackageCollectionProvider: PackageCollectionProvider {
6767
}
6868

6969
if let errors = source.validate()?.errors() {
70-
return callback(.failure(MultipleErrors(errors)))
70+
return callback(.failure(JSONPackageCollectionProviderError.invalidSource("\(errors)")))
7171
}
7272

7373
// Source is a local file
@@ -89,18 +89,18 @@ struct JSONPackageCollectionProvider: PackageCollectionProvider {
8989
switch result {
9090
case .failure(HTTPClientError.badResponseStatusCode(let statusCode)):
9191
if statusCode == 404 {
92-
return callback(.failure(Errors.collectionNotFound(source.url)))
92+
return callback(.failure(JSONPackageCollectionProviderError.collectionNotFound(source.url)))
9393
} else {
94-
return callback(.failure(Errors.collectionUnavailable(source.url, statusCode)))
94+
return callback(.failure(JSONPackageCollectionProviderError.collectionUnavailable(source.url, statusCode)))
9595
}
9696
case .failure(let error):
9797
return callback(.failure(error))
9898
case .success(let response):
9999
guard let contentLength = response.headers.get("Content-Length").first.flatMap(Int64.init) else {
100-
return callback(.failure(Errors.invalidResponse(source.url, "Missing Content-Length header")))
100+
return callback(.failure(JSONPackageCollectionProviderError.invalidResponse(source.url, "Missing Content-Length header")))
101101
}
102102
guard contentLength <= self.configuration.maximumSizeInBytes else {
103-
return callback(.failure(Errors.responseTooLarge(source.url, contentLength)))
103+
return callback(.failure(JSONPackageCollectionProviderError.responseTooLarge(source.url, contentLength)))
104104
}
105105
// next do a get request to get the actual content
106106
var getOptions = self.makeRequestOptions(validResponseCodes: [200])
@@ -109,9 +109,9 @@ struct JSONPackageCollectionProvider: PackageCollectionProvider {
109109
switch result {
110110
case .failure(HTTPClientError.badResponseStatusCode(let statusCode)):
111111
if statusCode == 404 {
112-
return callback(.failure(Errors.collectionNotFound(source.url)))
112+
return callback(.failure(JSONPackageCollectionProviderError.collectionNotFound(source.url)))
113113
} else {
114-
return callback(.failure(Errors.collectionUnavailable(source.url, statusCode)))
114+
return callback(.failure(JSONPackageCollectionProviderError.collectionUnavailable(source.url, statusCode)))
115115
}
116116
case .failure(let error):
117117
return callback(.failure(error))
@@ -120,13 +120,13 @@ struct JSONPackageCollectionProvider: PackageCollectionProvider {
120120
// if not returning head and exceeding size
121121
// TODO: store bad actors to prevent server DoS
122122
guard let contentLength = response.headers.get("Content-Length").first.flatMap(Int64.init) else {
123-
return callback(.failure(Errors.invalidResponse(source.url, "Missing Content-Length header")))
123+
return callback(.failure(JSONPackageCollectionProviderError.invalidResponse(source.url, "Missing Content-Length header")))
124124
}
125125
guard contentLength < self.configuration.maximumSizeInBytes else {
126-
return callback(.failure(Errors.responseTooLarge(source.url, contentLength)))
126+
return callback(.failure(JSONPackageCollectionProviderError.responseTooLarge(source.url, contentLength)))
127127
}
128128
guard let body = response.body else {
129-
return callback(.failure(Errors.invalidResponse(source.url, "Body is empty")))
129+
return callback(.failure(JSONPackageCollectionProviderError.invalidResponse(source.url, "Body is empty")))
130130
}
131131

132132
let certPolicyKey = self.sourceCertPolicy.certificatePolicyKey(for: source) ?? .default
@@ -175,15 +175,15 @@ struct JSONPackageCollectionProvider: PackageCollectionProvider {
175175
}
176176
// Collection is unsigned
177177
guard let collection = try? self.decoder.decode(JSONModel.Collection.self, from: data) else {
178-
return callback(.failure(Errors.invalidJSON(source.url)))
178+
return callback(.failure(JSONPackageCollectionProviderError.invalidJSON(source.url)))
179179
}
180180
callback(self.makeCollection(from: collection, source: source, signature: nil))
181181
}
182182
}
183183

184184
private func makeCollection(from collection: JSONModel.Collection, source: Model.CollectionSource, signature: Model.SignatureData?) -> Result<Model.Collection, Error> {
185185
if let errors = self.validator.validate(collection: collection)?.errors() {
186-
return .failure(MultipleErrors(errors))
186+
return .failure(JSONPackageCollectionProviderError.invalidCollection("\(errors)"))
187187
}
188188

189189
var serializationOkay = true
@@ -345,27 +345,31 @@ struct JSONPackageCollectionProvider: PackageCollectionProvider {
345345
)
346346
}
347347
}
348+
}
348349

349-
public enum Errors: Error, Equatable, CustomStringConvertible {
350-
case invalidJSON(URL)
351-
case invalidResponse(URL, String)
352-
case responseTooLarge(URL, Int64)
353-
case collectionNotFound(URL)
354-
case collectionUnavailable(URL, Int)
355-
356-
public var description: String {
357-
switch self {
358-
case .invalidJSON(let url):
359-
return "The package collection at \(url.absoluteString) contains invalid JSON."
360-
case .invalidResponse(let url, let message):
361-
return "Received invalid response for package collection at \(url.absoluteString): \(message)"
362-
case .responseTooLarge(let url, _):
363-
return "The package collection at \(url.absoluteString) is too large."
364-
case .collectionNotFound(let url):
365-
return "No package collection found at \(url.absoluteString). Please make sure the URL is correct."
366-
case .collectionUnavailable(let url, _):
367-
return "The package collection at \(url.absoluteString) is unavailable. Please make sure the URL is correct or try again later."
368-
}
350+
public enum JSONPackageCollectionProviderError: Error, Equatable, CustomStringConvertible {
351+
case invalidSource(String)
352+
case invalidJSON(URL)
353+
case invalidCollection(String)
354+
case invalidResponse(URL, String)
355+
case responseTooLarge(URL, Int64)
356+
case collectionNotFound(URL)
357+
case collectionUnavailable(URL, Int)
358+
359+
public var description: String {
360+
switch self {
361+
case .invalidSource(let errorMessage), .invalidCollection(let errorMessage):
362+
return errorMessage
363+
case .invalidJSON(let url):
364+
return "The package collection at \(url.absoluteString) contains invalid JSON."
365+
case .invalidResponse(let url, let message):
366+
return "Received invalid response for package collection at \(url.absoluteString): \(message)"
367+
case .responseTooLarge(let url, _):
368+
return "The package collection at \(url.absoluteString) is too large."
369+
case .collectionNotFound(let url):
370+
return "No package collection found at \(url.absoluteString). Please make sure the URL is correct."
371+
case .collectionUnavailable(let url, _):
372+
return "The package collection at \(url.absoluteString) is unavailable. Please make sure the URL is correct or try again later."
369373
}
370374
}
371375
}

Tests/PackageCollectionsTests/JSONPackageCollectionProviderTests.swift

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -124,13 +124,10 @@ class JSONPackageCollectionProviderTests: XCTestCase {
124124
httpClient.configuration.retryStrategy = .none
125125
let provider = JSONPackageCollectionProvider(httpClient: httpClient, diagnosticsEngine: DiagnosticsEngine())
126126
XCTAssertThrowsError(try tsc_await { callback in provider.get(source, callback: callback) }, "expected error", { error in
127-
guard let internalError = (error as? MultipleErrors)?.errors.first else {
127+
guard case .invalidSource(let errorMessage) = error as? JSONPackageCollectionProviderError else {
128128
return XCTFail("invalid error \(error)")
129129
}
130-
guard let validationError = internalError as? ValidationError, case .other(let message) = validationError else {
131-
return XCTFail("invalid error \(error)")
132-
}
133-
XCTAssertTrue(message.contains("Scheme (\"ftp\") not allowed: \(url.absoluteString)"))
130+
XCTAssertTrue(errorMessage.contains("Scheme (\"ftp\") not allowed: \(url.absoluteString)"))
134131
})
135132
}
136133

@@ -152,7 +149,7 @@ class JSONPackageCollectionProviderTests: XCTestCase {
152149
let configuration = JSONPackageCollectionProvider.Configuration(maximumSizeInBytes: 10)
153150
let provider = JSONPackageCollectionProvider(configuration: configuration, httpClient: httpClient, diagnosticsEngine: DiagnosticsEngine())
154151
XCTAssertThrowsError(try tsc_await { callback in provider.get(source, callback: callback) }, "expected error", { error in
155-
XCTAssertEqual(error as? JSONPackageCollectionProvider.Errors, .responseTooLarge(url, maxSize * 2))
152+
XCTAssertEqual(error as? JSONPackageCollectionProviderError, .responseTooLarge(url, maxSize * 2))
156153
})
157154
}
158155

@@ -181,7 +178,7 @@ class JSONPackageCollectionProviderTests: XCTestCase {
181178
let configuration = JSONPackageCollectionProvider.Configuration(maximumSizeInBytes: 10)
182179
let provider = JSONPackageCollectionProvider(configuration: configuration, httpClient: httpClient, diagnosticsEngine: DiagnosticsEngine())
183180
XCTAssertThrowsError(try tsc_await { callback in provider.get(source, callback: callback) }, "expected error", { error in
184-
XCTAssertEqual(error as? JSONPackageCollectionProvider.Errors, .responseTooLarge(url, maxSize * 2))
181+
XCTAssertEqual(error as? JSONPackageCollectionProviderError, .responseTooLarge(url, maxSize * 2))
185182
})
186183
}
187184

@@ -201,7 +198,7 @@ class JSONPackageCollectionProviderTests: XCTestCase {
201198
let configuration = JSONPackageCollectionProvider.Configuration(maximumSizeInBytes: 10)
202199
let provider = JSONPackageCollectionProvider(configuration: configuration, httpClient: httpClient, diagnosticsEngine: DiagnosticsEngine())
203200
XCTAssertThrowsError(try tsc_await { callback in provider.get(source, callback: callback) }, "expected error", { error in
204-
XCTAssertEqual(error as? JSONPackageCollectionProvider.Errors, .invalidResponse(url, "Missing Content-Length header"))
201+
XCTAssertEqual(error as? JSONPackageCollectionProviderError, .invalidResponse(url, "Missing Content-Length header"))
205202
})
206203
}
207204

@@ -249,7 +246,7 @@ class JSONPackageCollectionProviderTests: XCTestCase {
249246
httpClient.configuration.retryStrategy = .none
250247
let provider = JSONPackageCollectionProvider(httpClient: httpClient, diagnosticsEngine: DiagnosticsEngine())
251248
XCTAssertThrowsError(try tsc_await { callback in provider.get(source, callback: callback) }, "expected error", { error in
252-
XCTAssertEqual(error as? JSONPackageCollectionProvider.Errors, .collectionUnavailable(url, statusCode))
249+
XCTAssertEqual(error as? JSONPackageCollectionProviderError, .collectionUnavailable(url, statusCode))
253250
})
254251
}
255252

@@ -275,7 +272,7 @@ class JSONPackageCollectionProviderTests: XCTestCase {
275272
httpClient.configuration.retryStrategy = .none
276273
let provider = JSONPackageCollectionProvider(httpClient: httpClient, diagnosticsEngine: DiagnosticsEngine())
277274
XCTAssertThrowsError(try tsc_await { callback in provider.get(source, callback: callback) }, "expected error", { error in
278-
XCTAssertEqual(error as? JSONPackageCollectionProvider.Errors, .collectionUnavailable(url, statusCode))
275+
XCTAssertEqual(error as? JSONPackageCollectionProviderError, .collectionUnavailable(url, statusCode))
279276
})
280277
}
281278

@@ -294,7 +291,7 @@ class JSONPackageCollectionProviderTests: XCTestCase {
294291
httpClient.configuration.retryStrategy = .none
295292
let provider = JSONPackageCollectionProvider(httpClient: httpClient, diagnosticsEngine: DiagnosticsEngine())
296293
XCTAssertThrowsError(try tsc_await { callback in provider.get(source, callback: callback) }, "expected error", { error in
297-
XCTAssertEqual(error as? JSONPackageCollectionProvider.Errors, .collectionNotFound(url))
294+
XCTAssertEqual(error as? JSONPackageCollectionProviderError, .collectionNotFound(url))
298295
})
299296
}
300297

@@ -319,7 +316,7 @@ class JSONPackageCollectionProviderTests: XCTestCase {
319316
httpClient.configuration.retryStrategy = .none
320317
let provider = JSONPackageCollectionProvider(httpClient: httpClient, diagnosticsEngine: DiagnosticsEngine())
321318
XCTAssertThrowsError(try tsc_await { callback in provider.get(source, callback: callback) }, "expected error", { error in
322-
XCTAssertEqual(error as? JSONPackageCollectionProvider.Errors, .collectionNotFound(url))
319+
XCTAssertEqual(error as? JSONPackageCollectionProviderError, .collectionNotFound(url))
323320
})
324321
}
325322

@@ -347,7 +344,7 @@ class JSONPackageCollectionProviderTests: XCTestCase {
347344
let provider = JSONPackageCollectionProvider(httpClient: httpClient, diagnosticsEngine: DiagnosticsEngine())
348345
let source = PackageCollectionsModel.CollectionSource(type: .json, url: url)
349346
XCTAssertThrowsError(try tsc_await { callback in provider.get(source, callback: callback) }, "expected error", { error in
350-
XCTAssertEqual(error as? JSONPackageCollectionProvider.Errors, .invalidJSON(url))
347+
XCTAssertEqual(error as? JSONPackageCollectionProviderError, .invalidJSON(url))
351348
})
352349
}
353350

0 commit comments

Comments
 (0)