Reputation: 7902
I'm struggling to form a strongly-typed error object (the ApiErrorResponse
object in my example below) from URLSession's .dataTaskPublisher(for:)
publisher, but couldn't find a clue for that. Here I'm creating a class that fetches a joke object from a remote API and then I handle the result and error as follows (the class can be compiled as is in Xcode Playgrounds):
class DadJokes {
struct Joke: Codable {
let id: String
let joke: String
}
enum Error: Swift.Error {
case network
case parsing(apiResponse: ApiErrorResponse)
case unknown(urlResponse: URLResponse)
}
struct ApiErrorResponse: Codable {
let code: Int
let message: String
}
func getJoke(id: String) -> AnyPublisher<Joke, Error> {
let url = URL(string: "https://myJokes.com/\(id)")!
var request = URLRequest(url: url)
request.allHTTPHeaderFields = ["Accept": "application/json"]
return URLSession.shared
.dataTaskPublisher(for: request)
.map(\.data)
.decode(type: Joke.self, decoder: JSONDecoder())
.mapError { error -> DadJokes.Error in
switch error {
case is DecodingError:
//(1) <--- here I want to get the URLResponse.data from the upstream dataTaskPublisher to decode an object of type ApiErrorResponse (which is returned by the remote API) and pass it to the parsing error case
return .parsing(apiResponse: ApiErrorResponse(code: 1, message: "test"))
case is URLError:
return .network
default:
//(2) <---- here I want to get the URLResponse object that is emitted from the upstream dataTaskPublisher and pass it to the .unknown error case
// I need the URLResponse to read the underlying error info for debugging purposes
return .unknown(urlResponse: URLResponse())
}
}
.eraseToAnyPublisher()
}
}
I have three questions, two of them are commented in the code above. The third one is: what should I do in order to return a never failing publisher from getJoke
function ? i.e. I need the return type of the function to be AnyPublisher<Result<Joke, Error>, Never>
Upvotes: 3
Views: 2170
Reputation: 299275
First, I suggest a slight modification to the error enum to add URLError to the .network
case. Otherwise you can't log information about network-level errors. (But this doesn't really impact the rest of the answer.)
enum Error: Swift.Error {
case network(URLError)
case parsing(apiResponse: ApiErrorResponse)
case unknown(URLResponse)
}
To the core issue, if you want the URLResponse, then you can't throw it away so quickly by calling .map(\.data)
. You need to keep it until you decide whether you need it.
You also need to consider cases like if the data from the API doesn't decode to either Joke or ApiErrorResponse. That's possible, and you have to deal with it.
Since this getting a little complex, it's better to leave behind the basic .decode
and just handle the cases directly in your own .map
:
return URLSession.shared
.dataTaskPublisher(for: request)
.map { (data, response) -> Result<Joke, Error> in
// Either decode a Joke
if let joke = try? JSONDecoder().decode(Joke.self, from: data) {
return .success(joke)
}
// or if that fails, try to decode an error
else if let apiResponse = try? JSONDecoder().decode(ApiErrorResponse.self, from: data) {
return .failure(.parsing(apiResponse: apiResponse))
}
// Wasn't either of those; return the whole response
else {
return .failure(.unknown(response))
}
}
.catch { Just(.failure(.network($0))) } // And also catch errors from dataTaskPublisher
.eraseToAnyPublisher()
Upvotes: 2
Reputation: 33967
The key here is to map the success value to a success result, and then catch the error and make it a success of the Result.failure type. Like this:
func getJoke(id: String) -> AnyPublisher<Result<Joke, Error>, Never> {
let url = URL(string: "https://myJokes.com/\(id)")!
var request = URLRequest(url: url)
request.allHTTPHeaderFields = ["Accept": "application/json"]
return URLSession.shared
.dataTaskPublisher(for: request)
.map(\.data)
.decode(type: Joke.self, decoder: JSONDecoder())
.map { .success($0) }
.catch { (error) -> AnyPublisher<Result<Joke, Error>, Never> in
switch error {
case is DecodingError:
return Just(.failure(.parsing(apiResponse: ApiErrorResponse(code: 1, message: "test")))).eraseToAnyPublisher()
case is URLError:
return Just(.failure(.network)).eraseToAnyPublisher()
default:
return Just(.failure(.unknown(urlResponse: URLResponse()))).eraseToAnyPublisher()
}
}
.eraseToAnyPublisher()
}
Upvotes: 1