Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

I Broke AJAX in Chrome 52!

DZone's Guide to

I Broke AJAX in Chrome 52!

This strange bug from yesterday won’t let me sleep. Why does Chrome 52 sometimes behave like different instances of a class are the same object?

· Web Dev Zone
Free Resource

Add user login and MFA to your next project in minutes. Create a free Okta developer account, drop in one of our SDKs to your application and get back to building.

This strange bug from yesterday won’t let me sleep. Why does Chrome 52 sometimes behave like different instances of a class are the same object?

Backbone’s fetch method triggers the bug. But maybe that’s not the real issue? I’d hate to submit a bug report to Chrome project only to be told “Fool, that’s a Backbone issue.”

It’s terrifying to tell the Chrome team they made a mistake.

This is what yesterday’s repro code looks like:

var BugModel = Backbone.Model.extend({
    url: "bla.json"
});

let bug = new BugModel();
bug.fetch({
    success: () => {
        console.log('fetch 1'); // prints
        doWeirdness(bug);
    }
});

function doWeirdness(bug) {
    let newBug = new BugModel({id: 1});

    console.log('about to re-fetch'); // prings

    newBug.fetch({
        success: () => console.log('fetch 2', newBug), // doesn't print
        error: () => console.log('error')
    });
    newBug.fetch({
       success: () => console.log('fetch 3') // prints
       });
}
2 fetches, 1 callback

If you press Cmd+R, the bug happens. If you press Cmd+Shift+R, it does not. That’s a new clue that points at either Chrome’s speed optimizations, or worse, the network stack. Can we call it a network stack? I guess Chrome is almost an operating system at this point …

Adding console.log(newBug == bug) prints false, which implies that Chrome does not think both instances are the same object. This invalidates my original hypothesis.

So what does Backbone’s Model.fetch method do?

fetch: function(options) {
      options = _.extend({parse: true}, options);
      var model = this;
      var success = options.success;
      options.success = function(resp) {
        var serverAttrs = options.parse ? model.parse(resp, options) : resp;
        if (!model.set(serverAttrs, options)) return false;
        if (success) success.call(options.context, model, resp, options);
        model.trigger('sync', model, resp, options);
      };
      wrapError(this, options);
      return this.sync('read', this, options);
    },

A lot of this stuff is unnecessary in ES6, but Backbone is from the before times.

We start with a default value for options – {parse: true}, then use the var = this trick because we don’t have arrow functions. Then we copy the options.success callback to a variable and define our own. You can think of it as a wrapper.

Inside the success wrapper, we parse data returned from the server and set new values on our model. Then we trigger a sync event. This could be where the bug happens.

Outside the wrapper, we defer to sync to actually talk to the server.

If I copy this method to my own model definition, we can inspect where it fails.

Success wrapper doesn't fire

The success wrapper doesn’t fire.

Let’s see what happens inside sync … ugh, it’s a long function. I’m not pasting it here. It does some setup, then defers to $.ajax to perform an ajax request to the server.

Can we make the same bug happen without Backbone, then?

$.ajax({
    url: 'bla.json',
    complete: () => {
        console.log('done 1st request');
        $.ajax({
            url: 'bla.json',
            complete: () => {
                console.log('done 2nd request');
            }
        });
    }
});
Bug without Backbone

It worked! 8 lines of code reproduce the bug.

And yes, both requests happen without error.

Network calls do happen

It might be safe to say that jQuery is battle tested enough that this couldn’t be a jQuery bug. But let’s try superagent to make sure. It’s a great library for making requests and it’s implemented independently of jQuery.

Does the bug still happen?

request.get('bla.json')
       .end(() => {
           console.log('1st success');
           request.get('bla.json')
                  .end(() => {
                      console.log('2nd success');
                  });
       });
Superagent repros too

Yeeeeep.

Now you might think: “A-ha! Every even Ajax call to the same URL fails.” I tried that, too -> it doesn’t. If you make requests in a loop, they all work.

The bug only happens, if you make the same AJAX request in the callback. You can extend a chain like this forever:

request.get('bla.json')
       .end(() => {
           console.log('1st success');
           request.get('bla.json')
                  .end(() => {
                      console.log('2nd success');
                  });

           request.get('bla.json')
                  .end(() => {
                      console.log('3rd success');

                      request.get('bla.json')
                             .end(() => {
                                 console.log('4th success');
                             });
                  });
       });

And it only prints the odd-numbered console.logs.

Guess it’s time to submit my first bug report to a big open source project. Yay, I’m helping!

Launch your application faster with Okta’s user management API. Register today for the free forever developer edition!

Topics:
ajax ,jquery ,bug ,chrome

Published at DZone with permission of Swizec Teller, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}