I would like to perform a recursive once retry with Swift Combine when the server responds with a certain message (in the example a 401 error). The data in that response alters a model, which allows for a single retry.
I wrote a small extension for the result type that was used pre iOS 13
extension URLSession {
typealias HTTPResponse = (response: HTTPURLResponse, data: Data)
typealias DataTaskResult = ((Result<HTTPResponse, Error>) -> Void)
func dataTask(with request: URLRequest, completionHandler: #escaping DataTaskResult) -> URLSessionDataTask {
self.dataTask(with: request) { (data, response, error) in
if let error = error {
completionHandler(.failure(error))
}
completionHandler(.success((response as! HTTPURLResponse, data!)))
}
}
}
I used this extension to do the following
class Account {
enum CommunicationError: Swift.Error {
case counterOutOfSync
}
var counter: Int = 0
func send(isRetry: Bool = false, completionBlock: #escaping URLSession.DataTaskResult) {
var request = URLRequest(url: URL(string: "https://myserver.com/fetch/")!)
request.setValue("\(counter)", forHTTPHeaderField: "MESSAGE-COUNTER")
request.httpMethod = "POST"
URLSession.shared.dataTask(with: request) { [weak self] taskResult in
do {
let taskResponse = try taskResult.get()
if taskResponse.response.statusCode == 401 {
if isRetry { throw CommunicationError.counterOutOfSync }
// Counter is resynced based on taskResponse.data
self?.send(isRetry: true, completionBlock: completionBlock)
} else {
completionBlock(.success(taskResponse))
}
} catch {
completionBlock(.failure(error))
}
}.resume()
}
}
You can see the recursive call in the function. I would like to do the same with Combine, but I don't know how to. This is as far as I get
func combine(isRetry: Bool = false) -> AnyPublisher<Data, Error> {
var request = URLRequest(url: URL(string: "https://myserver.com/fetch/")!)
request.setValue("\(counter)", forHTTPHeaderField: "MESSAGE-COUNTER")
request.httpMethod = "POST"
return URLSession.shared.dataTaskPublisher(for: request).tryMap {
let response = $0.response as! HTTPURLResponse
if response.statusCode == 401 {
if isRetry { throw CommunicationError.counterOutOfSync }
// Counter is resynced based on $0.data
return self.combine(isRetry: true)
} else {
return $0.data
}
}.eraseToAnyPublisher()
}
Any help is appreciated
If you have the original send(isRetry:completionBlock:), you can use Future to convert it to a publisher:
func send() -> AnyPublisher<URLSession.HTTPResponse, Error> {
Future { [weak self] promise in
self?.send(isRetry: false) { result in promise(result) }
}
.eraseToAnyPublisher()
}
Alternatively, Combine already has a .retry operator, so the entire thing could be made purely in Combine:
URLSession.shared.dataTaskPublisher(for: request)
.tryMap { data, response in
let response = response as! HTTPURLResponse
if response.statusCode == 401 {
throw CommunicationError.counterOutOfSync
} else {
return (response: response, data: data)
}
}
.retry(1)
.eraseToAnyPublisher()
This will retry once whenever there's any error (not just 401) from upstream. You can play around more to only retry under some conditions (e.g. see this answer)
Related
I am starting to use RxSwift to make the service call.
This was my old code:
class Service: GraphQLService {
func graphQL(body: [String: Any?], onSuccess: #escaping (Foundation.Data) throws -> (), onFailure: #escaping (Error) -> ()) {
guard let urlValue = Bundle.main.urlValue else { return }
guard let url = URL(string: urlValue) else { return
print("Error with info.plist")
}
var request = URLRequest(url: url)
let userKey = Bundle.main.userKeyValue
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue(userKey, forHTTPHeaderField: "userid")
request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: .fragmentsAllowed)
URLSession.shared.dataTask(with: request) { (data, response, error) in
if let error = error {
onFailure(error)
}
if let data = data {
do{
try onSuccess(data)
}
catch{
onFailure(error)
}
}
}.resume()
}
And here I do the function to get time deposits:
final class TimeDepositManager: Service, TimeDepositManagerProtocol {
let timeDepositQuery = Bundle.main.queryValue
func getTimeDeposits(onSuccess: #escaping ([TimeDeposits]) -> (), onFailure: #escaping (Error) -> ()) {
let body = ["query": timeDepositQuery]
Service().graphQL(body: body, onSuccess: { data in
let json = try? JSONDecoder().decode(GraphQLResponse.self, from: data)
onSuccess(json?.data?.account?.timeDeposits ?? [])
}, onFailure: onFailure)
}
And so far this is my code with RxSwift:
class Service: GraphQLService {
func graphQL(body: [String : Any?]) -> Observable<Foundation.Data> {
return Observable.create { observer in
let urlValue = Bundle.main.urlValue
let url = URL(string: urlValue ?? "")
let session = URLSession.shared
var request = URLRequest(url: url!)
let userKey = Bundle.main.userKeyValue
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue(userKey, forHTTPHeaderField: "userid")
request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: .fragmentsAllowed)
session.dataTask(with: request) { (data, response, error) in
if let error = error {
observer.onError(error)
}
if let data = data {
do{
try onSuccess(data)
observer.onNext(data)
}
catch{
//onFailure(error)
observer.onError(error)
print("Error: \(error.localizedDescription)")
}
}
}.resume()
return Disposables.create {
session.finishTasksAndInvalidate()
}
}
}
This is where I don't understand how in my getTimeDeposits () I can do the deserialization with try? JSONDecoder () ... with RxSwift without using onSuccess?
final class TimeDepositManager: Service, TimeDepositManagerProtocol {
let timeDepositQuery = Bundle.main.queryValue
func getTimeDeposits() -> Observable<[TimeDeposits]> {
let body = ["query": timeDepositQuery]
Service().graphQL(body: body)
}
You can have getTimeDeposits() return an Observable as well and handle the deserialization in a map closure. A couple of other things.
RxCocoa already has a method on URLSession so you don't need to write your own.
I suggest reducing the amount of code you have in a function that makes the network request. You want to be able to test your logic for making the request without actually making it.
Something like this:
final class TimeDepositManager: Service, TimeDepositManagerProtocol {
let timeDepositQuery = Bundle.main.queryValue
func getTimeDeposits() -> Observable<[TimeDeposits]> {
let body = ["query": timeDepositQuery]
return Service().graphQL(body: body)
.map { try JSONDecoder().decode(GraphQLResponse.self, from: $0).data?.account?.timeDeposits ?? [] }
}
}
class Service: GraphQLService {
func graphQL(body: [String: Any?]) -> Observable<Data> {
guard let urlValue = Bundle.main.urlValue else { fatalError("Error with info.plist") }
let request = urlRequest(urlValue: urlValue, body: body)
return URLSession.shared.rx.data(request: request) // this is in RxCocoa
}
func urlRequest(urlValue: String, body: [String: Any?]) -> URLRequest {
guard let url = URL(string: urlValue) else { fatalError("Error with urlValue") }
var request = URLRequest(url: url)
let userKey = Bundle.main.userKeyValue
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue(userKey, forHTTPHeaderField: "userid")
request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: .fragmentsAllowed)
return request
}
}
If you don't want to use RxCocoa for some reason, here is the correct way to wrap the URLSession.dataTask method:
extension URLSession {
func data(request: URLRequest) -> Observable<Data> {
Observable.create { observer in
let task = self.dataTask(with: request, completionHandler: { data, response, error in
guard let response = response as? HTTPURLResponse else {
observer.onError(URLError.notHTTPResponse(data: data, response: response))
return
}
guard 200 <= response.statusCode && response.statusCode < 300 else {
observer.onError(URLError.failedResponse(data: data, response: response))
return
}
guard let data = data else {
observer.onError(error ?? RxError.unknown)
return
}
observer.onNext(data)
observer.onCompleted() // be sure to call `onCompleted()` when you are done emitting values.
// make sure every possible path through the code calls some method on `observer`.
})
return Disposables.create { task.cancel() } // don't forget to handle cancelation properly. You don't want to kill *all* tasks, just this one.
}
}
}
enum URLError: Error {
case notHTTPResponse(data: Data?, response: URLResponse?)
case failedResponse(data: Data?, response: HTTPURLResponse)
}
I'm building a network API.
I'm new to Combine and I'm having some troubles with it, I'm trying to chain publish network requests, in this case I'm forming an URLRequest publisher and dispatching it on another publisher, the problem is that I cant make the flatMap work on the second publisher.
First I assemble the URLRequest with the Auth token:
func asURLRequest(baseURL: String) -> AnyPublisher<URLRequest, NetworkRequestError> {
return Deferred {
Future<URLRequest, NetworkRequestError> { promise in
if var urlComponents = URLComponents(string: baseURL) {
urlComponents.path = "\(urlComponents.path)\(path)"
urlComponents.queryItems = queryItemsFrom(params: queryParams)
if let finalURL = urlComponents.url {
if let user = Auth.auth().currentUser {
print("##### final url -> \(finalURL)")
// Retrieves the Firebase authentication token, possibly refreshing it if it has expired.
user.getIDToken(completion: { (token, error) in
if let fbToken = token {
var request = URLRequest(url: finalURL)
request.httpMethod = method.rawValue
request.httpBody = requestBodyFrom(params: body)
let defaultHeaders: HTTPHeaders = [
HTTPHeaderField.contentType.rawValue: contentType.rawValue,
HTTPHeaderField.acceptType.rawValue: contentType.rawValue,
HTTPHeaderField.authentication.rawValue: fbToken
]
request.allHTTPHeaderFields = defaultHeaders.merging(headers ?? [:], uniquingKeysWith: { (first, _) in first })
print("##### API TOKEN() SUCCESS: \(defaultHeaders)")
promise(.success(request))
}
if let fbError = error {
print("##### API TOKEN() ERROR: \(fbError)")
promise(.failure(NetworkRequestError.decodingError))
}
})
}
} else {
promise(.failure(NetworkRequestError.decodingError))
}
} else {
promise(.failure(NetworkRequestError.decodingError))
}
}
}.eraseToAnyPublisher()
}
Then I'm trying to dispatch a request (publisher) and return another publisher, the problem is that the .flatMap is not getting called:
struct APIClient {
var baseURL: String!
var networkDispatcher: NetworkDispatcher!
init(baseURL: String,
networkDispatcher: NetworkDispatcher = NetworkDispatcher()) {
self.baseURL = baseURL
self.networkDispatcher = networkDispatcher
}
/// Dispatches a Request and returns a publisher
/// - Parameter request: Request to Dispatch
/// - Returns: A publisher containing decoded data or an error
func dispatch<R: Request>(_ request: R) -> AnyPublisher<R.ReturnType, NetworkRequestError> {
print("##### --------> \(request)")
//typealias RequestPublisher = AnyPublisher<R.ReturnType, NetworkRequestError>
return request.asURLRequest(baseURL: baseURL)
.flatMap { request in
//NOT GETTING CALLED
self.networkDispatcher.dispatch(request: request)
}.eraseToAnyPublisher()
}
The final publisher that is not being called is the following:
struct NetworkDispatcher {
let urlSession: URLSession!
public init(urlSession: URLSession = .shared) {
self.urlSession = urlSession
}
/// Dispatches an URLRequest and returns a publisher
/// - Parameter request: URLRequest
/// - Returns: A publisher with the provided decoded data or an error
func dispatch<ReturnType: Codable>(request: URLRequest) -> AnyPublisher<ReturnType, NetworkRequestError> {
return urlSession
.dataTaskPublisher(for: request)
// Map on Request response
.tryMap({ data, response in
// If the response is invalid, throw an error
if let response = response as? HTTPURLResponse,
!(200...299).contains(response.statusCode) {
throw httpError(response.statusCode)
}
// Return Response data
return data
})
// Decode data using our ReturnType
.decode(type: ReturnType.self, decoder: JSONDecoder())
// Handle any decoding errors
.mapError { error in
handleError(error)
}
// And finally, expose our publisher
.eraseToAnyPublisher()
}
}
Running the code:
struct ReadUser: Request {
typealias ReturnType = UserData
var path: String
var method: HTTPMethod = .get
init(_ id: String) {
path = "users/\(id)"
}
}
let apiClient = APIClient(baseURL: BASE_URL)
var cancellables = [AnyCancellable]()
apiClient.dispatch(ReadUser(Auth.auth().currentUser?.uid ?? ""))
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { result in
switch result {
case .failure(let error):
// Handle API response errors here (WKNetworkRequestError)
print("##### Error loading data: \(error)")
default: break
}
},
receiveValue: { value in
})
.store(in: &cancellables)
I took your code and boiled it down to just the Combine parts. I could not reproduce the issue you are describing. I'll post that code below. I recommend you start simplifying your code a bit at a time to see if that helps. Factoring out the Auth and Facebook token code seems like a good candidate to start with. Another good debugging technique might be to put in more explicit type declarations to make sure your closures are taking and returning what you expect. (just the other day I had a map that I thought I was applying to an Array when I was really mapping over Optional).
Here's the playground:
import UIKit
import Combine
func asURLRequest(baseURL: String) -> AnyPublisher<URLRequest, Error> {
return Deferred {
Future<URLRequest, Error> { promise in
promise(.success(URLRequest(url: URL(string: "https://www.apple.com")!)))
}
}.eraseToAnyPublisher()
}
struct APIClient {
var networkDispatcher: NetworkDispatcher!
init(networkDispatcher: NetworkDispatcher = NetworkDispatcher()) {
self.networkDispatcher = networkDispatcher
}
func dispatch() -> AnyPublisher<Data, Error> {
return asURLRequest(baseURL: "Boo!")
.flatMap { (request: URLRequest) -> AnyPublisher<Data, Error> in
print("Request Received. \(String(describing: request))")
return self.networkDispatcher.dispatch(request: request)
}.eraseToAnyPublisher()
}
}
func httpError(_ code: Int) -> Error {
return NSError(domain: "Bad Things", code: -1, userInfo: nil)
}
func handleError(_ error: Error) -> Error {
debugPrint(error)
return error
}
struct NetworkDispatcher {
let urlSession: URLSession!
public init(urlSession: URLSession = .shared) {
self.urlSession = urlSession
}
func dispatch(request: URLRequest) -> AnyPublisher<Data, Error> {
return urlSession
.dataTaskPublisher(for: request)
.tryMap({ data, response in
if let response = response as? HTTPURLResponse,
!(200...299).contains(response.statusCode) {
throw httpError(response.statusCode)
}
// Return Response data
return data
})
.mapError { error in
handleError(error)
}
.eraseToAnyPublisher()
}
}
let apiClient = APIClient()
var cancellables = [AnyCancellable]()
apiClient.dispatch()
.print()
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { result in
debugPrint(result)
switch result {
case .failure(let error):
// Handle API response errors here (WKNetworkRequestError)
print("##### Error loading data: \(error)")
default: break
}
},
receiveValue: { value in
debugPrint(value)
})
.store(in: &cancellables)
I refactored your code. Breaking down the offending method into several functions. I could not find any problem. Below is my refactoring. You will notice that I broke all the code that constructs things into their own functions so they can be easily tested without dealing with the effect (I don't even have to mock the effect to test the logic.)
extension Request {
func asURLRequest(baseURL: String) -> AnyPublisher<URLRequest, NetworkRequestError> {
guard let user = Auth.auth().currentUser else {
return Fail(error: NetworkRequestError.missingUser)
.eraseToAnyPublisher()
}
return user.idTokenPublisher()
.catch { error in
Fail(error: NetworkRequestError.badToken(error))
}
.tryMap { token in
makeRequest(
finalURL: try finalURL(baseURL: baseURL),
fbToken: token
)
}
.eraseToAnyPublisher()
}
func finalURL(baseURL: String) throws -> URL {
guard var urlComponents = URLComponents(string: baseURL) else {
throw NetworkRequestError.malformedURLComponents
}
urlComponents.path = "\(urlComponents.path)\(path)"
urlComponents.queryItems = queryItemsFrom(params: queryParams)
guard let result = urlComponents.url else {
throw NetworkRequestError.malformedURLComponents
}
return result
}
func makeRequest(finalURL: URL, fbToken: String) -> URLRequest {
var request = URLRequest(url: finalURL)
request.httpMethod = method.rawValue
request.httpBody = requestBodyFrom(params: body)
let defaultHeaders: HTTPHeaders = [
HTTPHeaderField.contentType.rawValue: contentType.rawValue,
HTTPHeaderField.acceptType.rawValue: contentType.rawValue,
HTTPHeaderField.authentication.rawValue: fbToken
]
request.allHTTPHeaderFields = defaultHeaders.merging(
headers ?? [:],
uniquingKeysWith: { (first, _) in first }
)
return request
}
}
extension User {
func idTokenPublisher() -> AnyPublisher<String, Error> {
Deferred {
Future { promise in
getIDToken(completion: { token, error in
if let token = token {
promise(.success(token))
}
else {
promise(.failure(error ?? UnknownError()))
}
})
}
}
.eraseToAnyPublisher()
}
}
struct UnknownError: Error { }
I have a function that sends a GET request to an API and I need to wait for the response before I can continue with the rest of my code.
I have tried various different threads but haven't got it to work. I've been trying to solve this by myself but I am quite confused by threads and so fourth.
These are my functions:
func checkDomains() {
let endings = [".com"]
for ending in endings {
let domainName = names[randomNameIndx] + ending
let urlString = "https://domainr.p.mashape.com/v2/status?mashape-key={my-key}" + domainName
let url = URL(string: urlString)
var request = URLRequest(url: url!)
request.setValue("{my-key}", forHTTPHeaderField: "X-Mashape-Key")
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.httpMethod = "GET"
var ourBool = false
DispatchQueue.main.sync {
let task = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, er) in
do {
print("hey")
if let json = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? NSDictionary {
ourBool = String(describing: json).contains("inactive")
if ourBool {
self.domainStatuses.append("available")
} else {
self.domainStatuses.append("taken")
}
}
} catch {
}
})
task.resume()
}
}
}
#objc func btnTapped(sender: UIButton) {
self.prepareForNewName()
sender.shake(direction: "horizontal", swings: 1)
self.checkDomains() // This needs to finish before calling the next functions
self.setupForNewName()
self.animateForNewName()
}
My suggestion is in adding callback param into your async function. Example below:
func checkDomains(_ onResult: #escaping (Error?) -> Void)
Then you can call onResult inside your function in places where server return success result or error.
func checkDomains(_ onResult: #escaping (Error?) -> Void) {
...
let task = URLSession.shared.dataTask ... {
do {
//Parse response
DispatchQueue.main.async { onResult(nil) }
} catch {
DispatchQueue.main.async { onResult(error) }
}
...
}
}
The last thing you need pass callback param in place where you calling checkDomains function
#objc func btnTapped(sender: UIButton) {
self.prepareForNewName()
sender.shake(direction: "horizontal", swings: 1)
self.checkDomains { [unowned self] error in
if let error = error {
// handle error
return
}
self.setupForNewName()
self.animateForNewName()
}
}
Thank you for your answers. I just came home and realized there is a simpler way of doing this.
let task = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in
do {
...
if response != nil {
DispatchQueue.main.async {
self.setupForNewName()
self.animateForNewName()
}
}
}
} catch {
print(error)
}
})
task.resume()
Of course this is dependent on the response of the API but I am confident enough the API will always respond.
Lately it is a good practice to do network call asynchronously. There some technics to make code simpler, like https://cocoapods.org/pods/ResultPromises
TBC...
So we have this function that retrieves JSON data and presents it in its completion block, what I'm trying to understand is why use the signature: ((Data) -> Void) instead of just (Data), is the void really necessary? Here is the function:
typealias JSONData = ((Data) -> Void)
func getJSONData(type: String, urlExtension: String, completion: #escaping JSONData) {
let request = URLRequest(url: URL(string:"\(baseURL)\(type)/\(urlExtension)?api_key=\(apiKey)®ion=US&append_to_response=videos,images,releases")! )
let dataTask = session.dataTask(with: request, completionHandler: { (data, response, error) in
if error == nil {
if let httpResponse = response as? HTTPURLResponse {
switch (httpResponse.statusCode) {
case 200:
if let data = data {
completion(data)
}
default:
print(httpResponse.statusCode)
}
}
} else {
DispatchQueue.main.async {
if let error = error {
print("Error: \(error.localizedDescription)") }
return
}
}
})
dataTask.resume()
}
Swift syntax dictates that you must declare closures with a return type after the ->.
You have two options:
typealias JSONData = (Data) -> Void
typealias JSONData = (Data) -> ()
I see Apple using #1 most frequently.
When i try to hit Xcode server code coverage API by passing integration ID, instead of JSON response it is downloading a .bz2 file directly. I want to show the file wise coverage report in my custom dashboard using this API.
Is there any way i can get JSOn response from this API (https://developer.apple.com/library/content/documentation/Xcode/Conceptual/XcodeServerAPIReference/CodeCoverage.html) instead of .bz2 file?
Unfortunately, the API only returns the .bz2 compressed JSON file. Even when specifying a HTTP Header of Accept=application/json.
The only way around this is to decompress the data to access the underlying JSON.
Here's an example of what this could look like on iOS/swift using the framework BZipCompression to decompress the data stream:
import Foundation
import BZipCompression
public class Coverage {
public typealias CoverageCompletion = (_: Data?, _: Error?) -> Void
public enum Errors: Error {
case invalidURL
case invalidResponse
case invalidStatusCode
case invalidData
}
static var session: URLSession {
let session = URLSession(configuration: URLSessionConfiguration.default, delegate: LocalhostSessionDelegate.default, delegateQueue: nil)
return session
}
static public func coverage(forIntegrationWithIdentifier identifier: String, completion: #escaping CoverageCompletion) {
guard let url = URL(string: "https://localhost:20343/api/integrations/\(identifier)/coverage") else {
completion(nil, Errors.invalidURL)
return
}
let request = URLRequest(url: url)
let task = session.dataTask(with: request) { (data, response, error) in
guard error == nil else {
completion(nil, error)
return
}
guard let urlResponse = response as? HTTPURLResponse else {
completion(nil, Errors.invalidResponse)
return
}
guard urlResponse.statusCode == 200 else {
completion(nil, Errors.invalidStatusCode)
return
}
guard let d = data else {
completion(nil, Errors.invalidData)
return
}
var decompressedData: Data
do {
decompressedData = try self.decompress(data: d)
} catch let decompressionError {
completion(nil, decompressionError)
return
}
completion(decompressedData, nil)
}
task.resume()
}
static internal func decompress(data: Data) throws -> Data {
let decompressedData = try BZipCompression.decompressedData(with: data)
guard let decompressedString = String(data: decompressedData, encoding: .utf8) else {
throw Errors.invalidData
}
guard let firstBrace = decompressedString.range(of: "{") else {
throw Errors.invalidData
}
guard let lastBrace = decompressedString.range(of: "}", options: .backwards, range: nil, locale: nil) else {
throw Errors.invalidData
}
let range = decompressedString.index(firstBrace.lowerBound, offsetBy: 0)..<decompressedString.index(lastBrace.lowerBound, offsetBy: 1)
let json = decompressedString.substring(with: range)
guard let validData = json.data(using: .utf8) else {
throw Errors.invalidData
}
return validData
}
}
/// Class implementing the NSURLSessionDelegate which forcefully bypasses untrusted SSL Certificates.
public class LocalhostSessionDelegate: NSObject, URLSessionDelegate {
static public var `default` = LocalhostSessionDelegate()
// MARK: - NSURLSessionDelegate
#objc open func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard challenge.previousFailureCount < 1 else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
var credentials: URLCredential?
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
if let serverTrust = challenge.protectionSpace.serverTrust {
credentials = URLCredential(trust: serverTrust)
}
}
completionHandler(.useCredential, credentials)
}
}
I've noticed that the decompressed data often includes invalid control characters and other garbage at the beginning and end of the valid JSON block. The decompress() cleans up the data before returning it in the completion block.
You may want to check out my swift XCServerAPI framework on GitHub. I'll be adding the Code Coverage endpoint with this exact solution.