7

I needed to catch a possible login page in all responses from the server, so I have overridden Backbone.sync globally so I can check all errors before passing them on.

Backbone.originalSync = Backbone.sync;

Backbone.sync = function (method, model, options) {
    var originalSuccess, originalError;
    console.log("sync override...");
    // remember original success so we can call it if user logs in successfully
    originalSuccess = options.success;
    // proxy the error callback to first check if we get a login page back
    originalError = options.error;
    options.error = function (model, xhr, options) {
        if (xhr.status === 200 && xhr.responseText === "") {
            // parse error from empty response (jq1.9 invalid json, ok)
            originalSuccess(model, xhr, options);
        } else {
            console.log("Sync error " + statusTxt + ", " + thrown.message);
            if (xhr.status === 200 || xhr.status === 302 || xhr.status === 0) {
                // login page returned instead of json...
                // open a new window with relogon.html to trigger a new login
                window.showModalDialog("../relogon.html");
            } else {
                // normal error, pass along 
                if (originalError) {
                    originalError(model, xhr, options);
                }
            }
        }
    };

    // call the original sync
    Backbone.originalSync(method, model, options);
};

This broke miserably when going from 0.9.9 to 1.0. Looks like the original Backbone.sync wraps its error handlers differently, causing my error handler to be called first, with a jquery xhr signature. I had to change the signature of the error handler to this:

    options.error = function (xhr, statusTxt, thrown) {

Ok so now it works, but I get the feeling that I am doing something wrong.

Is there a better way to do this?

I tried with jquery promises but I need to be able to switch from error state to success (when calling originalSuccess), which did not seem to work with promises.

d4kris
  • 528
  • 5
  • 11

2 Answers2

9

All sync errors are passed to model's error event, so you may to listen to this event.

From http://backbonejs.org/#Events-catalog:

"error" (model, xhr, options) — when a model's save call fails on the server.

To capture error globally you may use http://api.jquery.com/ajaxError/

Andrey Kuzmin
  • 4,479
  • 2
  • 23
  • 28
  • Is it possible to listen to all models and collections globally? This error can happen anytime so I'd like to handle it globally. Anyway, I need to capture the error and handle it, not only listen to it, so I dont think this would work in my scenario. – d4kris May 10 '13 at 10:44
  • Aren't you confusing backbone events with jquery ajax handlers here? AFAIK, ajaxError is not at all related to the backbone error event; if you manually trigger an error event, for example, you wouldn't fire the ajaxError handler, and the arguments to the callback will be different – SpoonMeiser Mar 29 '16 at 10:19
8

You can build your own jQuery Deferred object to alter the default Backbone.sync behavior

Backbone.sync = function (method, model, opts) {
    var xhr, dfd;

    dfd = $.Deferred();

    // opts.success and opts.error are resolved against the deferred object
    // instead of the jqXHR object
    if (opts)
        dfd.then(opts.success, opts.error);

    xhr = Backbone.originalSync(method, model, _.omit(opts, 'success', 'error'));

    // success : forward to the deferred
    xhr.done(dfd.resolve);

    // failure : resolve or reject the deferred according to your cases
    xhr.fail(function() {
        if (xhr.status === 200 && xhr.responseText === "") {
            dfd.resolve.apply(xhr, arguments);
        } else {
            if (xhr.status === 200 || xhr.status === 302 || xhr.status === 0) {
                console.log('login');
            }
            dfd.reject.apply(xhr, arguments);
        }
    });

    // return the promise to add callbacks if necessary
    return dfd.promise();
};

The promise reflects the end state you choose.

http://jsfiddle.net/AsMYQ/4/ for a fail demo, http://jsfiddle.net/AsMYQ/5/ for a success.

And if I may

  • you probably should not tie so tightly Backbone.sync with your login handling. Use events, from Backbone or jQuery.ajaxError as @Andrey suggested
  • your server response should indicate an authorization failure, probably a 401 status
  • don't forget to return your deferred promise/jqXHR object when you override sync, that might come in handy down the line
nikoshr
  • 32,926
  • 33
  • 91
  • 105
  • Thank you, now I see what I did wrong when I tried with deferred. About your suggestions, you are probably right - I will try to decouple the relogin, whats worse is I dont have any control over the server responses (there is a legacy SSO intercepting all calls) – d4kris May 13 '13 at 20:28
  • Why don't you return the xhr object from your new sync() function, rather than make a new promise? – TV's Frank Nov 10 '14 at 13:25
  • Because the conditions for success and failure are not the ones defined by the xhr – nikoshr Nov 10 '14 at 13:29