I'm learning Swift´s Combine by making an app, trying to solve real world use cases.
A common case, I'm performing a request, and the auth token expired. I would like to refresh the token if the request fails with a 401.
Something like:
fetchData()
.flatMap { data, response
if response.statusCode == 401 {
refreshToken()
.fetchData()
} else {
Just(data)
}
}
.keepDoingThings()
Keep in mind this is just pseudo code.
I have tried a few things indeed, but it's a mess. :)
How can this be done?
Thank you!
I tried porting my RxSwift version of this to Combine, but the latter is missing some key operators (retryWhen and flatMapFirst). One solution, therefore, is to bring RxCombine and RxSwift into your project so you can use my RxSwift solution in your project.
Here's my implementation: https://medium.com/#danielt1263/retrying-a-network-request-despite-having-an-invalid-token-b8b89340d29
RxCombine: https://github.com/freak4pc/RxCombine
RxSwift: https://github.com/ReactiveX/RxSwift
I don't expect this answer to be accepted as correct, but it can be a work-around until the necessary operators are added to Combine.
You can use tryMap() to throw Error before refreshToken, then do retry.
fetchData()
.tryMap { data, response in
if response.statusCode == 401 {
refreshToken()
.fetchData()
throw __MYERROR__.invalidServerResponse
} else {
return data
}
}
.retry(3)
.keepDoingThings()
Related
What is the correct way to alter a Request performing an asynchronous task before the Request happens?
So any request Rn need to become transparently Tn then Rn.
A little of background here: The Task is a 3rd party SDK that dispatch a Token I need to use as Header for the original request.
My idea is to decorate the Rn, but in doing this I need to convert my Tn task into a Siesta Request I can chain then.
So I wrapped the Asynchronous Task and chained to my original request.
Thus any Rn will turn into Tn.chained { .passTo(Rn) }
In that way, this new behaviour is entirely transparent for the whole application.
The problem
Doing this my code end up crashing in a Siesta internal precondition:
precondition(completedValue == nil, "notifyOfCompletion() already called")
In my custom AsyncTaskRequest I collect the callbacks for success, failure, progress etc, in order to trigger them on the main queue when the SDK deliver the Token.
I noticed that removing all the stored callback once they are executed, the crash disappear, but honestly I didn't found the reason why.
I hope there are enough informations for some hints or suggests.
Thank you in advance.
Yes, implementing Siesta’s Request interface is no picnic. Others have had exactly the same problem — and luckily Siesta version 1.4 includes a solution.
Documentation for the new feature is still thin. To use the new API, you’ll implement the new RequestDelegate protocol, and pass your implementation to Resource.prepareRequest(using:). That will return a request that you can use in a standard Siesta request chain. The result will look something like this (WARNING – untested code):
struct MyTokenHandlerThingy: RequestDelegate {
// 3rd party SDK glue goes here
}
...
service.configure(…) {
if let authToken = self.authToken {
$0.headers["X-Auth-Token"] = authToken // authToken is an instance var or something
}
$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.refreshAuthToken().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.
}
}
)
}
}
func refreshAuthToken() -> Request {
return Request.prepareRequest(using: MyTokenHandlerThingy())
.onSuccess {
self.authToken = $0.jsonDict["token"] as? String // Store the new token, then…
self.invalidateConfiguration() // …make future requests use it
}
}
}
To understand how to implement RequestDelegate, you best bet for now is to look at the new API docs directly in the code.
Since this is a brand new feature not yet released, I’d greatly appreciate a report on how it works for you and any troubles you encounter.
the documentation shows how to make targets require bearer tokens, which I did like this
extension MyService: AccessTokenAuthorizable {
var authorizationType: AuthorizationType {
switch self {
case .resetPassword, .postTextBook, .bookmarkBook, .getBookmarks, .logout, .verify:
return .bearer
default:
return .none
}
}
}
then it shows how to add tokens to the providers, which I did like this
let token = "abc123"
let authPlugin = AccessTokenPlugin(tokenClosure: token)
let provider = MoyaProvider<MyService>(plugins: [authPlugin])
but when the token expires, how can I change the token? and does Moya offer a way to automate this process, where if I get a forbidden http response (meaning I am not authorized), it automatically requests a token?
The implementation details of authentication/authorization can be quite different for each API out there. This is the reason why Moya will not handle the auth for you.
That said, implementing your own authentication/authorization can be done in many ways. It will depend on your constraints and/or preferences. As of today, you can find a few solutions sparsely outlined in Moya documentation:
Use the PluginType to add your auth to the requests. But think that this can potentially be used to refresh the token if needed. You may also need to intercept the completion of the request to detect authorization errors and apply your preferred recovery scenario (eg. refresh the token and retry the call).
Same can be implemented using the endpointClosure and/or requestClosure.
You can also consider implementing Alamofire's RequestAdapter and RequestRetrier. Depending on your needs, this can make retries easier. However, on them you will not have straightforward access to your TargetType, so you may need to find a way to recognize the different auth methods needed (ie. your bearer or none).
A few direct references to their documentation:
Plugins
Endpoints
Authentication
Alamofire Automatic Validation
Also, I highly encourage anybody to learn/get inspiration from Eilodon's Networking source code.
for change/refresh token i used this
static func send(request: TargetType) -> PrimitiveSequence<SingleTrait, Response> {
return provider.rx.request(request)
.retry(1)
.observeOn(ConcurrentDispatchQueueScheduler.init(qos: .default))
.filterSuccessfulStatusAndRedirectCodes()
.retryWhen({ (errorObservable: Observable<Error>) in
errorObservable.flatMap({ (error) -> Single<String> in
if let moyaError: MoyaError = error as? MoyaError, let response: Response = moyaError.response {
if **check forbidden http responses here** {
return provider.rx.request(.refreshToken(*your refresh token here*))
.filterSuccessfulStatusCodes()
.mapString(atKeyPath: "*json path to new access token*")
.catchError { (_) in
logout()
throw error
}
.flatMap({ (newAccessToken) -> PrimitiveSequence<SingleTrait, String> in
changeAccessToken()
return Single.just(newAccessToken)
})
}
}
throw error
})
})
}
static func logout() {
// logout action
}
static func changeAccessToken() {
// set new access token
}
When I try to access external API's for my google action from my webhook which is hosted on firebase functions, I am getting back only partial content. It stops getting the whole data provided by the api.
For example I tried getting data from wikipedia api using this code
var request = require('request');//required module
//inside the function
request({ method: 'GET',url:'https://en.wikipedia.org/w/api.php?action=query&prop=extracts&format=json&explaintext=&exsectionformat=plain&redirects=&titles=11_September'},function (error, response, body)
{
if (!error && response.statusCode == 200)
{
console.log(body);
}
});
app.ask('data obtained');
Can anyone please help me out with this.
I am having a pay as you go firebase account that allows egress of data.
From just the code fragment, the problem is that you're replying to the user outside the callback from request(). This means that it is handled immediately and the function may end before the entire body has been received. Try something like this (I've also changed ask() to tell() since you're not prompting for another response here, and you shouldn't leave the microphone open.)
var request = require('request');//required module
//inside the function
request({ method: 'GET',url:'https://en.wikipedia.org/w/api.php?action=query&prop=extracts&format=json&explaintext=&exsectionformat=plain&redirects=&titles=11_September'},function (error, response, body)
{
if (!error && response.statusCode == 200)
{
console.log(body);
app.tell('data obtained');
}
});
I am trying to make a REST API call as below and once the call is complete, I want to print "Done".
But with the below example "Done" is getting printed even before the REST call is complete.
return this.remote
.then(function() {
request('http://www.google.com', function (error, response, body) {
if (!error && response.statusCode == 200) {
console.log(body) // Show the HTML for the Google homepage.
}
})
})
.then(function() {
console.log("Done")
})
Am I missing something here? If this not the right way, could someone please let me know what the right way is.
Thanks.
For Leadfoot (the library that Intern uses to drive functional tests) to keep track of an asynchronous operation, asynchronous operations in then callbacks should return Promises (or thenables). Luckily, request returns a promise, so just do:
.then(function () {
return request('...', function (...) {
...
});
})
I find transactions (https://www.firebase.com/docs/transactions.html) to be a cool way of handling concurrency, however it seems they can only be done from clients.
The way we use Firebase is mainly by writing data from our servers and observing them on clients. Is there a way to achieve optimistic concurrency model when writing data via REST API?
Thanks!
You could utilize an update counter to make write ops work in a similar way to transactions. (I'm going to use some pseudo-code below; sorry for that but I didn't want to write out a full REST API for an example.)
For example, if I have an object like this:
{
total: 100,
update_counter: 0
}
And a write rule like this:
{
".write": "newData.hasChild('update_counter')",
"update_counter": {
".validate": "newData.val() === data.val()+1"
}
}
I could now prevent concurrent modifications by simply passing in the update_counter with each operation. For example:
var url = 'https://<INSTANCE>.firebaseio.com/path/to/data.json';
addToTotal(url, 25, function(data) {
console.log('new total is '+data.total);
});
function addToTotal(url, amount, next) {
getCurrentValue(url, function(in) {
var data = { total: in.total+amount, update_counter: in.update_counter+1 };
setCurrentValue(ref, data, next, addToTotal.bind(null, ref, amount, next));
});
}
function getCurrentValue(url, next) {
// var data = (results of GET request to the URL)
next( data );
}
function setCurrentValue(url, data, next, retryMethod) {
// set the data with a PUT request to the URL
// if the PUT fails with 403 (permission denied) then
// we assume there was a concurrent edit and we need
// to try our pseudo-transaction again
// we have to make some assumptions that permission_denied does not
// occur for any other reasons, so we might want some extra checking, fallbacks,
// or a max number of retries here
// var statusCode = (server's response code to PUT request)
if( statusCode === 403 ) {
retryMethod();
}
else {
next(data);
}
}
FYI, Firebase Realtime Database officially supports this now.
Read the blog and the docs for more info.
check out Firebase-Transactions project: https://github.com/vacuumlabs/firebase-transactions
I believe, this may be quite handy for your case, especially if you do a lot of writes from the server.
(disclaimer: I'm one of the authors)