Search code examples
jqueryajaxbackbone.jscors

Backbone model CORS fetch not being sent


I'm trying to use a model.fetch() from another domain, but I seem to have stumbled upon a wall. It seems that I can do a cross-domain request OR set custom headers (it doesn't matter WHICH one), not both. Unfortunately, I need both, since the API I'm trying to fetch from uses Basic Auth.

Here's things in a nutshell:

var Model = Backbone.Model.extend({
    url: function() {
        return a_cross_domain_url;
    },
    initialize: function() {
        this.fetch();
    }
});

Backbone.sync = _.wrap(Backbone.sync, function(sync, method, model, options) {
    if (!options.xhrFields) {
        options.xhrFields = {withCredentials:true};
    }

    options.headers = options.headers || {};

    // credentials is a string that looks like 'Basic d2Vwb3c6McriNzk3YZgvZTNkMTkzOGE4MTk3NjMwMDkzNmMwZGI='
    options.headers['Authorization'] = credentials;

    sync(method, model, options);
});

var m = new Model();

I'm abstracting a lot of code, but I think this is the relevant info. What matters is that the end request looks like this:

Firebug's log of end CORS request

As far as I understand, it's all I need for this to work. It's funny, because the $.ajax.beforeSend function does get fired, but there's no log of the actual GET ever firing at all (server receives nothing). It seems to be cancelled before it's even called. The error callback does fire, and prints:

SyntaxError: JSON.parse: unexpected end of data

The JSON.parse may be because I'm expecting JSON from the server when an error arrives to display them to the end user on my app, however, besides that message I'm not getting any clues as to what is happening or why the request is being cancelled.

As a last note, if I remove the header from the request (on the js above), the GET gets executed, but refused by the server since there's no auth.

Any idea of what's going on would be greatly appreciated.


Solution

  • So, it's been a bumpy ride, but I've learnt a lot about CORS and whatnot lately, and thought I might as well answer this for future generations to realise. Hope this will help someone out there.

    First, a little background:

    This is a simple form that gets sent to a server on another domain, hence the CORS bit. I am using Backbone to handle the client, Rails on the server and HAL to manage the data all around. The CORS is handled using Basic Auth. Upon receiving the data, the server responds with a 201 - Created, an empty body and a location response header where I should go next.

    This brings a LOT of ah-ha's moments that I'm about to share with you.

    Basic Auth

    @j03w was spot on when he mentioned that it may be not the front-end fault, but backend. It was, indeed, a lack of headers on the preflight request what was making my request fail. Turns out, if this preflight fails, no request is ever shown, even though one technically took place. This was quickly solved by means of this nifty gem.

    That was not the end of it, though.

    Empty response? You're about to suffer!

    I was now being able to interact with the server, but when the form was posted, the response was a "201 - Created" code, which is a success code, but the success callback was not called. In it's place, the error one took over. Why? Well, it turns out that my server was returning nothing in the body of the response. This was the one at fault.

    At first, I thought thought it was Backbone's fault, since it is expecting the updated model as a response. Turns out, it's not Backbone's fault. It is still expecting the updated model from the server, but it's not what's triggering the error callback. This is actually jQuery's fault. As of 1.9, an empty string returned for JSON data is considered to be malformed JSON (because it is).

    But the server does not want to return the model! (I'm a front-end developer, but am asuming the backend guys have a really good reason for not doing this). How am I to fix this?

    Well, turns out that changing the Status Code fixed this. Instead of a "201 - Created", a "204 - No Content" was used. jQuery handles this perfectly, and the success callback is now called.

    A matter of relocating

    The last step on this painfully longer-than-needed journey was relocating the user to the url the server sent on the Location response header. "Easy as pie!" - said the naive developer. "I just need to access the headers using xhr.getResponseHeader('Location')!" - he exclaimed with excitement.

    Except that didn't work. After some debugging (trying everything before a simple console.log(xhr.getAllResponseHeaders() ), I figured out that it wasn't returning the Location header. In fact, it was only returning 2 useless headers: Cache-Control and Content-Type! I don't need this! I turned my head to the Response on the POST request on my firebug console, and there it was. The location header was being sent, along with a LOT of response headers that xhr.getAllResponseHeaders() was not returning! Why, in the name of the triforce is this not working?

    Well, turns out it needs some more configuration as well. More server-configuration options turned out that we needed to explicitly tell which headers the client could read using "Access-Control-Expose-Headers". Listing "Location" there fixed this problem.

    resource '*', { 
      headers: :any,
      expose:  ['Location'],
      methods: [:get, :post, :options]
    }
    

    So there you have it. It's been a long and bumpy ride, but I hope this will help someone out there figure things quickier when dealing with all these technologies.