Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Need some advice about handling 302 redirects from Ajax #932

Closed
freeman-g opened this issue Jun 1, 2017 · 13 comments
Closed

Need some advice about handling 302 redirects from Ajax #932

freeman-g opened this issue Jun 1, 2017 · 13 comments

Comments

@freeman-g
Copy link

freeman-g commented Jun 1, 2017

Summary

We have a single page app based on Vue.js which uses Axios to make API calls. When a user's session expires, API calls start returning a 302 redirect to the login page. Since its a single page app, sometimes users may just browse around directly in the app, therefore only making AJAX calls and not reloading the main index.html file.

When this happens, the 302's are basically swallowed and the user is not redirected and the app breaks.

To complicate matters a bit further, the login page is based on a federated login service in Azure, so it's on a different domain. This causes the browser to throw the following CORS error:

XMLHttpRequest cannot load login.microsoftonline.com. 
No 'Access-Control-Allow-Origin' header is present on the requested resource.  
Origin 'null' is therefore not allowed access.

I tried to use an interceptor but the error.response object is undefined, so I cannot even tell what the HTTP status of the response was. So the best I can come up with is that when error.response is undefined to assume it was a 302 and assume I need to forward the user to the login page:

axios.interceptors.response.use((response) => {
  return response
}, (error) => {
  // we can't seem to catch the 302 status code as an error,
  // however, since it redirects to another domain (login.microsoftonline.com) it causes
  // a CORS error which makes error.response be undefined here.  This assumes that any time
  // error.response is undefined that we need to redirect to the login page
  if (typeof error.response === 'undefined') {
    window.location = 'https://login.microsoftonline.com'
  } else {
    return Promise.reject(error)
  }
})

Is this the best we can do? Any further suggestions?

Context

  • Vue.js, vue-router single page app
  • axios version: v0.16.0
  • Environment: chrome 58, windows 10
@gugurete
Copy link

gugurete commented Jun 7, 2017

Yes, I have the same issue. It is critical enough to consider alternatives to axios

@jcready
Copy link
Contributor

jcready commented Jun 9, 2017

Browsers always follow redirects for XHRs or fetch() requests. There is no library that could prevent the redirect. What you need to do on your server-side is distinguish between XHR requests and normal browser navigation requests and send either a 403 w/ JSON and specify the URL you want to redirect to in there or send a 302 if the request is being made by a browser navigation request. You can do this a couple different ways, but I'll list some here assuming you're using Express:

Check the req.xhr to respond based on its boolean value

// The user needs to login again
if (req.xhr) {
  res.status(403).json({
    error: 'You must login to see this',
    location: 'https://login.microsoftonline.com'
  })
} else {
  res.redirect('https://login.microsoftonline.com')
}

Use res.format() to respond based on what the client accepts

By default axios sends an Accept header of application/json, text/plain, */* where as browser generally send the Accept header with text/html being listed first.

// The user needs to login again
res.format({
  json: () => res.status(403).json({
    error: 'You must login to see this',
    location: 'https://login.microsoftonline.com'
  }),
  html: () => res.redirect('https://login.microsoftonline.com')
  default: () => res.redirect('https://login.microsoftonline.com')
})

Then on your client-side you would use something like this when using either of the above solutions:

axios.interceptors.response.use((response) => {
  return response
}, (error) => {
  if (error.response && error.response.data && error.response.data.location) {
    window.location = error.response.data.location
  } else {
    return Promise.reject(error)
  }
})

@freeman-g
Copy link
Author

@jcready - thanks for the information.

In our case, requests flow from Client > Azure Authentication Service > Our app. In the case of an expired session, the response back to the client comes from the Azure Auth service and I'm not controlling that server side config. I guess this is a tradeoff of using a PaaS solution. I think your suggestion would work if our server side app had the Auth layer in it, so we may have to move away from the PaaS solution and implement Auth directly in our app.

@johnnyodonnell
Copy link

johnnyodonnell commented May 14, 2018

Browsers always follow redirects for XHRs or fetch() requests.

Does this mean the browser will always load the html into memory, but won't always go to that link and display the html? I'm confused because when I get a 302 response from an XHR request, the browser does not go to the redirected link and display the html.

@nmaves
Copy link

nmaves commented Jun 13, 2018

Sorry to bring up an old issue but @jcready are you positive that axios can't control the redirects?

How are other libraries like https://github.com/request/request able to do this?

@jcready
Copy link
Contributor

jcready commented Jun 13, 2018

@nmaves the request library can only handle redirects when running inside node, not the browser. If you use something like browserify to use the request library inside the browser it will no longer be able to handle/respond to redirects.

@nmaves
Copy link

nmaves commented Jun 13, 2018

Makes sense thank you !

@parties
Copy link

parties commented Jul 25, 2019

I know this is an old issue, but I wanted to point out that it is possible to handle redirects in the browser when using fetch, you just need to use the redirect: "manual" flag.

I'm currently using this in an app to handle redirection when trying to access protected endpoints without an active authenticated session.

This example isn't perfect and may not cover every case, but this is what we're doing and it's currently working for us:

fetch(url, {
    redirect: "manual"
}).then((res) => {
    if (res.type === "opaqueredirect") {
        // redirect to login page
        window.location.href = response.url;
    } else {
        // handle normally / pass on to next handler
    }
}).catch(...);

You can read a bit more on MDN: WindowOrWorkerGlobalScope.fetch().


It would be nice if Axios also covered this functionality.

@ntomka
Copy link

ntomka commented Aug 1, 2019

@parties The problem with opaqueredirect is:

The Response's status is 0, headers are empty, body is null and trailer is empty.

see: https://developer.mozilla.org/en-US/docs/Web/API/Response/type

So you have nothing if you want to find out what happened on the server side and where it wants to redirect you.

@parties
Copy link

parties commented Aug 1, 2019

Yes, except that the response still has response.url which is the url that the server is attempting to redirect you to, so you can at least handle navigating to the redirected url instead of forwarding the fetch request to it.

@se1by
Copy link

se1by commented Aug 28, 2019

@parties the response.url will still be the url you originally requested. See https://fetch.spec.whatwg.org/#atomic-http-redirect-handling for details.

@gxjcoo
Copy link

gxjcoo commented Sep 5, 2019

非常感谢,typeof error.response === 'undefined'很好用。神来之笔!!!

@parties
Copy link

parties commented Sep 20, 2019

@se1by You're right, thanks for the call-out. The reason I didn't catch this in my own code was because we are still able to detect and redirect (which is all we need in this application).

This is all it's doing:

fetch("/api/user", {
    redirect: "manual"
}).then((res) => {
    if (res.type === "opaqueredirect") {
        window.location.href = res.url;
    } else {
        return res;
    }
}).catch(handleFetchError);

In our application any protected endpoint can send a redirect back to the client to go to the login page, and all the client needs to do is then navigate to the protected endpoint (this is what I was misunderstanding).

I had been assuming that when res.type === "opaqueredirect" that res.url equated to "the login page URL" -- I see now that it's only echoing back the URL that was requested. However, this still works because the client simply navigates to the protected endpoint and redirected as expected.


Which brings us back to the origin of this thread: being able to handle 302 redirects coming from Axios requests.

I feel that if the fetch spec allows for this redirect: "manual" option and behavior then there should be a way to customize this behavior within Axios, at least to some degree.

Thoughts?

(And thank you all for the activity and responses on the old thread, friends 🙏)

facebook-github-bot pushed a commit to magma/magma that referenced this issue Nov 12, 2019
Summary:
If an axios request is sent while user is logged out/not authenticated, res.redirect() doesn't work. Plus, it would redirect to the wrong url i.e. instead of the url of the page that sent the axios request, it would load the url of the request itself.

followed solution from axios/axios#932 (comment) to distinguish between axios request and regular browser navigation requests. On the client side, we force reload the page since that will give us the correct login url.

question: where is a better location to put `axios.interceptors`??  It sets interceptor for axios globally so it only needs to be run once. I tried putting it in `axiosConfig.js` but that didn't work.

Reviewed By: rckclmbr

Differential Revision: D18364572

fbshipit-source-id: e5a8e75da7d7fe0e7b7cc036e7286a34661e4b73
gjalves pushed a commit to gjalves/magma that referenced this issue Nov 19, 2019
Summary:
If an axios request is sent while user is logged out/not authenticated, res.redirect() doesn't work. Plus, it would redirect to the wrong url i.e. instead of the url of the page that sent the axios request, it would load the url of the request itself.

followed solution from axios/axios#932 (comment) to distinguish between axios request and regular browser navigation requests. On the client side, we force reload the page since that will give us the correct login url.

question: where is a better location to put `axios.interceptors`??  It sets interceptor for axios globally so it only needs to be run once. I tried putting it in `axiosConfig.js` but that didn't work.

Reviewed By: rckclmbr

Differential Revision: D18364572

fbshipit-source-id: e5a8e75da7d7fe0e7b7cc036e7286a34661e4b73
@axios axios locked and limited conversation to collaborators May 22, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants