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 - 如何将异步代码包含到请求链中?的主要内容,如果未能解决你的问题,请参考以下文章
Siesta 作为 SwiftPM 在 Linux 上的依赖项