Home > database >  Swift Combine to map URLSession.shared.dataTaskPublisher HTTP response errors
Swift Combine to map URLSession.shared.dataTaskPublisher HTTP response errors

Time:11-14

Given an API that for invalid requests, along with 400-range HTTP status code the server returns a JSON payload that includes a readable message. As an example, the server could return { "message": "Not Found" } with a 404 status code for deleted or non-existent content.

Without using publishers, the code would read,

struct APIErrorResponse: Decodable, Error {
  let message: String
}

func request(request: URLRequest) async throws -> Post {
  let (data, response) = try await URLSession.shared.data(for: request)

  let statusCode = (response as! HTTPURLResponse).statusCode
  if 400..<500 ~= statusCode {
    throw try JSONDecoder().decode(APIErrorResponse.self, from: data)
  }

  return try JSONDecoder().decode(Post.self, from: data)
}

Can this be expressed succinctly using only functional code? In other words, how can the following pattern be adapted to decode a different type based on the HTTPURLResponse.statusCode property, to return as an error, or more generally, how can the response property be handled separately from data attribute?

URLSession.shared.dataTaskPublisher(for: request)
  .map(\.data)
  .decode(type: Post.self, decoder: JSONDecoder())
  .eraseToAnyPublisher()

CodePudding user response:

you could try something like this approach:

func request(request: URLRequest) -> AnyPublisher<Post, any Error> {
    URLSession.shared.dataTaskPublisher(for: request)
        .tryMap { (output) -> Data in
            let statusCode = (output.response as! HTTPURLResponse).statusCode
            if 400..<500 ~= statusCode {
                throw try JSONDecoder().decode(APIErrorResponse.self, from: output.data)
            }
            return output.data
        }
        .decode(type: Post.self, decoder: JSONDecoder())
        .eraseToAnyPublisher()
}

CodePudding user response:

I use a helper method for this:

extension Publisher where Output == (data: Data, response: HTTPURLResponse) {

    func decode<Success, Failure>(
        success: Success.Type,
        failure: Failure.Type,
        decoder: JSONDecoder
    ) -> AnyPublisher<Success, Error> where Success: Decodable, Failure: DecodableError {
        tryMap { data, httpResponse -> Success in
            guard httpResponse.statusCode < 500 else {
                throw MyCustomError.serverUnavailable(status: httpResponse.statusCode)
            }
            guard httpResponse.statusCode < 400 else {
                let error = try decoder.decode(failure, from: data)
                throw error
            }
            let success = try decoder.decode(success, from: data)

            return success
        }
        .eraseToAnyPublisher()
    }
}

typealias DecodableError = Decodable & Error

which allows me to simplify the call sites like so:

URLSession.shared.dataTaskPublisher(for: request)
  .decode(success: Post.self, failure: MyCustomError.self, decoder: JSONDecoder())
  .eraseToAnyPublisher()
  • Related