web-dev-qa-db-fra.com

Alamofire: comment gérer les erreurs à l'échelle mondiale

Ma question est assez similaire à celle-ci, mais pour Alamofire: AFNetworking: Gérer l'erreur globalement et répéter la demande

Comment être capable de détecter globalement une erreur (généralement un 401) et de la gérer avant que d'autres requêtes ne soient faites (et finalement échoué si elles ne sont pas gérées)?

Je pensais à enchaîner un gestionnaire de réponse personnalisé, mais c'est idiot de le faire à chaque demande de l'application.
Peut-être une sous-classe, mais quelle classe dois-je sous-classer pour gérer cela?

36
Sylver

La gestion de l'actualisation de 401 réponses dans un flux oauth est assez compliquée compte tenu de la nature parallèle des sessions NSURL. J'ai passé pas mal de temps à créer une solution interne qui a extrêmement bien fonctionné pour nous. Ce qui suit est un extraction de très haut niveau de l'idée générale de sa mise en œuvre.

import Foundation
import Alamofire

public class AuthorizationManager: Manager {
    public typealias NetworkSuccessHandler = (AnyObject?) -> Void
    public typealias NetworkFailureHandler = (NSHTTPURLResponse?, AnyObject?, NSError) -> Void

    private typealias CachedTask = (NSHTTPURLResponse?, AnyObject?, NSError?) -> Void

    private var cachedTasks = Array<CachedTask>()
    private var isRefreshing = false

    public func startRequest(
        method method: Alamofire.Method,
        URLString: URLStringConvertible,
        parameters: [String: AnyObject]?,
        encoding: ParameterEncoding,
        success: NetworkSuccessHandler?,
        failure: NetworkFailureHandler?) -> Request?
    {
        let cachedTask: CachedTask = { [weak self] URLResponse, data, error in
            guard let strongSelf = self else { return }

            if let error = error {
                failure?(URLResponse, data, error)
            } else {
                strongSelf.startRequest(
                    method: method,
                    URLString: URLString,
                    parameters: parameters,
                    encoding: encoding,
                    success: success,
                    failure: failure
                )
            }
        }

        if self.isRefreshing {
            self.cachedTasks.append(cachedTask)
            return nil
        }

        // Append your auth tokens here to your parameters

        let request = self.request(method, URLString, parameters: parameters, encoding: encoding)

        request.response { [weak self] request, response, data, error in
            guard let strongSelf = self else { return }

            if let response = response where response.statusCode == 401 {
                strongSelf.cachedTasks.append(cachedTask)
                strongSelf.refreshTokens()
                return
            }

            if let error = error {
                failure?(response, data, error)
            } else {
                success?(data)
            }
        }

        return request
    }

    func refreshTokens() {
        self.isRefreshing = true

        // Make the refresh call and run the following in the success closure to restart the cached tasks

        let cachedTaskCopy = self.cachedTasks
        self.cachedTasks.removeAll()
        cachedTaskCopy.map { $0(nil, nil, nil) }

        self.isRefreshing = false
    }
}

La chose la plus importante à retenir ici est que vous ne voulez pas exécuter un appel de rafraîchissement pour chaque 401 qui revient. Un grand nombre de demandes peuvent être lancées en même temps. Par conséquent, vous souhaitez agir sur le premier 401 et mettre en file d'attente toutes les demandes supplémentaires jusqu'à ce que le 401 réussisse. La solution que j'ai décrite ci-dessus fait exactement cela. Toute tâche de données démarrée via la méthode startRequest sera automatiquement actualisée si elle atteint un 401.

Certaines autres choses importantes à noter ici qui ne sont pas prises en compte dans cet exemple très simplifié sont:

  • Sécurité des fils
  • Succès ou échec des appels garantis
  • Stockage et récupération des jetons oauth
  • Analyse de la réponse
  • Distribution de la réponse analysée au type approprié (génériques)

Espérons que cela aide à faire la lumière.


Mise à jour

Nous avons maintenant publié ???????? Alamofire 4.0 ???????? qui ajoute les protocoles RequestAdapter et RequestRetrier vous permettant de construire facilement votre propre système d'authentification indépendamment des détails d'implémentation de l'autorisation! Pour plus d'informations, veuillez consulter notre README qui contient un exemple complet de la façon dont vous pouvez implémenter le système OAuth2 dans votre application.

Divulgation complète: L'exemple du README est uniquement destiné à être utilisé comme exemple. Veuillez s'il vous plaît s'il vous plaît ne PAS allez simplement copier-coller le code dans une application de production.

96
cnoon

dans Alamofire 5, vous pouvez utiliser RequestInterceptor Voici ma gestion des erreurs pour l'erreur 401 dans l'un de mes projets, toutes les demandes que je lui transmets EnvironmentInterceptor, la fonction de nouvelle tentative sera appelée si la demande aboutit à une erreur et l'adapt func peut également aider vous pour ajouter une valeur par défaut à vos demandes

struct EnvironmentInterceptor: RequestInterceptor {

func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (AFResult<URLRequest>) -> Void) {
    var adaptedRequest = urlRequest
    guard let token = KeychainWrapper.standard.string(forKey: KeychainsKeys.token.rawValue) else {
        completion(.success(adaptedRequest))
        return
    }
    adaptedRequest.setValue("Bearer \(token)", forHTTPHeaderField: HTTPHeaderField.authentication.rawValue)
    completion(.success(adaptedRequest))
}

func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) {
    if let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 {
        //get token

        guard let refreshToken = KeychainWrapper.standard.string(forKey: KeychainsKeys.refreshToken.rawValue) else {
            completion(.doNotRetryWithError(error))
            return
        }

        APIDriverAcountClient.refreshToken(refreshToken: refreshToken) { res in
            switch res {
            case .success(let response):
                let saveAccessToken: Bool = KeychainWrapper.standard.set(response.accessToken, forKey: KeychainsKeys.token.rawValue)
                let saveRefreshToken: Bool = KeychainWrapper.standard.set(response.refreshToken, forKey: KeychainsKeys.refreshToken.rawValue)
                let saveUserId: Bool = KeychainWrapper.standard.set(response.userId, forKey: KeychainsKeys.uId.rawValue)
                print("is accesstoken saved ?: \(saveAccessToken)")
                print("is refreshToken saved ?: \(saveRefreshToken)")
                print("is userID saved ?: \(saveUserId)")
                completion(.retry)
                break
            case .failure(let err):
                //TODO logout
                break

            }

        }
    } else {
        completion(.doNotRetry)
    }
}

et vous pouvez l'utiliser comme ceci:

@discardableResult
private static func performRequest<T: Decodable>(route: ApiDriverTrip, decoder: JSONDecoder = JSONDecoder(), completion: @escaping (AFResult<T>)->Void) -> DataRequest {

    return AF.request(route, interceptor: EnvironmentInterceptor())
        .responseDecodable (decoder: decoder){ (response: DataResponse<T>) in
         completion(response.result)
}
2
Hamed safari