Swift Siesta - 如何将异步代码包含到请求链中?
Swift Siesta - How to include asynchronous code into a request chain?
我尝试使用 Siesta 装饰器启用一个流程,当登录用户获得 401 时,我的 authToken 会自动刷新。对于身份验证,我使用 Firebase。
在 Siesta 文档中有一个关于如何链接 Siesta 请求的直接示例,但我找不到如何返回异步 Firebase getIDTokenForcingRefresh:completion: working here. The problem is that Siesta always expects a Request or a RequestChainAction 的方法,这是不可能的Firebase 身份验证令牌刷新 api.
我知道请求链接主要是为仅 Siesta 用例完成的。但是有没有一种方法可以使用异步第三方 API,例如 FirebaseAuth,它并不完全适合图片?
代码如下:
init() {
configure("**") {
[=11=].headers["jwt"] = self.authToken
[=11=].decorateRequests {
self.refreshTokenOnAuthFailure(request: )
}
}
func refreshTokenOnAuthFailure(request: Request) -> Request {
return request.chained {
guard case .failure(let error) = [=11=].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 = [=11=].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 令牌发送原始请求
- 由于没有为响应调用 createUser 的回调,这个带有正确 jwt 令牌的新请求丢失了 -> 所以 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:
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("**") {
[=13=].expirationTime = 1
[=13=].headers["bearer-token"] = self.authToken
[=13=].decorateRequests {
self.refreshTokenOnAuthFailure(request: )
}
}
self.configureTransformer("/users") {
try self.jsonDecoder.decode(UserModel.self, from: [=13=].content)
}
}
var users: Resource { return resource("/users") }
func refreshTokenOnAuthFailure(request: Request) -> Request {
return request.chained {
guard case .failure(let error) = [=13=].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 = [=13=].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 = [=13=].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 do I do request chaining with some arbitrary asynchronous code instead of a request?" 的思路重新表述问题的主旨,使其不是特定于 Firebase 的。这样对社区会更有用。然后您可以提及 Firebase 身份验证是您的特定用例。我将相应地回答你的问题。
(编辑:已经回答了这个问题,我现在看到保罗已经在这里回答了:)
Siesta 的 RequestDelegate
可以满足您的需求。引用文档:"This is useful for taking things that are not standard network requests, and wrapping them so they look to Siesta as if they are. To create a custom request, pass your delegate to Resource.prepareRequest(using:)
."
您可以使用类似这样的东西作为一个粗略的起点 - 它 运行 是一个闭包(在您的情况下是 auth 调用),要么成功但没有输出,要么 returns 出现错误。根据用途,您可以调整它以使用实际内容填充实体。
// 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 = [=10=] {
// 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"
}
我发现的一个问题是响应转换器不是 运行 用于这样的 "requests" - 转换器管道特定于 Siesta 的 NetworkRequest。 (这让我感到惊讶,我不确定我是否喜欢它,但 Siesta 似乎通常充满了正确的决定,所以我主要相信这是有充分理由的。)
可能值得留意其他非请求类行为。
我尝试使用 Siesta 装饰器启用一个流程,当登录用户获得 401 时,我的 authToken 会自动刷新。对于身份验证,我使用 Firebase。
在 Siesta 文档中有一个关于如何链接 Siesta 请求的直接示例,但我找不到如何返回异步 Firebase getIDTokenForcingRefresh:completion: working here. The problem is that Siesta always expects a Request or a RequestChainAction 的方法,这是不可能的Firebase 身份验证令牌刷新 api.
我知道请求链接主要是为仅 Siesta 用例完成的。但是有没有一种方法可以使用异步第三方 API,例如 FirebaseAuth,它并不完全适合图片?
代码如下:
init() {
configure("**") {
[=11=].headers["jwt"] = self.authToken
[=11=].decorateRequests {
self.refreshTokenOnAuthFailure(request: )
}
}
func refreshTokenOnAuthFailure(request: Request) -> Request {
return request.chained {
guard case .failure(let error) = [=11=].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 = [=11=].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 令牌发送原始请求
- 由于没有为响应调用 createUser 的回调,这个带有正确 jwt 令牌的新请求丢失了 -> 所以 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:
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("**") {
[=13=].expirationTime = 1
[=13=].headers["bearer-token"] = self.authToken
[=13=].decorateRequests {
self.refreshTokenOnAuthFailure(request: )
}
}
self.configureTransformer("/users") {
try self.jsonDecoder.decode(UserModel.self, from: [=13=].content)
}
}
var users: Resource { return resource("/users") }
func refreshTokenOnAuthFailure(request: Request) -> Request {
return request.chained {
guard case .failure(let error) = [=13=].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 = [=13=].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 = [=13=].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 do I do request chaining with some arbitrary asynchronous code instead of a request?" 的思路重新表述问题的主旨,使其不是特定于 Firebase 的。这样对社区会更有用。然后您可以提及 Firebase 身份验证是您的特定用例。我将相应地回答你的问题。
(编辑:已经回答了这个问题,我现在看到保罗已经在这里回答了:
Siesta 的 RequestDelegate
可以满足您的需求。引用文档:"This is useful for taking things that are not standard network requests, and wrapping them so they look to Siesta as if they are. To create a custom request, pass your delegate to Resource.prepareRequest(using:)
."
您可以使用类似这样的东西作为一个粗略的起点 - 它 运行 是一个闭包(在您的情况下是 auth 调用),要么成功但没有输出,要么 returns 出现错误。根据用途,您可以调整它以使用实际内容填充实体。
// 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 = [=10=] {
// 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"
}
我发现的一个问题是响应转换器不是 运行 用于这样的 "requests" - 转换器管道特定于 Siesta 的 NetworkRequest。 (这让我感到惊讶,我不确定我是否喜欢它,但 Siesta 似乎通常充满了正确的决定,所以我主要相信这是有充分理由的。)
可能值得留意其他非请求类行为。