Thursday, May 24, 2012

How to manage a redirect request after a jQuery Ajax call


Im using $.post() to call a Servlet using Ajax and then use the resulting HTML fragment to replace a div element in the User's current page. However, if the session timeouts the server sends a redirect directive to send the user to the login page. Nonetheless, JQuery is replacing the div element with the contents of the login page, forcing the user's eyes to witness a rare scene indeed.



How can I manage a redirect directive from an Ajax call?



  • jQuery 1.2.6


Source: Tips4all

19 comments:

  1. I read this question and implemented the approach that has been stated regarding setting the response status code to 278 in order to avoid the browser transparently handling the redirects. Even though this worked, I was a little dissatisfied as it is a bit of a hack.

    After more digging around, I ditched this approach and used JSON. In this case, all responses to ajax requests have the status code 200 and the body of the response contains a JSON object that is constructed on the server. The javascript on the client can then use the JSON object to decide what it needs to do.

    I had a similar problem to yours. I perform an ajax request that has 2 possible responses: one that redirects the browser to a new page and one that replaces an existing HTML form on the current page with a new one. The jquery code to do this looks something like:

    $.ajax({
    type: "POST",
    url: reqUrl,
    data: reqBody,
    dataType: "json",
    success: function(data, textStatus) {
    if (data.redirect) {
    // data.redirect contains the string URL to redirect to
    window.location.href = data.redirect;
    }
    else {
    // data.form contains the HTML for the replacement form
    $("#myform").replaceWith(data.form);
    }
    }
    });


    The JSON object "data" is constructed on the server to have 2 members: data.redirect and data.form. I found this approach to be much better.

    ReplyDelete
  2. I solved this issue by:


    Adding a custom header to the Response

    public ActionResult Index(){
    if (!HttpContext.User.Identity.IsAuthenticated)
    {
    HttpContext.Response.AddHeader("REQUIRES_AUTH","1");
    }
    return View()
    }

    Bind a Javascript function to the ajaxSuccess event and check to see if the header exists

    $('body').bind('ajaxSuccess',function(event,request,settings){
    if (request.getResponseHeader('REQUIRES_AUTH') === '1'){
    window.location = '/';
    };
    });

    ReplyDelete
  3. No browsers handles 301 and 302 responses correctly. And in fact the standard even says they should handle them "transparently" which is a MASSIVE headache for Ajax Library vendors. In Ra-Ajax we were forced into using HTTP response status code 278 (just some "unused" success code) to handle transparently redirects from the server...

    This really annoys me, and if someone here have some "pull" in W3C I would appreciate that you could let W3C know that we really need to handle 301 and 302 codes ourselves...! ;)

    ReplyDelete
  4. The solution that was eventually implemented was to use a wrapper for the callback function of the Ajax call and in this wrapper check for the existence of a specific element on the returned HTML chunk. If the element was found then the wrapper executed a redirection. If not, the wrapper forwarded the call to the actual callback function.

    For example, our wrapper function was something like:


    function cbWrapper(data, funct){
    if($("#myForm", data).size() > 0)
    top.location.href="login.htm";//redirection
    else
    funct(data);
    }


    Then, when making the Ajax call we used something like:


    $.post("myAjaxHandler",
    {
    param1: foo,
    param2: bar
    },
    function(data){
    cbWrapper(data, myActualCB);
    },
    "html");


    This worked for us because all Ajax calls always returned HTML inside a DIV element that we use to replace a piece of the page. Also, we only needed to redirect to the login page.

    ReplyDelete
  5. I like Timmerz's method with a slight twist of lemon. If you ever get returned contentType of text/html when you're expecting JSON, you are most likely being redirected. In my case, I just simply reload the page, and it gets redirected to the login page. Oh, and check that the jqXHR status is 200, which seems silly, because you are in the error function, right? Otherwise, legitimate error cases will force an iterative reload (oops)

    $.ajax(
    error: function (jqXHR, timeout, message) {
    var contentType = jqXHR.getResponseHeader("Content-Type");
    if (jqXHR.status === 200 && contentType.toLowerCase().indexOf("text/html") >= 0) {
    // assume that our login has expired - reload our current page
    window.location.reload();
    }

    });

    ReplyDelete
  6. Use the low-level $.ajax() call:

    $.ajax({
    url: "/yourservlet",
    data: { },
    complete: function(xmlHttp) {
    // xmlHttp is a XMLHttpRquest object
    alert(xmlHttp.status);
    }
    });


    Try this for a redirect:

    if (xmlHttp.code != 200) {
    top.location.href = '/some/other/page';
    }

    ReplyDelete
  7. I have a simple solution that works for me, no server code change needed...just add a tsp of nutmeg...

    $(document).ready(function ()
    {
    $(document).ajaxSend(
    function(event,request,settings)
    {
    var intercepted_success = settings.success;
    settings.success = function( a, b, c )
    {
    if( request.responseText.indexOf( "<html>" ) > -1 )
    window.location = window.location;
    else
    intercepted_success( a, b, c );
    };
    });
    });


    I check the presence of html tag, but you can change the indexOf to search for whatever unique string exists in your login page...

    ReplyDelete
  8. Try

    $(document).ready(function () {
    if ($("#site").length > 0) {
    window.location = "<%= Url.Content("~") %>" + "Login/LogOn";
    }
    });


    Put it on the login page. If it was loaded in a div on the main page, it will redirect til the login page. "#site" is a id of a div which is located on all pages except login page.

    ReplyDelete
  9. in the servlet you should put
    response.setStatus(response.SC_MOVED_PERMANENTLY);
    to send the '301' xmlHttp status you need for a redirection...

    and in the $.ajax function you should not use the .toString() function..., just

    if (xmlHttp.status == 301) {
    top.location.href = 'xxxx.jsp';
    }

    the problem is it is not very flexible, you can't decide where you want to redirect..

    redirecting through the servlets should be the best way. but i still can not find the right way to do it.

    ReplyDelete
  10. Putting together what Vladimir Prudnikov and Thomas Hansen said:


    Change your server-side code to detect if it's an XHR. If it is, set the response code of the redirect to 278.
    In django:



    if request.is_ajax():
    response.status_code = 278



    This makes the browser treat the response as a success, and hand it to your Javascript.


    In your JS, make sure the form submission is via Ajax, check the response code and redirect if needed:



    $('#my-form').submit(function(event){

    event.preventDefault();
    var options = {
    url: $(this).attr('action'),
    type: 'POST',
    complete: function(response, textStatus) {
    if (response.status == 278) {
    window.location = response.getResponseHeader('Location')
    }
    else { ... your code here ... }
    },
    data: $(this).serialize(),
    };
    $.ajax(options);
    });

    ReplyDelete
  11. I just wanted to latch on to any ajax requests for the entire page. @SuperG got me started. Here is what I ended up with:

    // redirect ajax requests that are redirected, not found (404), or forbidden (403.)
    $('body').bind('ajaxComplete', function(event,request,settings){
    switch(request.status) {
    case 301: case 404: case 403:
    window.location.replace("http://mysite.tld/login");
    break;
    }
    });


    I wanted to specifically check for certain http status codes to base my decision on. However, you can just bind to ajaxError to get anything other than success (200 only perhaps?) I could have just written:

    $('body').bind('ajaxError', function(event,request,settings){
    window.location.replace("http://mysite.tld/login");
    }

    ReplyDelete
  12. I resolved this issue like this:

    Add a middleware to process response, if it is a redirect for an ajax request, change the response to a normal response with the redirect url.

    class AjaxRedirect(object):
    def process_response(self, request, response):
    if request.is_ajax():
    if type(response) == HttpResponseRedirect:
    r = HttpResponse(json.dumps({'redirect': response['Location']}))
    return r
    return response


    Then in ajaxComplete, if the response contains redirect, it must be a redirect, so change the browser's location.

    $('body').ajaxComplete(function (e, xhr, settings) {
    if (xhr.status == 200) {
    var redirect = null;
    try {
    redirect = $.parseJSON(xhr.responseText).redirect;
    if (redirect) {
    window.location.href = redirect.replace(/\?.*$/, "?next=" + window.location.pathname);
    }
    } catch (e) {
    return;
    }

    }

    ReplyDelete
  13. Additionally you will probably want to redirect user to the given in headers URL. So finally it will looks like this:

    $.ajax({
    //.... other definition
    complete:function(xmlHttp){
    if(xmlHttp.status.toString()[0]=='3'){
    top.location.href = xmlHttp.getResponseHeader('Location');
    }
    });


    UPD: Opps. Have the same task, but it not works. Doing this stuff. I'll show you solution when I'll find it.

    ReplyDelete
  14. Based on my brief testing of Firefox, Safari, Opera, IE6/7, it seems the XMLHttpRequest.status does not return the same values and its not compatible across different browsers. I haven't found a more elegant solution.

    ReplyDelete
  15. I was having this problem on a django app I'm tinkering with (disclaimer: I'm tinkering to learn, and am in no way an expert). What I wanted to do was use jQuery ajax to send a DELETE request to a resource, delete it on the server side, then send a redirect back to (basically) the homepage. When I sent HttpResponseRedirect('/the-redirect/') from the python script, jQuery's ajax method was receiving 200 instead of 302. So, what I did was to send a response of 300 with:

    response = HttpResponse(status='300')
    response['Location'] = '/the-redirect/'
    return response


    Then I sent/handled the request on the client with jQuery.ajax like so:

    <button onclick="*the-jquery*">Delete</button>

    where *the-jquery* =
    $.ajax({
    type: 'DELETE',
    url: '/resource-url/',
    complete: function(jqxhr){
    window.location = jqxhr.getResponseHeader('Location');
    }
    });


    Maybe using 300 isn't "right", but at least it worked just like I wanted it to.

    PS :this was a huge pain to edit on the mobile version of SO. Stupid ISP put my service cancellation request through right when I was done with my answer!

    ReplyDelete
  16. <script>
    function showValues() {
    var str = $("form").serialize();
    $.post('loginUser.html',
    str,
    function(responseText, responseStatus, responseXML){
    if(responseStatus=="success"){
    window.location= "adminIndex.html";
    }
    });
    }
    </script>

    ReplyDelete
  17. I solved this by putting the following in my login.php page.

    <script type="text/javascript">
    if (top.location.href.indexOf('login.php') == -1) {
    top.location.href = '/login.php';
    }
    </script>

    ReplyDelete
  18. You can also hook XMLHttpRequest send prototype. This will work for all sends (jQuery/dojo/etc) with one handler.

    I wrote this code to handle a 500 page expired error, but it should work just as well to trap a 200 redirect. Ready the wikipedia entry on XMLHttpRequest onreadystatechange about the meaning of readyState.

    // Hook XMLHttpRequest
    var oldXMLHttpRequestSend = XMLHttpRequest.prototype.send;

    XMLHttpRequest.prototype.send = function() {
    //console.dir( this );

    this.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 500 && this.responseText.indexOf("Expired") != -1) {
    try {
    document.documentElement.innerHTML = this.responseText;
    } catch(error) {
    // IE makes document.documentElement read only
    document.body.innerHTML = this.responseText;
    }
    }
    };

    oldXMLHttpRequestSend.apply(this, arguments);
    }

    ReplyDelete
  19. I just wanted to share my approach as this might it might help someone:

    I basically included a JavaScript module which handles the authentication stuff like displaying the username and also this case handling the redirect to the login page.

    My scenario: We basically have an ISA server in between which listens to all requests and responds with a 302 and a location header to our login page.

    In my JavaScript module my initial approach was something like

    $(document).ajaxComplete(function(e, xhr, settings){
    if(xhr.status === 302){
    //check for location header and redirect...
    }
    });


    The problem (as many here already mentioned) is that the browser handles the redirect by itself wherefore my ajaxComplete callback got never called, but instead I got the response of the already redirected Login page which obviously was a status 200. The problem: how do you detect whether the successful 200 response is your actual login page or just some other arbitrary page??

    The solution

    Since I was not able to capture 302 redirect responses, I added a LoginPage header on my login page which contained the url of the login page itself. In the module I now listen for the header and do a redirect:

    if(xhr.status === 200){
    var loginPageRedirectHeader = xhr.getResponseHeader("LoginPage");
    if(loginPageRedirectHeader && loginPageRedirectHeader !== ""){
    window.location.replace(loginPageRedirectHeader);
    }
    }


    ...and that works like charm :). You might wonder why I include the url in the LoginPage header...well basically because I found no way of determining the url of GET resulting from the automatic location redirect from the xhr object...

    ReplyDelete