Search code examples
swiftoperator-keywordcombinepublisher

Swift Combine: handle no data before decode without an error


My API usually returns a certain format in JSON (simplified notation):

{
   status: // http status
   error?: // error handle
   data?:  // the response data
   ...
}

In my Combine operators, I take the data from a URLSession dataTaskPublisher and parse the response into a Decodable object that reflects the above schema. That works great.

However, I have an endpoint that returns the HTTP status code 201 (operation successful), and has no data at all. How would I chain this with my operators without throwing an error?

This is what I have:

publisher
.map { (data, response) in
    guard data.count > 0 else {
       let status = (response as! HTTPURLResponse).statusCode
       return Data("{\"status\": \(status), \"data\": \"\"}".utf8)
    }
    return data
}
.mapError { CustomError.network(description: "\($0)")}
.decode(type: MyResponse<R>.self, decoder: self.agent.decoder)
.mapError { err -> CustomError in CustomError.decoding(description: "\(err)") }
...

As you can see, I simply construct an appropriate response, where the response's "data" is an empty string. However, this is ugly and a bit hacky, and I do not see the reason, why the pipeline should continue with parsing, etc, when I already have all I need. How can I interrupt it and finish the pipeline successfully for its final subscriber?


Solution

  • I would suggest creating a separate Publisher for handling the specific endpoint which doesn't return any Data. You can use a tryMap to check the HTTP status code and throw an error in case it's not in the accepted range. If you don't care about the result, only that there was a successful response, you can map to a Void. If you care about the result (or the status code), you can map to that too.

    extension URLSession.DataTaskPublisher {
        func emptyBodyResponsePublisher() -> AnyPublisher<Void, CustomError> {
            tryMap { _, response in
                guard let httpResponse = response as? HTTPURLResponse else { throw CustomError.nonHTTPResponse }
                let statusCode = httpResponse.statusCode
                guard (200..<300).contains(statusCode) else { throw CustomError.incorrectStatusCode(statusCode) }
                return Void()
            }.mapError { CustomError.network($0) }
            .eraseToAnyPublisher()
        }
    }