RxJs Observables: run retryWhen after some more async requests - javascript

My use case is:
User requests asset from our API which fails because of JWT expiring (passed as an httpOnly cookie) - API returns a 401 status code.
We go and authenticate them (without the user doing anything) again using a refresh_token to retrieve a new JWT with a request from our client to auth0.
We send that new JWT to our API to be set as an httpOnly cookie to replace the expired one.
We then want to retry the original request the user made to the API in step 1.
I'm trying to use Observables within my Redux app with redux-observable. If you can think of another way of making the above user flow work I would be happy to hear how.
NB. Im using rxjs V5
export const fetchAssetListEpic = (action$, store) => {
return action$.ofType('FETCH_ASSET_LIST')
.switchMap( action => {
const options = {
crossDomain: true,
withCredentials: true,
url: uriGenerator('assetList', action.payload)
};
return ajax(options);
})
.map(fetchAssetListSuccess)
.retryWhen(handleError)
.catch(redirectToSignIn);
};
function handleError(err) {
return (err.status === 401) ?
/* Authenticate here [Step 2] */
/* Send new JWT to API [Step 3] */
/* If successful make original request again [Step 4] */
:
Observable.throw(err);
}
function redirectToSignIn() {
/*I will redirect here*/
}
So far I able to complete steps 1, 2 and 3 but not too sure of a way to add step 4. I may be completely off the mark but any help would be great!

Well one thing you probably won't want to do is allow the error to make it to the top level stream. Even if you do a catch you have effectively killed the top level stream. So unless your redirect is doing a hard redirect instead of a a soft one via something like react-router, you won't be able to use this epic any more.
Thus I would say that you want most of the logic to be encapsulated within the switchMap:
function withAuthorizedFlow(source) {
return source
.map(fetchAssetListSuccess)
// retryWhen takes a callback which accepts an Observable of errors
// emitting a next causes a retry, while an error or complete will
// stop retrying
.retryWhen(e => e.flatMap(err =>
Observable.if(
// Returns the first stream if true, second if false
() => err.status === 401,
reauthenticate, // A stream that will emit once authenticated
Observable.throw(err) // Rethrow the error
))
)
.catch(redirectToSignIn);
}
/** Within the epic **/
.switchMap(({payload}) => {
const options = {
crossDomain: true,
withCredentials: true,
url: uriGenerator('assetList', payload)
};
// Invoke the ajax request
return ajax(options)
// Attach a custom pipeline here
// Not strictly necessary but it keeps this method clean looking.
.let(withAuthorizedFlow);
})
The use of let above is completely optional, I threw it in to clean up the function. Essentially though you want to contain the error to the inner stream so that it can't halt the outer one. I am not sure which ajax library you are using but you should also confirm that it will in fact return a cold Observable otherwise you will need to wrap it in a defer block to in order for the retryWhen to work.

Related

onError Authentication with refresh token

In the Apollographql documentation it states:
The onError link can retry a failed operation based on the type of GraphQL error that's returned. For example, when using token-based authentication, you might want to automatically handle re-authentication when the token expires.
This is followed up by their sample code:
onError(({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) {
for (let err of graphQLErrors) {
switch (err.extensions.code) {
// Apollo Server sets code to UNAUTHENTICATED
// when an AuthenticationError is thrown in a resolver
case "UNAUTHENTICATED":
// Modify the operation context with a new token
const oldHeaders = operation.getContext().headers;
operation.setContext({
headers: {
...oldHeaders,
authorization: getNewToken(),
},
});
// Retry the request, returning the new observable
return forward(operation);
}
}
}
// To retry on network errors, we recommend the RetryLink
// instead of the onError link. This just logs the error.
if (networkError) {
console.log(`[Network error]: ${networkError}`);
}
});
My question is in regards to the getNewToken(), as no code was provided for this function, I want to know (assuming this is another request to the backend and I am not sure how it could not be), if you are able to and or supposed to use query/mutation in graphql or make the request through axios for example.
One problem, if it can/should be a graphql query or mutation, is to get the new token, the onError code is defined in the same file as the ApolloClient as ApolloClient needs access to onError, thus when trying to implement this as retrieving a new token through a graphql mutation I got the following error:
React Hook "useApolloClient" is called in function "refresh" that is
neither a React function component nor a custom React Hook function.
After trying to useQuery/useMutation hook and realizing I cannot outside of a react component and at the top level I found this post whose answers suggested you can use useApolloClient.mutate instead but I still ran into issues. My code was (and tried multiple iterations of this same code like useApolloClient() outside of the function and inside etc.):
const refresh = () => {
const client = useApolloClient();
const refreshFunc = () => {
client
.mutate({ mutation: GET_NEW_TOKEN })
.then((data) => {
console.log(data);
})
.catch((err) => {
console.log(err);
});
};
refreshFunc();
};
I could capitalize Refresh but this still would not work and would break the rules of hooks.
And to clarify all the above would do is I would replace the console.logs with setting session storage to the retrieved new token and then re trying the original request with onError.
Now in another post I found when looking into this, the users getNewToken request was a rest request using axios:
const getNewToken = async () => {
try {
const { data } = await axios.post(
"https://xxx/api/v2/refresh",
{ token: localStorage.getItem("refreshToken") }
);
localStorage.setItem("refreshToken", data.refresh_token);
return data.access_token;
} catch (error) {
console.log(error);
}
};
Now from my understanding, if I wanted to implement it this way I would have to change my backend to include express as I am only using apolloserver. Now I could definitely be wrong about that as my backend knowledge is quite limited and would love to be corrected their.
So my question is, what is the best way to do this, whether natively using graphql queries/mutations (if possible), doing it with axios, or maybe their is another best practice for this seemingly common task I am unaware of.

axios interceptor - hold off the request until cookie API call is finished, and only then continue

I am trying to get the axios to wait until one extra call in the interceptor finishes. So I am using NuxtJS as a frontend SPA and API in Laravel 8.
I've tried a lot of different things over the course of last ~ 4 days but nothing seems to be working.
THE GOAL
I need my axios REQUEST interceptor to check for existence of the cookie. If cookie is not present I need to make an API call first to grab the cookie and then we can continue with any other request.
WHAT I AM DOING?
So basically I have Axios interceptor for the requests that will call cookie endpoint if the cookie doesn't exist.
I am also saving cookie request promise to be reused in case there are multiple calls and the cookie still is not there.
PROBLEM
While it was supposed to just call cookie API first and everything else after I am mostly getting two results in different variations of the attached code.
A) I am making an extra cookie call but it is not in the required order so I still end up hitting laravel endpoint multiple times without cookies which causes extra sessions to spawn.
B) It is not making any calls at all (attached example).
Does anyone know what in the world I am confusing here?
export default function ({$axios, redirect, $cookiz, store}) {
$axios.onRequest(async request => {
// make sure that XSRF cookie exists before we make aby calls to prevent backend from
// creating multiple session when page on load calls more than one endpoint, if we don't have
// that cookie we will first have to get it and then call the rest of the endpoints
const xsrfCookie = $cookiz.get('XSRF-TOKEN')
if (xsrfCookie === undefined || xsrfCookie === null || xsrfCookie === '') {
await store.dispatch('login/getXsrfCookie')
$axios.request(request)
}
$axios.request(request)
})
}
getXsrfCookie(context) {
if (context.state.xsrfCookiePromise instanceof Promise) {
return context.state.xsrfCookiePromise
}
const xsrfCookiePromise = this.$axios.get('/csrf-cookie').then(response => {
context.commit('setXsrfCookiePromise', null)
console.log('This is the cookie response', response)
})
context.commit('setXsrfCookiePromise', xsrfCookiePromise)
return context.state.xsrfCookiePromise
}
I don't know anything about nuxt, and have only a vague idea about axios interceptors, but just looking at the code...
I think you want to persist a cookie, not the promise for a cookie.
I don't think you need to involve the store.
I think you can do that with your cookie plugin. If I'm right about that, using the set method is what you need. (you might need an options param, described here)
async getXsrfCookie() {
if (!$cookiz.get('XSRF-TOKEN')) {
// the op should double check which part of the response to persist, whether to stringify it, etc.
const response = await this.$axios.get('/csrf-cookie');
$cookiz.set('XSRF-TOKEN', response.data);
}
}
export default function ({$axios, redirect, $cookiz, store}) {
$axios.onRequest(async request => {
await getXsrfCookie();
return $axios.request(request)
})
}

Is it a bad practice to not check if fetch request failed?

In my app ( i'm using next.js but it's more a general question ) i have a button that updates number of likes when clicked (+1). Here is relevant part of code:
const handleLikeClick = () => {
setNumberLikes(numberLikes + 1)
fetch('/api/updateLikes?origin=so-filter', {
method: 'POST'
})
}
And my API:
import { connectToDatabase } from '../../utils/mongodb'
export default async (req, res) => {
try {
const { db } = await connectToDatabase()
const { origin } = req.query
if (req.method === 'POST') {
await db.collection('likes').findOneAndUpdate({ page: origin }, { $inc: { likes: 1 }})
res.status(200)
}
}
catch (err) {
res.status(500)
}
}
I don't really care much if this POST request fails or not, therefore, i'm not checking for it and there is no additional logic if it actually fails. Is it a bad practice to do so ? Should i actually res.status(200).json({success:'updated'}) and .then my fetch request? Thank you.
Depends on what you want to achieve at the user level.
Although the result doesn't influence the flow of your program and doesn't break it, most of the times there is some importance to let the fetcher/user know what happened with the request.
Sometimes (like in your case) it can have an impact to the user experience. In your example, in case of failure, I think the user should get an error message or some sort of visualization that the like didn't cast, so he could try again or at least know that there was a problem.
(I'm pretty sure Facebook, Youtube, and StackOverflow just grays out upvoted or likes if something went wrong. In StackOverflow you even get a message with the specific error).
Edit
Code-wise it will work just fine since you are care to give a returned status code in any case (of success or failure).
From the documents:
The Promise returned from fetch() won’t reject on HTTP error status even if the response is an HTTP 404 or 500. Instead, it will resolve normally (with ok status set to false), and it will only reject on network failure or if anything prevented the request from completing.`
(Notice that you will want to handle network failures though).

Rx.js and application workflow

I've got a web application where I'm using the Rx.js for handling event streams. The app uses a rest api provided by a backend.
Mostly I make a subscription for an api call and when a request is done I render results and reset other controls states (hide progress elements and so on).
Some api calls can be failed when an auth token is expired and I have to make a user to login again (show a login popup or so).
I'm curious is there a way to "restore" an api call stream after a successful login? Where a user has not to provide additional actions to get a server response.
Primitive example of the current workflow:
var apiCallStream = $.ajaxAsObservable(params):
apiCallStream.subscribe(
result => renderResult(result),
err => handleError(err));
function handleError(err) {
if (err.xhr.error === 401) {
LoginPopup();
} else {
ErrorPopup(err);
}
}
Here some some (very rough pseudo-code), but you could do this with retryWhen:
// use Rx.DOM.get for an Observable Ajax GET
var source = Rx.DOM.get('/some/end/point')
.retryWhen(function(errors) {
// retryWhen: errors is a stream of errors
// whatever observable you return, when it emits, the
// observable you're operating on will be retried. (the entire thing)
return errors.filter(function(e) { return e.status === 401; })
// if status is 401, then tell the user to login
.flatMapLatest(function() { return doUserLogin; });
});
// a contrived observable that shows a form and
// return a stream of successful logins
var doUserLogin = Observable.create(function(observer) {
// show a form
var form = $('.my-form').show();
// when submit is fired on said form...
return Rx.Observable.fromEvent(form, 'submit')
// send a login over ajax
.flatMap(e => Rx.DOM.post('some/url/login', form.serialize()))
});
Hopefully that gives you something to start with.

A design pattern for async requests to handle success, failure, retry ? (javascript)

I'm writing a mobile app with Appcelerator Titanium that makes a lot of different xhr requests. This is not really an Appcelerator Titanium specific question. But if you do write some code, I hope it's javascript.
The app needs to authenticate itself, the user must be logged for some interactions, etc.
I've come to a point where any request might get any kind of response such as:
not authenticated
not logged
bad params
successful
...
The requests are wrapped in different model methods or helpers.
The thing is, I'm not familiar with this kind of app. I was wondering what are the best practices.
Some real questions for example would be:
If the app is not authenticated (token expired, first launch), should the app try to authenticate itself and then send again the request that was denied ? (transparent to user)
Should I send an authentication request each time the app launches and then "forget" about it?
The problem I'm facing is that the code becomes quickly big if I try to handle this for each request. Full of nested callbacks, retry conditions, various events listeners to manage, etc. It just does not feel very "nice". And it's not DRY at all, when what I really need is for any request, check what was wrong, try to fix it (authenticate if not, automatic login if possible or show the login UI, etc..) then if that works retry the original request a couple of times, abort if needed.
I've been looking at the promise pattern but only know theory and don't know if it could be what I need.
So I welcome any advice regarding this particular problem. I wonder how apps like "Facebook" handle this.
Thank you for your help
This question is not easily answered, but let me try to give you some Ideas:
The most important thing, before coding anything in your app, is the API itself. It has to be reliable and adhere to standards. I will not go into too much detail here, but a well written RESTful API can reduce the complexity of your httpClient significantly. It has to respond with standard http status codes and to methods like POST, GET, PUT, DELETE...
A pretty good read is The REST API Design Handbook by George Reese.
My approach to httpClients with Titanium is a single module, which is loaded via require() wherever needed. I stick to one single client at a time, as I had massive problems with multiple parallel calls. Whenever a call is made, the client checks if there is already a call in progress and sends it to a queue if necessary.
Let me show you an example. I have left out lots of stuff for sake of brevity:
// lib/customClient.js
var xhrRequest; // This will be our HTTPClient
var callQueue = []; // This will be our queue
// Register the request
// params are:
// method (e.g. 'GET')
// url (e.g. 'http://test.com/api/v1/user/1')
// done (callback function)
function registerRequest(params) {
if(!xhrRequest) {
sendRequest(params);
} else {
queueRequest(params);
}
}
// This simply sends the request
// to the callQueue
function queueRequest(params) {
callQueue.push(params);
}
// Send the request with the params from register
// Please note that I do not hardcode error messages,
// I just do it here so it is easier to read
function sendRequest(params) {
// Set callback if available and valid
var callback = params.done && typeof(params.done) === "function" ? params.callback : null;
// Set method
var method = params.method || 'GET';
// Create the HTTP Client
xhrRequest = Ti.Network.createHTTPClient({
// Success
onload: function() {
// You can check for status codes in detail here
// For brevity, I will just check if it is valid
if (this.status >= 200 && this.status < 300) {
if(this.responseText) {
// You might want to check if it can be parsed as JSON here
try {
var jsonData = JSON.parse(this.responseText);
if(callback) callback({ success: true, response: jsonData });
} catch(e) {
if(callback) callback({ success: false, errormessage: 'Could not parse JSON data' });
}
processQueue();
} else {
if(callback) callback({ success: false, errormessage: 'No valid response received' });
processQueue();
}
} else {
if(callback) callback({ success: false, errormessage: 'Call response is success but status is ' + this.status });
processQueue();
}
},
// Error
onerror: function(e) {
if(this.responseText) {
try {
var jsonData = JSON.parse(this.responseText);
if(callback) callback({ success: false, reponse: jsonData });
} catch(e) {};
}
processQueue();
},
});
// Prepare and send request
// A lot more can (and should) be configured here, check documentation!
xhrRequest.setTimeout(10000);
xhrRequest.open(method, params.url);
xhrRequest.send();
}
// Checks if there is anything else in the queue
// and sends it
function processQueue() {
xhrRequest = null;
var nextInQueue = callQueue.shift();
if(nextInQueue) sendRequest(nextInQueue);
}
// Our public API
var publicAPI = {
sendRequest: function(params) {
registerRequest(params);
}
};
module.exports = publicAPI;
I can then send a call from any other controller/view
var customClient = require('lib/customClient'); // omit 'lib' if you use alloy
// Send the request
customClient.sendRequest({
method : 'GET',
url : 'http://test.com/api/v1/user/1',
done : function(response) {
Ti.API.debug(JSON.stringify(response));
}
});
Note that this is not complete and does not check for connectivity, has no real error handling etc., but it might help you to get an idea.
I think there is loads of stuff to talk about here, but I will stop here for now...

Categories

Resources