HaroTear
HaroTear

Reputation: 23

Combine sink does not complete when publisher fails

I'm currently building a SwiftUI app and used a networking library from github (GitHub/Squid) to implement the api calls.

It works fine except when the request fails, then the library internal debug messages indicate an error (404 in my test), but the Combine subscriber never receives a completion signal.

I created a new request that will try to fetch and decode the users of "jsonplaceholder.typicode.com".

import Foundation
import Squid

struct GetUsersFromMockApiRequest: JsonRequest {
    typealias Result = [User]

    var routes: HttpRoute {
        ["users"]
    }
}

User is just a simple struct:

struct User: Decodable {
    let id: Int
    let username: String
    let name: String
}

In my ApiRequestManager.swift file I declared a function to request the scheduling:

public func getUsersFromMockApi() -> Response<GetUsersFromMockApiRequest> {
    return GetUsersFromMockApiRequest().schedule(with: ApiService(apiUrl: "jsonplaceholder.typicode.com"))
}

And in my view where I want to call the api, I have a cancellable set and a instance of the ApiRequestManager.

@State private var cancellableSet: Set<AnyCancellable> = []
private let requestManager = ApiRequestManager()

Now the last step is to call a function on the press of a button which executes the following code:

self.requestManager.getUsersFromMockApi()
  .print("Debug:")
  .sink(receiveCompletion: { completion in
    switch completion {
    case .failure:
      print("ApiCall failed.")
    case .finished:
      print("ApiCall finished.")
    }
  }) { result in
    print("Received users from apiCall: \(result)")
  }
  .store(in: &cancellableSet)

If the api request is sucessfull, everything works out fine. But as soon as it hits any error the sink will get any signals / completion at all.

I tried to print every message that the subscriber received, breakOnError and also followed the library step by step to find out if he even throws an error. It does correctly check the HTTP response code against the accepted status codes in the schedule() function in the NetworkScheduler.swift file and throws a custom error throw Squid.Error.requestFailed(statusCode: statusCode, response: response.data) on line 285.

However with my lacking debug and combine knowledge I have a hard time following the throw afterwards.

Upvotes: 2

Views: 1324

Answers (1)

heckj
heckj

Reputation: 7367

The issue you're seeing may be that a 404 isn't considered an error by underlying Cocoa libraries (URLSession specifically). This seems like something that's being done (or not) within the Squid library, and you're reacting to what it provides.

URLSession itself only throws errors when it's unable to get a response from the server - so when issues are related to not being able to resolve the hostname or make a connection kinds of things. If you do make a connection and get a response, URLSession won't throw an error, but the status code will be correctly reflected - it'll just "look" like a successful request.

The example above doesn't show that section, but what happens with a 404 response from URLSession is that the request completes, and the status code 404 is encoded within the response, but it's not thrown as an error state.

A typical pattern of dealing with this in a Combine publisher chain is by passing the result data from URLSession into a tryMap operator, where you can do further inspection of the result (status code, data, etc) and determine if you want to turn that into a thrown error (such as if you get a 404 response).

You can find an example of this kind of pattern at https://heckj.github.io/swiftui-notes/#patterns-datataskpublisher-trymap. The gist of this pattern is allowing you to define whatever error you want (assuming you a thrown exception in those cases) based on inspection of the result.

You can find other examples of using Combine (along with URLSession.dataTaskPublisher) in Using Combine.

Upvotes: 3

Related Questions