ios – Refresh token using Swift Combine


Im trying to refresh token using Combine in Swift. I put refresh part in the tryCatch part inside URLSession.shared.dataTaskPublisher but for some reason .sink returns as error even if this part in tryCatch is working and new token is fetched.

This is what I have in ViewModel:

private func testApi() {
        useCase.getSomeData
            .receive(on: DispatchQueue.main)
            .sink { completion in
                switch completion {
                    case .failure(_):
                        //This is where Im getting error code!
                    case .finished: break
                }
            } receiveValue: { [weak self] response in
                print(response
            }
            .store(in: &cancellables)
    }

And this func’s are in my Network class:

   public func request<T>(_ req: NetworkRequest) -> AnyPublisher<T, NetworkError> where T: Decodable, T: Encodable {
        guard let url = URL(string: req.url) else {
            return AnyPublisher(
                Fail<T, NetworkError>(error: NetworkError.badURL("Invalid Url"))
            )
        }
        
        let urlRequest = req.buildURLRequest(with: url)
        
        return URLSession
            .shared
            .dataTaskPublisher(for: urlRequest)
            .tryMap { output in
                let code = (output.response as? HTTPURLResponse)?.statusCode ?? 200
                
                let httpStatusCode = HTTPStatusCode(rawValue: code) ?? .noResponse
                let outputData = output.data

                let message = self.responseErrorMessage(outputData)
                guard output.response is HTTPURLResponse else {
                    throw NetworkError.serverError(code: code, error: message)
                }
                
                switch httpStatusCode {
                    case .ok, .created, .accepted:
                        
                        return output.data
                    case .badRequest, .notFound:
                        throw NetworkError.badRequest(code: code, error: message)
                    case .conflict:
                        throw NetworkError.unknown(code: code, error: message)
                    case .tokenExpired:
                        throw NetworkError.unknown(code: code, error: message)
                    case .unauthorised:
                        throw NetworkError.unauthorised(code: code, error: message)
                    case .paymentRequired, .forbidden, .methodNotAllowed, .requestTimeout, .unsupportedMediaType, .tooManyRequests:
                        throw NetworkError.unknown(code: code, error: message)
                    case .noResponse:
                        throw NetworkError.noResponse(message)
                    case .internalServerError, .serviceUnavailable:
                        throw NetworkError.serverError(code: code, error: message)
                }
            }
            .tryCatch { [weak self] error -> AnyPublisher<Data, NetworkError> in
                guard let self = self else { throw error }
                if error as? NetworkError == .unauthorised(code: 401, error: "Unauthorized") {
                    return refreshToken
                        .flatMap { _ -> AnyPublisher<Data, NetworkError> in
                            self.request(self.updateRequest(req))
                        }
                        .eraseToAnyPublisher()
                }
                throw error
            }
            .decode(type: T.self, decoder: jsonDecoder)
            .mapError { error in
                if let error = error as? NetworkError {
                    return error
                }
                return NetworkError.invalidJSON(String(describing: error))
            }
            .eraseToAnyPublisher()
    }

This is refresh part:

    private var refreshToken: AnyPublisher<Void, NetworkError> {
        URLSession
            .shared
            .dataTaskPublisher(for: buildRefreshRequest())
            .map(\.data)
            .decode(type: LoginResponseData.self, decoder: jsonDecoder)
            .map { response in
                let token = response.accessToken
                UserDefaults.token = token
            }
            .mapError { error in
                if let error = error as? NetworkError {
                    return error
                }
                return NetworkError.invalidJSON(String(describing: error))
            }
            .eraseToAnyPublisher()
    }

So in short, when I call useCase.getSomeData, I simulate token expire in debug mode by deleting token from headers, after that Im getting 401 in tryMap and in tryCatch catches that error and calling refreshToken and ‘self.request(self.updateRequest(req))’ is called after new token is fetched but Im not getting code in ‘receiveValue: { [weak self] response ‘ in viewModel, instead it completes in ‘.sink ‘ as a failure.

Im fresh in Combine and trying to implement this 401/refreshToken part but I could not figure out what Im missing here.

Latest articles

spot_imgspot_img

Related articles

Leave a reply

Please enter your comment!
Please enter your name here

spot_imgspot_img