Skip to content

Commit

Permalink
Merge pull request #186 from AvdLee/feature/better-errors
Browse files Browse the repository at this point in the history
Parse JSON error response
  • Loading branch information
AvdLee committed Jul 29, 2022
2 parents 9838e4c + 19c1f8b commit ed790a6
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 16 deletions.
36 changes: 33 additions & 3 deletions Example/Shared/AppsListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,14 @@ struct AppsListView: View {
.opacity(viewModel.apps.isEmpty ? 1.0 : 0.0)
}.navigationTitle("List of Apps")
.toolbar {
Button("Refresh") {
viewModel.loadApps()
ToolbarItemGroup(placement: .bottomBar) {
Button("Refresh") {
viewModel.loadApps()
}

Button("Fail") {
viewModel.loadFailureExample()
}
}
}
}.onAppear {
Expand Down Expand Up @@ -65,7 +71,31 @@ final class AppsListViewModel: ObservableObject {
let apps = try await self.provider.request(request).data
await self.updateApps(to: apps)
} catch {
print("Something went wrong fetching the apps: \(error)")
print("Something went wrong fetching the apps: \(error.localizedDescription)")
}
}
}

/// This demonstrates a failing example and how you can catch error details.
func loadFailureExample() {
Task.detached {
let requestWithError = APIEndpoint
.v1
.builds
.id("app.appId")
.get()

do {
print(try await self.provider.request(requestWithError).data)
} catch APIProvider.Error.requestFailure(let statusCode, let errorResponse, _) {
print("Request failed with statuscode: \(statusCode) and the following errors:")
errorResponse?.errors?.forEach({ error in
print("Error code: \(error.code)")
print("Error title: \(error.title)")
print("Error detail: \(error.detail)")
})
} catch {
print("Something went wrong fetching the apps: \(error.localizedDescription)")
}
}
}
Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,27 @@ let apps = try await self.provider.request(request).data
print("Did fetch \(apps.count) apps")
```

### Handling errors
Whenever an error is returned from a request, you can get the details by catching the error as follows:

```swift
do {
print(try await self.provider.request(requestWithError).data)
} catch APIProvider.Error.requestFailure(let statusCode, let errorResponse, _) {
print("Request failed with statuscode: \(statusCode) and the following errors:")
errorResponse?.errors?.forEach({ error in
print("Error code: \(error.code)")
print("Error title: \(error.title)")
print("Error detail: \(error.detail)")
})
} catch {
print("Something went wrong fetching the apps: \(error.localizedDescription)")
}
```

The error title and detail should help you solve the failure.
For more info regarding errors, see: [Parsing the Error Response Code](https://developer.apple.com/documentation/appstoreconnectapi/interpreting_and_handling_errors/parsing_the_error_response_code) as documented by Apple.

## Installation

### Swift Package Manager
Expand Down
16 changes: 8 additions & 8 deletions Sources/APIProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public final class APIProvider {
public enum Error: Swift.Error, CustomDebugStringConvertible, LocalizedError {
case requestGeneration
case unknownResponseType
case requestFailure(StatusCode, Data?, URL?)
case requestFailure(StatusCode, ErrorResponse?, URL?)
case decodingError(Swift.Error, Data)
case downloadError
case dateDecodingError(String)
Expand All @@ -47,17 +47,17 @@ public final class APIProvider {
public var errorDescription: String? {
debugDescription
}

public var debugDescription: String {
switch self {
case .requestGeneration:
return "Failed to generate request."
case .unknownResponseType:
return "Unknown response type."
case .requestFailure(let statusCode, let data, let url):
case .requestFailure(let statusCode, let errorResponse, let url):
let url = url?.absoluteString ?? ""
if let data = data, let response = String(data: data, encoding: .utf8) {
return "Request \(url) failed with status code \(statusCode) and response \(response))."
if let errorResponse = errorResponse {
return "Request \(url) failed with status code \(statusCode). \(errorResponse))."
}
return "Request \(url) failed with status code \(statusCode)."
case .decodingError(let error, let data):
Expand Down Expand Up @@ -219,7 +219,7 @@ private extension APIProvider {
switch result {
case .success(let response):
guard let data = response.data, 200..<300 ~= response.statusCode else {
return .failure(Error.requestFailure(response.statusCode, response.data, response.requestURL))
return .failure(Error.requestFailure(response.statusCode, response.errorResponse, response.requestURL))
}

if let data = data as? T {
Expand All @@ -245,7 +245,7 @@ private extension APIProvider {
switch result {
case .success(let response):
guard 200..<300 ~= response.statusCode else {
return .failure(Error.requestFailure(response.statusCode, response.data, response.requestURL))
return .failure(Error.requestFailure(response.statusCode, response.errorResponse, response.requestURL))
}

return .success(())
Expand All @@ -262,7 +262,7 @@ private extension APIProvider {
switch result {
case .success(let response):
guard 200..<300 ~= response.statusCode else {
return .failure(Error.requestFailure(response.statusCode, nil, response.requestURL))
return .failure(Error.requestFailure(response.statusCode, response.errorResponse, response.requestURL))
}
if let data = response.data {
return .success(data)
Expand Down
22 changes: 22 additions & 0 deletions Sources/Extensions/ErrorResponseExtensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// ErrorResponseExtensions.swift
//
//
// Created by Antoine van der Lee on 29/07/2022.
//

import Foundation

extension ErrorResponse: CustomStringConvertible {
public var description: String {
var errorString = "Related response error(s):"
errors?.forEach({ error in
errorString.append("""
\n\nThe request failed with response code \(error.status) \(error.code)
\(error.title). \(error.detail)
""")
})
return errorString
}
}
7 changes: 7 additions & 0 deletions Sources/RequestExecutor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,18 @@ public struct Response<T> {
public let requestURL: URL?
public let statusCode: Int
public let data: T?
public let errorResponse: ErrorResponse?

public init(requestURL: URL?, statusCode: StatusCode, data: T?) {
self.requestURL = requestURL
self.statusCode = statusCode
self.data = data

if let data = data as? Data {
self.errorResponse = try? APIProvider.jsonDecoder.decode(ErrorResponse.self, from: data)
} else {
self.errorResponse = nil
}
}
}

Expand Down
27 changes: 22 additions & 5 deletions Tests/APIProviderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,19 @@ final class APIProviderTests: XCTestCase {
}
}

func testRequestExecutionErrorResponse() {
func testRequestExecutionErrorResponse() throws {
let expectedURL = URL(string: "https://api.appstoreconnect.apple.com")!
let response = Response<Data>(requestURL: expectedURL, statusCode: 500, data: nil)
let errorResponse = ErrorResponse(errors: [
.init(
id: UUID().uuidString,
status: "404",
code: "NOT_FOUND",
title: "The specified resource does not exist",
detail: "There is no resource of type 'builds' with id 'app.appId'"
)
])
let responseData = try JSONEncoder().encode(errorResponse)
let response = Response<Data>(requestURL: expectedURL, statusCode: 404, data: responseData)
let mockRequestExecutor = MockRequestExecutor(expectedResponse: Result.success(response))
let apiProvider = APIProvider(configuration: configuration, requestExecutor: mockRequestExecutor)

Expand All @@ -66,13 +76,20 @@ final class APIProviderTests: XCTestCase {
XCTAssertNotNil(result.error)
guard
let error = result.error as? APIProvider.Error,
case let APIProvider.Error.requestFailure(statusCode, data, url) = error else {
case let APIProvider.Error.requestFailure(statusCode, errorResponse, url) = error else {
XCTFail("We expect a requestFailure error")
return
}
XCTAssertNil(data)
XCTAssertEqual(statusCode, 500)
XCTAssertNotNil(errorResponse)
XCTAssertEqual(statusCode, 404)
XCTAssertEqual(url?.absoluteString, expectedURL.absoluteString)
XCTAssertEqual(error.localizedDescription, """
Request https://api.appstoreconnect.apple.com failed with status code 404. Related response error(s):
The request failed with response code 404 NOT_FOUND
The specified resource does not exist. There is no resource of type 'builds' with id 'app.appId').
""")
}
}

Expand Down

0 comments on commit ed790a6

Please sign in to comment.