Mozilla

History API changes in Firefox 4

This is a guest post by Jonas Sicking, one of the Gecko developers.

As I’m sure you know we’re getting ready to ship Firefox 4. And as you
might know Firefox 4 includes the history API (which includes the pushState() and replaceState() methods) defined in HTML5. This API is also implemented in Safari and Chrome, but Firefox 4 has important differences, which I describe in this post.

A few weeks ago someone discovered a pretty big flaw in the pushState API. The problem is that if you use the state argument to pushState() or replaceState(), and the user later reloads page with such a state, there is no way to get access to said state until after the load event fires. This because the only way to get access to said state is through the popstate event which doesn’t fire until after load has fired.

This means that for pages that use the state argument, the page has to render without knowledge of said state, and only after the page has been fully loaded can the correct state be shown to the user.

Note that the “state” that I’m talking about here is the state argument passed to pushState()/replaceState(). The URL (which arguably is the much more useful argument to pushState()/replaceState()) is always accessible using the normal APIs like document.location and window.location.

To fix this problem we are making two changes in our implementation compared to the current working draft:

  • Always expose the current state through a window.history.state property. This way a page immediately gets access to the current state of the page and doesn’t have to wait until the first popstate event fires.
  • Don’t always fire a popstate event right after the load event.
    Instead, only fire it during real session history transitions (i.e., when the user clicks Back or Forward or when history.back()/forward()/go() is called)

    The whole purpose of this extra popstate event was to give access to the page’s state. However the window.history.state property makes this redundant. We have found that pages just find this event unexpected and a source of bugs.

The first change should be fully backwards compatible since it’s a purely additive change. It doesn’t affect existing code, which presumably doesn’t use this property.

The second change is the bigger concern. If your code is expecting this event to always fire, then this might result in problems. Another thing that alleviates the risk with this change is that Safari 5 appears to have misunderstood the working draft on this issue, and doesn’t fire this popstate unless a state is specifically passed to pushState()/replaceState(). So basically Firefox will behave like Safari 5 as long as you don’t use the state argument.

We are also making a third change:

  • Allow popstate to fire while the page is loading.

The working draft currently has a somewhat surprising limitation in that it forbids any popstate events from firing before the load event for a page has fired. If the user clicks on a pushState-backed link while the page is loading (for example due to a slow-loading image), and then presses the Back button, no popstate event fires. Only after the load event for the page has fired is the first popstate allowed to fire. We have removed this limitation and always fire popstate when the Back or Forward button is pressed or when history.back()/forward()/go() is called.

I have done some testing and so far have not seen any problems due to these changes. Unfortunately, due to discovering these problems so late, these changes won’t appear in Firefox betas until Firefox 4 RC. There are test builds available, which you can test with right away.

10 comments

Comments are now closed.

  1. Christoph Pojer wrote on March 4th, 2011 at 14:25:

    Awesome, the fact that popstate fired on page load was very counter intuitive and I worked around it in my plugins. I hope the spec gets adjusted to adopt your changes.

  2. Wahooney wrote on March 4th, 2011 at 22:38:

    Pardon me if I’m getting it wrong (please correct me if I am), but isn’t the ability to change the user’s History a bit of a gaping security hole? There must be countless ways to bugger the user around with something like this? Pushing spam states, creating false histories, etc.

  3. André Luís wrote on March 7th, 2011 at 02:33:

    Yes!! Thank you. I even thought of disregarding te first popstate, but how could we know if it wasn’t a real interaction w/ back/fwd button?!

    Have you sent this proposal for change to the what-wg?

  4. André Luís wrote on March 7th, 2011 at 09:45:

    @Wahooney, well… you already have the user on your site. This only works for “same origin” URLs… so yes, there is a threat if you can run javascript on a hosted site like wordpress.com or blogspot… but since all personal areas are subdomains they’re different origins, so no biggie.

    If you can run javascript in paypal.com, well then, game over already. ;)

  5. radu wrote on March 8th, 2011 at 05:59:

    Firering a popstate event right after the load event was for me annoying, I think that is a great ideea to fire it every time the back/Forward button is pressed, or, history.bask(), history>forward, or history.go() is called.

  6. Benjamin Lupton wrote on March 11th, 2011 at 07:43:

    Totally agree with this change. Users of History.js have the method History.getState() available to fetch the current state, and the call of popstate on page load is annoying to say at least – it causes everybody to have code like:
    var first = true; $(‘body’).bind(‘popstate’,function(){if ( first ) { first = false; return; } …

    Which is just plain silly. I’ll will implement these changes in the new version of History.js (v1.6). One note though, using a variable for the state (history.state) seems kind of dangerous, what if they are working with a state, then the state changes – the object reference would change! Causing a side effect in the current executing code! Uh ohhh… Seems smarter to do history.getState() which would return a copy of the current state object – to avoid that issue. Thoughts?

  7. John wrote on March 31st, 2011 at 18:07:

    Here we go. Rolling down the slippery slope of “oh i don’t like the specs, so let me implement it different so that you’ll have to develop specifically for Firefox 4″
    I agree that the API is flawed, but changing the API is a dick move.
    I think I’ll spend the time to block Firefox 4.0 instead of supporting this.

    1. Benjamin Lupton wrote on March 31st, 2011 at 21:17:

      Wow John, relax.

      The spec is a draft, every (and I mean every) HTML5 browser implements the spec differently. As long as the spec remains a draft, it is encouraged to change it to make it the best – in fact, this move by firefox caused the spec to change, so actually they are following the spec now. The latest webkit nightly build also had adopted this change.

      There will always be differences with the implementation, which is why there are polyfills to ensure compatibility:
      https://github.com/balupton/history.js

      So no the world has not ended, it is moving forward, and you can support all the browsers by using a polyfill. Life is good.

  8. Chris wrote on June 21st, 2011 at 07:51:

    I think this is a much more sensible way of doing it, but the difference between this approach and the other existing implementations is hard to cater for.

    Before, I was detecting the first onpopstate call (the one that fires right after page load) and ignoring it. All subsequent calls to onpopstate then cause an ajax request for content. This works fine in webkit.

    Now, I can’t ignore the first onpopstate call, because in Firefox, it will not be a redundant call after pageload. It will be the user clicking the back button. However, I can’t _not_ ignore it, because then webkit calling onpopstate after page load would cause the page to be loaded twice (once a full page load, once an ajax request).

    There doesn’t appear to be any way of distinguishing webkit’s onpopstate call that is called as a result of a pageload from an onpopstate call triggered by history traversal. You could check for the state being null, but it would also be null if traversing the history back to the initial state of the page.

    I guess my predicament stems from the fact that webkit’s approach is silly, but before Firefox decided to do things differently, I had a reasonable workaround. I would rather avoid running different code based on the user-agent string. This wouldn’t even work, as I hear that webkit will be doing it this way too.

    I prefer this approach, but how can I elegantly cater for both existing implementations?

  9. Alan Kesselmann wrote on July 14th, 2011 at 07:40:

    Hello

    I stumbled upon exactly the same problem that previous commentor(Chris) wrote about. Then i set out googling and started using history.js (which i at first discarded because i wanted to get there myself – to really understand whats going on) and found that it fixed my problem.

    Now im interested in finding out why History.js works in both browsers? Because of using statechange instead of popstate i guess… Gah.. instead of writing here i should probably go and read about it at docs :).

    Guess my comment here is only good for saying that History.js fixes previous problem :P

    Alan

Comments are closed for this article.