Alamofire:如何在全球范围内处理错误

时间:2015-02-26 02:06:06

标签: ios swift networking alamofire

我的问题与此问题非常相似,但对于Alamofire:AFNetworking: Handle error globally and repeat request

如何能够捕获全局错误(通常是401)并在发出其他请求之前处理它(如果不管理则最终会失败)?

我正在考虑链接自定义响应处理程序,但在应用程序的每个请求上执行此操作都很愚蠢。
也许是子类化,但是我应该将哪个类子类化来处理它?<​​/ p>

2 个答案:

答案 0 :(得分:95)

考虑到NSURLSessions的并行特性,在oauth流中处理401响应的刷新非常复杂。我花了很多时间构建一个对我们来说非常好的内部解决方案。以下是对如何实施它的一般概念的非常高级别的提取。

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
    }
}

这里要记住的最重要的事情是,您不希望为每个返回的401运行刷新调用。大量请求可以同时竞赛。因此,您希望对第一个401执行操作,并将所有其他请求排入队列,直到401成功为止。我上面概述的解决方案确实如此。通过startRequest方法启动的任何数据任务将在达到401时自动刷新。

这个非常简单的例子中没有考虑的其他重要事项是:

  • 线程安全性
  • 保证成功或失败关闭电话
  • 存储和获取oauth令牌
  • 解析回复
  • 将解析后的响应转换为适当的类型(泛型)

希望这有助于解决问题。


更新

我们现在发布了Alamofire 4.0,它添加了RequestAdapterRequestRetrier协议,无论授权实现细节如何,您都可以轻松构建自己的身份验证系统!有关详情,请参阅我们的README,其中提供了有关如何在您的应用中实施OAuth2系统的完整示例。

  

完全披露:自述文件中的示例仅用作示例。请不要只是将代码复制粘贴到生产应用程序中。

答案 1 :(得分:0)

在Alamofire 5中,您可以使用RequestInterceptor  这是我在我的一个项目中对401错误的错误处理,如果请求出错,则我将通过EnvironmentInterceptor传递给它的每个请求都将调用重试功能 并且Adapt函数可以帮助您向请求中添加默认值

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)
    }
}

,您可以像这样使用它:

@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)
}