Swift Siesta - 如何将异步代码包含到请求链中?

Posted

技术标签:

【中文标题】Swift Siesta - 如何将异步代码包含到请求链中?【英文标题】:Swift Siesta - How to include asynchronous code into a request chain? 【发布时间】:2020-03-15 12:30:38 【问题描述】:

我尝试使用 Siesta 装饰器来启用一个流程,当登录的用户收到 401 时,我的 authToken 会自动刷新。对于身份验证,我使用 Firebase。

在 Siesta 文档中,有一个关于如何链接 Siesta 请求的直接示例,但我找不到如何让异步 Firebase getIDTokenForcingRefresh:completion: 在这里工作的方法。问题是 Siesta 总是希望返回一个 Request 或 RequestChainAction,而这对于 Firebase auth token refresh api 是不可能的。

我了解请求链接主要用于仅 Siesta 的用例。但是有没有办法使用像 FirebaseAuth 这样不完全适合的异步第三方 API?

代码如下:

init() 
    configure("**") 
        $0.headers["jwt"] = self.authToken
        
        $0.decorateRequests 
          self.refreshTokenOnAuthFailure(request: $1)
       
  

func refreshTokenOnAuthFailure(request: Request) -> Request 
  return request.chained 
    guard case .failure(let error) = $0.response,  // Did request fail…
      error.httpStatusCode == 401 else            // …because of expired token?
        return .useThisResponse                    // If not, use the response we got.
    

    return .passTo(
      self.createAuthToken().chained              // If so, first request a new token, then:
        if case .failure = $0.response            // If token request failed…
          return .useThisResponse                  // …report that error.
         else 
          return .passTo(request.repeated())       // We have a new token! Repeat the original request.
        
      
    )
  


//What to do here? This should actually return a Siesta request
func createAuthToken() -> Void 
  let currentUser = Auth.auth().currentUser
  currentUser?.getIDTokenForcingRefresh(true)  idToken, error in
    if let error = error 
      // Error
      return;
    
    self.authToken = idToken
    self.invalidateConfiguration()
  

编辑:

根据Adrian 的建议答案,我尝试了以下解决方案。它仍然无法按预期工作:

我使用request().post 发送请求 使用该解决方案,我在回调中收到“请求已取消”失败 createUser 的回调被调用后,原始请求被更新后的 jwt 令牌发送 这个带有正确 jwt 令牌的新请求会丢失,因为没有为响应调用 createUser 的回调 -> 所以在这种情况下永远不会达到 onSuccess

如何确保仅在使用更新的 jwt 令牌发送原始请求后才调用 createUser 的回调? 这是我不工作的解决方案 - 很高兴有任何建议:

 // This ends up with a requestError "Request Cancelled" before the original request is triggered a second time with the refreshed jwt token.
    func createUser(user: UserModel, completion: @escaping CompletionHandler) 
    do 
        let userAsDict = try user.asDictionary()
        Api.sharedInstance.users.request(.post, json: userAsDict)
            .onSuccess 
                data in
                if let user: UserModel = data.content as? UserModel 
                    completion(user, nil)
                 else 
                    completion(nil, "Deserialization Error")
                
        .onFailure 
            requestError in
            completion(nil, requestError)
        
     catch let error 
        completion(nil, nil, "Serialization Error")
    

Api 类:

    class Api: Service 
    
    static let sharedInstance = Api()
    var jsonDecoder = JSONDecoder()
    var authToken: String? 
        didSet 
            // Rerun existing configuration closure using new value
            invalidateConfiguration()
            // Wipe any cached state if auth token changes
            wipeResources()
        
    
    
    init() 
        configureJSONDecoder(decoder: jsonDecoder)
        super.init(baseURL: Urls.baseUrl.rawValue, standardTransformers:[.text, .image])
        SiestaLog.Category.enabled = SiestaLog.Category.all
        
        configure("**") 
            $0.expirationTime = 1
            $0.headers["bearer-token"] = self.authToken
            $0.decorateRequests 
                self.refreshTokenOnAuthFailure(request: $1)
            
        
        
        self.configureTransformer("/users") 
            try self.jsonDecoder.decode(UserModel.self, from: $0.content)
        
        
    
    
    var users: Resource  return resource("/users") 
    
    func refreshTokenOnAuthFailure(request: Request) -> Request 
        return request.chained 
            guard case .failure(let error) = $0.response,  // Did request fail…
                error.httpStatusCode == 401 else            // …because of expired token?
                    return .useThisResponse                    // If not, use the response we got.
            
            return .passTo(
                self.refreshAuthToken(request: request).chained           // If so, first request a new token, then:
                    if case .failure = $0.response 
                        return .useThisResponse                  // …report that error.
                     else 
                        return .passTo(request.repeated())       // We have a new token! Repeat the original request.
                    
                
            )
        
    
    
    func refreshAuthToken(request: Request) -> Request 
        return Resource.prepareRequest(using: RefreshJwtRequest())
            .onSuccess 
                self.authToken = $0.text                  // …make future requests use it
        
    

RequestDelegate:

    class RefreshJwtRequest: RequestDelegate 

    func startUnderlyingOperation(passingResponseTo completionHandler: RequestCompletionHandler) 
        if let currentUser = Auth.auth().currentUser 
            currentUser.getIDTokenForcingRefresh(true)  idToken, error in
                if let error = error 
                    let reqError = RequestError(response: nil, content: nil, cause: error, userMessage: nil)
                    completionHandler.broadcastResponse(ResponseInfo(response: .failure(reqError)))
                    return;
                
                let entity = Entity<Any>(content: idToken ?? "no token", contentType: "text/plain")
                completionHandler.broadcastResponse(ResponseInfo(response: .success(entity)))            
         else 
            let authError = RequestError(response: nil, content: nil, cause: AuthError.NOT_LOGGED_IN_ERROR, userMessage: "You are not logged in. Please login and try again.".localized())
            completionHandler.broadcastResponse(ResponseInfo(response: .failure(authError)))
        
    
    
    func cancelUnderlyingOperation() 

    func repeated() -> RequestDelegate  RefreshJwtRequest() 

    private(set) var requestDescription: String = "CustomSiestaRequest"

【问题讨论】:

这能回答你的问题吗? How to decorate Siesta request with an asynchronous task 【参考方案1】:

首先,您应该重新表述您的问题的主旨,使其不是 Firebase 特定的,按照“如何使用一些任意异步代码而不是请求进行请求链接?”。这样对社区会更有用。然后您可以提及 Firebase 身份验证是您的特定用例。我会相应地回答你的问题。

(编辑:已经回答了这个问题,我现在看到 Paul 已经在这里回答了这个问题:How to decorate Siesta request with an asynchronous task)

Siesta 的 RequestDelegate 可以满足您的需求。引用文档:“这对于获取非标准网络请求的内容非常有用,并将它们包装起来,使它们看起来像 Siesta 一样。要创建自定义请求,请将您的委托传递给 Resource.prepareRequest(using:)。”

您可能会使用这样的东西作为一个粗略的起点 - 它运行一个闭包(在您的情况下是 auth 调用),该闭包要么成功但没有输出,要么返回错误。根据使用情况,您可以调整它以使用实际内容填充实体。

// todo better name
class SiestaPseudoRequest: RequestDelegate 
    private let op: (@escaping (Error?) -> Void) -> Void

    init(op: @escaping (@escaping (Error?) -> Void) -> Void) 
        self.op = op
    

    func startUnderlyingOperation(passingResponseTo completionHandler: RequestCompletionHandler) 
        op 
            if let error = $0 
                // todo better
                let reqError = RequestError(response: nil, content: nil, cause: error, userMessage: nil)
                completionHandler.broadcastResponse(ResponseInfo(response: .failure(reqError)))
            
            else 
                // todo you might well produce output at this point
                let ent = Entity<Any>(content: "", contentType: "text/plain")
                completionHandler.broadcastResponse(ResponseInfo(response: .success(ent)))
            
        
    

    func cancelUnderlyingOperation() 

    func repeated() -> RequestDelegate  SiestaPseudoRequest(op: op) 

    // todo better
    private(set) var requestDescription: String = "SiestaPseudoRequest"

我发现的一个问题是响应转换器不会针对此类“请求”运行 - 转换器管道特定于 Siesta 的 NetworkRequest。 (这让我大吃一惊,我不确定我是否喜欢它,但 Siesta 似乎通常充满了正确的决定,所以我主要是相信它有充分的理由。)

可能值得留意其他非请求类行为。

【讨论】:

以上是关于Swift Siesta - 如何将异步代码包含到请求链中?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Swift 上使这段代码异步

在 Siesta 中覆盖本地数据?

Siesta 作为 SwiftPM 在 Linux 上的依赖项

Siesta configureTransformer for Resource 带参数

Swift 异步方法和返回/完成块

Siesta 加载在陈旧资源上失败