41

I have a reverse proxy that checks global authentication for several applications. When the user is disconnected but still trying to use my application, the proxy sends a 302 response :

HTTP/1.1 302 Found
Date: Wed, 11 Sep 2013 09:05:34 GMT
Cache-Control: no-store
Location: https://other.url.com/globalLoginPage.html
Content-Length: 561
Content-Type: text/html; charset=iso-8859-1
Via: 1.1 my-proxy.com
Connection: Keep-Alive

In angularJs, the error callback is called but the response headers are empty, status is 0 and data is an empty string. So it seems that I really can't do nothing to handle the response...
I've seen several questions on the subject, and I still don't understand what is going on (CORS because of the proxy or different domain in the location?, 302 browser behavior?).
In particular there is this part from an answer (https://stackoverflow.com/a/17903447/1093925):

Note: If your server sets a response code of 301 or 302, you will not be able to get the Location header, as it will be automatically and transparently followed by the XMLHttpRequest object.

What about this XMLHttpRequest object?
In a very old version of Chrome (can't use a newer version) I can see that a corresponding request in the network panel, but it seems to fail as there is no response.
In the latest version of firefox, there is nothing going on.

Can I do anything about that, since I can't change the proxy configuration and response?

Update:
I replayed my scenario today, and thanks to a newer version of firebug, I was able to get more details about what is going on.
I was not far from the anwser in my question : Cross domain policy.
Since it is an HTTP request made by my application, the browser denies the following XMLHttpRequest (which in-app looks like the same request). Hence the error and the empty response.
So I think there is nothing special I can do about it

Community
  • 1
  • 1
D0m3
  • 1,222
  • 2
  • 11
  • 16
  • Does anyone know how to get the `Location` header in AngularJS from a 302? It's exposed in the browser, but when inspecting the error response (config, headers, data, etc.), it doesn't seem to be present in Angular app....I need to redirect my user to whatever URL is in the Location header – jlewkovich Jul 07 '16 at 16:48

5 Answers5

47

I had the same problem in my app. You can't really "catch" a 302 redirect response. The browser catches it before Angular get it's hand on it. so actually, when you do receive your response - it is already too late.

The bad news: it's not a problem in the angular platform. The xmlHttpRequest do not support this kind of behaviour, and the browser act before you can do anything about it. reference: Prevent redirection of Xmlhttprequest

The good news: there are many ways to bypass this problem, by intercepting the response- and find some way to recognize that it's your lovable 302. This is a hack, but it's the best you can do at the moment.

So. For example, in my app, the redirect was back to the login.html page of the app, and in the app i got the response with a 200 status and the data of the response was the content of my login.html page. so in my interceptor, i checked if the result is a string (usually not! so- no efficiency prob..) and if so- checked if it's my login.html page. that way, i could catch the redirect and handle it my way.

yourApp.factory('redirectInterceptor', ['$location', '$q', function($location, $q) {
    return function(promise) {
        promise.then(
            function(response) {
                if (typeof response.data === 'string') {
                    if (response.data.indexOf instanceof Function &&
                        response.data.indexOf('<html id="ng-app" ng-app="loginApp">') != -1) {
                        $location.path("/logout");
                        window.location = url + "logout"; // just in case
                    }
                }
                return response;
            },
            function(response) {
                return $q.reject(response);
            }
        );
        return promise;
    };
}]);

Then insert this interceptor to your app. something like this:

$httpProvider.responseInterceptors.push('redirectInterceptor');

good luck.

Community
  • 1
  • 1
Ofer Segev
  • 4,375
  • 2
  • 18
  • 19
  • I quite like this approach, I used it to grab the title of login page, response.data.indexOf('title to grab') != -1. But it didn't work! any ideas why? – ssayyed Jul 17 '14 at 15:45
  • BTW - i don't like this approach (my answer's approach- that is), but in this case, i think we don't have much choice.. anyway- i have no idea why it's not working for you. It might be that the data you are working on do not contain your html content. try to stop inside this code with a debugger; and console.log() the response.data to investigate why is that happening to you. – Ofer Segev Jul 20 '14 at 12:02
  • Thx for your answer, actually my response.data is empty. I think I have just understood the issue thanks to a newer version of firebug. See my response. – D0m3 Sep 11 '14 at 14:58
  • good answer, really helps me to figure out 302 behaviors in browsers. – nightire Jul 10 '15 at 11:37
  • 2
    @OferSegev, your solution is only correct in case the redirect is to url with in the same domain. – john Smith Oct 11 '15 at 18:39
  • Yes, @johnSmith I am looking out for a solution to catch redirect responses from cross domain. – Rahul Patil Dec 21 '15 at 14:55
  • Good workaround, but it doesn't work in cases when integrating third-party libraries. For example, oidc-client which expects many redirects but instead receives `200` and a cached response from service worker. – JustAMartin May 05 '20 at 14:52
3

Your 302 -Redirect is being handled directly by the browser and there is nothing you can do about it directly. You can, however, use an httpInterceptor to help you along. You'll need to include $httpProvider in your app DI list, and then somewhere in your config function put a reference to it like this:

$httpProvider.responseInterceptors.push('HttpInterceptor');

A sample interceptor looks like this:

window.angular.module('HttpInterceptor', [])
.factory('HttpInterceptor', ['$q', '$injector',
    function($q, $injector) {
        'use strict';

        return function(promise) {
            return promise.then(success, error);
        };

        function success(response) {
            return response;
        }

        function error(response) {
            var isAuthRequest = (response.config.url.indexOf('/v1/rest/auth') !== -1);

            //if we are on the authenticating don't open the redirect dialog
            if (isAuthRequest) {
                return $q.reject(response);
            }

            //open dialog and return rejected promise
            openErrorDialog(response);
            return $q.reject(response);
        }

        function openErrorDialog(response) {
            $injector.get('$dialog').dialog({
                backdropFade: true,
                dialogFade: true,
                dialogClass: 'modal newCustomerModal',
                resolve: {
                    errorData: function() {
                        return response.data;
                    },
                    errorStatus: function() {
                        return response.status;
                    }
                }
            })
            .open('/views/error-dialog-partial.htm',
                'errorDialogController')
            .then(function(response) {
                if (response) {
                    window.location = '/';
                }
            });
        }
    }
]);
Chic
  • 7,241
  • 4
  • 28
  • 55
MBielski
  • 6,548
  • 3
  • 29
  • 39
  • 2
    Thanks for your answer, but I don't see how it helps my problem. In the interceptor, you still have no info about the response. – D0m3 Jan 06 '14 at 15:00
  • Hmm... you have a point there. Have you tried handling the route change via $route? (http://docs.angularjs.org/api/ngRoute.$route) – MBielski Jan 06 '14 at 16:12
  • Actually, I make a REST request, so I believe there is no relationship with $route. – D0m3 Jan 07 '14 at 14:07
  • Yes, but isn't your route changing because of the 302? You can detect that change and react to it in either $routeChangeStart or $routeChangeSuccess. – MBielski Jan 07 '14 at 15:37
  • 2
    No, the user isn't redirected. I make a HTTP request (with $http) in angularjs and I get the response as an error. Nothing else happens. – D0m3 Jan 08 '14 at 13:46
  • Ok, that's weird. 300-series status codes are not errors and the browser nor Angular should react to them that way, yet apparently that is what is happening. For errors I'm still thinking that the interceptor is your way to go. Have you tried it? – MBielski Jan 08 '14 at 23:19
  • Ok, I have to say that I am at a loss for what to do next. Sorry I couldn't help. – MBielski Jan 09 '14 at 15:47
  • Thanks for trying anyway. I will make some research to see if it is normal that angularjs considers the 302 response as an error. – D0m3 Jan 09 '14 at 15:51
  • I can tell you that my app does not react that way, fwiw. – MBielski Jan 09 '14 at 21:12
  • I have just understood why I got an error. See my anwser. – D0m3 Sep 11 '14 at 15:07
2

As discussed above, 302 responses are not available to Angular as xmlHttpRequest does not support returning redirects; the browser acts before you can do anything about it.

In addition to processing custom data responses and overriding status codes,
you could use a more generic solution and add a custom redirect header, such as X-Redirect and act upon that. The value of X-Redirect should be the url you want to redirect to eg https://other.url.com/globalLoginPage.html

$http.get(orig_url).then(function(response) {
        // do stuff
    }, function (response) {
        var headers = response.headers();
        if ('x-redirect' in headers)
        {
            document.location.href = headers['x-redirect'];
        }
        return response;
    }
}

For a more global solution, you could also use a HTTP Interceptor

app.factory('myHttpInterceptor', function ($q, myCache) {
    return {
        request: function (config) {
            return config;
        },
        response: function(response){
            var headers = response.headers();
            if ('x-redirect' in headers)
            {
                document.location.href = headers['x-redirect'];
            }    
            return response;
        },
        responseError: function(rejection) {    
            switch(rejection.status)
            {
                case 302:
                    // this will not occur, use the custom header X-Redirect instead
                    break;
                case 403:
                    alert.error(rejection.data, 'Forbidden');
                    break;
            }
            return $q.reject(rejection);
        }
    };
});

You can set the custom header server side.
For example, if you are using PHP Symfony:

$response = new Response('', 204);
$response->headers->set('X-Redirect', $this->generateUrl('other_url'));
return $response;
1

I had a very similar issue, and considered the solution provided by Ofer Segev, but checking the content of the response to see if it matched an html fragment of another page just seemed too hacky to me. What happens when someone changes that page?

Fortunately, I had control of the backend as well, so instead of returning a 302 (Redirect), I returned a 403 (Forbidden), and passed the desired location in the headers. Unlike the 302, the 403 will be handled in your error handler, where you can decide what to do next. Here was my resulting handler:

function ($scope, $http) {
    $http.get(_localAPIURL).then(function (response) {
            // do what I'd normally do
        }, function (response) {
        if (response.status == 403)
        {
            window.location.href = response.headers().location;
        }
        else
        {
            // handle the error
        }
plunkg
  • 41
  • 5
  • 1
    A much neater solution if you have access to the backend – ded May 16 '17 at 07:52
  • I had the same thought, but I only want to return 401 if it's a web api call. This [SO question/answer](https://stackoverflow.com/a/47240601/4271117) addressed the issue. – Weihui Guo May 31 '18 at 14:41
1

Just for reference in case anyone still looks at this: you can also make the call that is expected to return a 302 form an Iframe. That way you stay on the page, and you can communicate with https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage