I am using admin-on-rest (1.3.2) and trying to skip the default behaviour of calling AUTH_LOGOUT action on Promise rejection. I want to drop my behavior.
I found an issue on their Github Issues:
https://github.com/marmelab/admin-on-rest/issues/894, but without much information about the implementation here.
In the browser console, I see that executed saga is here:
https://github.com/marmelab/admin-on-rest/blob/v1.3.2/src/sideEffect/saga/crudResponse.js#L92-L97
I've just realized why what AUTH_LOGOUT happening.
My AUTH_ERROR check:
if (type === AUTH_ERROR) {
const { status } = params
if (status === 401) {
localStorage.removeItem('admin')
return Promise.reject()
}
}
Does not have check for other status codes rather than 401 and there was no Promise.resolve returned, which leads to Promise.reject('Unknown method') returned.
Related
I'm dipping my toe into the waters of Axios and async/await at the same time, and am trying to understand something about the control flow. Is the following legitimate?
let loading=true;
(async() => {
let response = null;
try {
response = await axios.get('https://whatever.com/api');
} finally {
loading=false;
}
if(response){
//do something with response here
}
})();
That is, can I count on the request to have returned at the point I am accessing the response variable? I appreciate I could guarantee it is by moving it into the 'try' immediately after the axios get, but then I would have to have the loading=false line before it, as well as in 'finally' (or 'catch'). I need to ensure that loading is set to false before any further actions, whether the request succeeds or fails, and I don't want to repeat myself. Maybe there's a better way of doing this?
Edit
Now that you have changed the question, the previous solution will not be working correctly. The issue is that the code inside the IIFE will be executed after everything else is finished, so loading will never be set to false from the perspective of the outside code. (the other code will be executed, and thеn the IIFE. That's because of the event loop). Your best bet is to make the outside code async and await the axios promise.
If you provide the problem details I might be able to help you refactor it.
Previous answer
I need to ensure that loading is set to false before any further actions
Any code after the await is guaranteed to NOT be loading:
(async() => {
let response = await axios.get('https://whatever.com/api');
// the request is finished, the await guarantees that
})();
If you need error handling, you can wrap it in a try/catch:
(async() => {
try {
let response = await axios.get('https://whatever.com/api');
// definitely not loading
}
catch (e) {
// definitely not loading, but an error occurred
}
})();
How do I get the status code back from a failed ajax call in RxJS so I can decide what to do with it?
import { ajax } from 'rxjs/observable/dom/ajax'
ajax('https://my.url')
.map(xhr => console.log('woo', xhr.response))
.catch(err => console.log('status code??'))
there are some fields on the err response and one of them is status but it's always 0 irrespective of the statusCode.
Edit:
I missed the fact that you see error.status so the question is just that why it's zero.
It's a browser thing. It's zero by default, and only gets changed when the request actually comes back. If it does not for any reason complete, it remains zero. That includes aborted requests, CORS issues, being offline, DNS issues, and any other network error. Which makes sense, cause there are no HTTP codes for most of these cases. A CORS request error might itself have a 401 (or other code) but the browser does not expose it to you programmatically.
Unfortunately, when this happens there's not much you can do programmatically to know what caused it. You can check navigator.onLine and if it's false might infer that it was caused by not being connected to the internet, though that's not 100% reliable.
In other cases, you're screwed, programmatically. There's no error message property with an explanation or other way to know. The true reason the error is typically in the dev console (so check there), but not accessible programmatically for security reasons.
Here are some additional resources about this:
https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/status
https://stackoverflow.com/a/26451773/1770633
XMLHttpRequest status 0 (responseText is empty)
In v5 (and at least in v4 too), status is available as a top-level property status of the provided error object:
import { ajax } from 'rxjs/observable/dom/ajax'
ajax('https://my.url')
.map(xhr => console.log('woo', xhr.response))
.catch(err => {
console.log('status code', error.status);
// also available as error.xhr.status (inside the raw XMLHttpRequest object)
return Observable.empty(); // switch to an empty stream i.e. swallow error
});
Note that catch is used to catch an error and then switch to a different Observable that you must return. So the error must be handled. If you do not want to handle the error, but just want to log it, you can use do:
ajax('https://my.url')
.map(xhr => console.log('woo', xhr.response))
.do({ error: err => console.log('status code', err.status) })
As per documentation: https://github.com/Reactive-Extensions/RxJS-DOM/blob/master/doc/operators/ajax.md
You do it like below:
ajax('https://my.url')
.map(xhr => console.log('woo', xhr.response))
.catch((err, status) => console.log(status))
thanks for the amazing job you've done with AOR, this have been a great source of inspiration for me, especially in how to implement a proper redux store.
My question :
If I make a bad request, (code 400), AOR log me out of the app.
How can I prevent this and instead show a notification to my user ?
You can follow the instructions to achieve it:
import { AUTH_ERROR } from 'admin-on-rest';
export default (type, params) => {
if (type === AUTH_ERROR) {
// As I noticed, this code is executed for any error, not only for auth-one.
// So process the error as you wish (show message), don't log out.
}
// Then resolve the promise.
return Promise.resolve();
};
UPDATE2: I revisited this issue and have solved the problem by carefully following the doco linked below. But first, for those who are struggling with this, you are in good company. There are so many versions of the doco from Google it is confusing! Do you include platform.js or client.js in your html? Do you load gapi.auth or gapi.auth2? Do you use gapi.auth2.render or gapi.auth.authorize, or gapi.auth2.init, and so on.
The way that returns an access_token (as of this article date) is linked below. I managed to get this working by carefully following the guide and reference using platform.js. Other libraries are then dynamically loaded such as client.js using gapi.load('drive', callback).
https://developers.google.com/identity/sign-in/web/listeners
https://developers.google.com/identity/sign-in/web/reference
==== ORIGINAL ISSUE FOR PROSPERITY ====
UPDATE 1:
I've updated the code sample to do a recursive search of the googleUser object. At least this shouldn't break in a subsequent library.
Below is a code snippet to handle an issue where the access_token in the Google gapi.auth2.AuthResponse object is not at the top level... it is hidden :( in the depths of the object!
So it is retrievable, but not at the top level!!?? I've noticed it seems to be a timing issue... once the application is running for a while on subsequent checks, it does contain the access token at the top level!!
var authResponse = _.googleUser.getAuthResponse();
_.id_token = authResponse.id_token; // Always exists
// access_token should also be a param of authResponse
if (authResponse.access_token) {
debug("Worked this time?");
_.access_token = authResponse.access_token;
} else {
// !!! Internal object access !!!
debug("Attempt to get access token from base object.");
_.access_token = _.objRecursiveSearch("access_token", _.googleUser);
if (_.access_token) {
debug("Access token wasn't on authResponse but was on the base object, WTF?");
} else {
debug("Unable to retrieve access token.");
return false;
}
}
_.objRecursiveSearch = function(_for, _in) {
var r;
for (var p in _in) {
if (p === _for) {
return _in[p];
}
if (typeof _in[p] === 'object') {
if ((r = _.objRecursiveSearch(_for, _in[p])) !== null) {
return r;
}
}
}
return null;
}
I'm guessing getAuthResponse somehow provides a callback once it is ready, but I can't see where in the API.
https://developers.google.com/identity/sign-in/web/reference
I know this question is fairly old, but it appears first when googling for ".getAuthResponse() doesn't have access_token," which is how I got here.
So for those of you in 2016 (and maybe later) here's what I have found out
There's a secret argument on .getAuthResponse, not documented anywhere I have found. If you would run the following in your app
console.log(gapi.auth2.getAuthInstance().currentUser.get().getAuthResponse);
You would see that you get the following (copy/pasted from my console)
function (a){if(a)return this.hg;a=.HE;var c=.rf(this.hg);!a.Ph||a.dL||a.Lg||(delete c.access_token,delete c.scope);return c}
This shows that the .getAuthResponse() function looks for an argument, and as far as I can tell doesn't even check its value -- it simply checks if it is there and then returns the whole object. Without that function, the rest of the code runs and we can see very clearly it is deleting two keys: access_token and scope.
Now, if we call this function with and without the argument, we can check the difference in the output. (note: I used JSON.stringify because trying to copy/paste the object from my browser console was causing me some issues).
console.log(JSON.stringify(gapi.auth2.getAuthInstance().currentUser.get().getAuthResponse()));
console.log(JSON.stringify(gapi.auth2.getAuthInstance().currentUser.get().getAuthResponse(true)));
getAuthResponse() object
{
"token_type":"Bearer",
"login_hint":"<Huge mess of letters>",
"expires_in":2112,
"id_token":"<insert your ridiculously long string here>",...}
getAuthResponse(true) object
{
"token_type":"Bearer",
"access_token":"<an actual access token goes here>",
"scope":"<whatever scopes you have authorized>",
"login_hint":"<another mess of letters>",
"expires_in":2112,
"id_token":"<Insert your ridiculously long string here>",
...}
Figured out the fix for this. Turns out that if we don't provide the login scope config in gapi.auth2.init it doesn't return access_token in getAuthResponse. Please call gapi.auth2.init as given below and access_token will be present.
gapi.auth2.init({
client_id: <googleClientID>,
'scope': 'https://www.googleapis.com/auth/plus.login'
})
I'm not sure if I'm understanding a certain aspect of promises correctly and I couldn't find what I was looking for after a brief google/SO search.
Can a resolved promise returned to a rejected callback ever fire a resolved method later in the chain? Using jQuery deferreds (and it's deprecated pipe method) as an example:
x = $.Deferred();
x
.then(null,function() {
return $.Deferred().resolve();
})
.then(function() {
console.log('resolved');
},function() {
console.log('rejected');
});
The above code, logs rejected, I would've expected it to log resolved because the Deferred in the first error callback returns a resolved promise.
On the contrary, the same code using jQuery's deprecated pipe method logs, as I would've expected resolved.
x = $.Deferred();
x
.pipe(null,function() {
return $.Deferred().resolve();
})
.pipe(function() {
console.log('resolved');
},function() {
console.log('rejected');
});
Am I thinking about something wrong by trying to resolve a promise inside a rejected callback?
For anybody who has run into this same thought process, the following page helped me out: https://gist.github.com/domenic/3889970
Specifically:
...you can feed the return value of one function straight into another,
and keep doing this indefinitely. More importantly, if at any point
that process fails, one function in the composition chain can throw an
exception, which then bypasses all further compositional layers until
it comes into the hands of someone who can handle it with a catch.
It seems jQuery's pipe method was a violation of the Promise's specification.