IT IS HERE! Get Smashing Node.JS on Amazon Kindle today!
Show Posts
← Back to homepage

tl;DR: only 2/3 bugs have an impact, one has a trivial workaround (jQuery snippet included below), the other one is UX related. Socket.IO needs no updates.

In the past 48 hours a lot has been written about the introduction of iOS6. With such a major software release it’s not surprise that a few issues have been introduced. Of particular importance to us web developers, three different bugs have been attributed to Mobile Safari:

  • A continuous “spinning loading indicator” reported on TechCrunch
  • Broken long-polling due to a connection limit set to 1 explained by Real Software
  • The introduction of aggressive caching of POST requests, reported on Stack Overflow

As it turns out, a lot of misinformation is being spread as well. In addition, all of these bugs are of very high importance to the development of the Socket.IO and Engine.IO realtime frameworks. Therefore I took the time to document the actual bugs and their workarounds in this post.

The spinning bug

This bug report points to the degradation of UX by the introduction of a spinner whenever an AJAX request is active. This is specially concerning for apps that are constantly polling the server (eg: any realtime app).

When I first read reports of this bug, I was skeptical of whether it really had to be attributed to iOS6. The main reason for this is that this behavior already existed, and Socket.IO already has had a workaround for this for a long time. In fact, this bug has also affected many mobile and desktop browsers, which led to our decision of always deferring long polling requests to when the page has finished loaded.

The problem is that this workaround no longer works on iOS6.

Conclusion: as of today, no known workaround is available. Update: from my observations the spinner is very consistently shown for both native and web applications, which means that a workaround might not be feasible at all.

The AJAX cache bug

The gist is that POST AJAX requests are aggressively cached in the absence of cache control headers, contrary to the behavior exhibited by most user agents.

I was able to reproduce this easily:

I’ve documented two workarounds:

  • Appending a random identifier or timestamp to the URL
  • Sending the Pragma: no-cache or Cache-control: no-cache header from the server

This is a serious bug that impacts a large number of web applications. If you’re in a hurry, you can quickly fix it on the client side, by including a timestamp in the query string. If you are using jQuery, the following snippet should do the trick:

if (/OS 6_/.test(navigator.userAgent)) {
  $.ajaxSetup({ cache: false });
}

Conclusion: I strongly recommend you implement one of the two documented workarounds for your application. Ideally you want to set the right header on the server-side to avoid the potential eviction of useful cached objects (thanks Giovanni Spedito for the pointer), and to avoid user agent sniffing. Socket.IO was already timestamping URLs to fix very similar behavior introduced as far back as IE6 days.

The long-polling bug

The report was that with iOS6 “Safari is allowing only one connection at a time”. I have not been able to verify this.

Socket.IO’s acceptance test suite for the long polling XHR transport is exhibiting similar performance to Desktop Chrome:

Conclusion: long-polling continues to work normally with HTTP Keep-Alive sockets and requests that are deferred until page load, the way Socket.IO has always done it.

Update: Steve Souders adds that iOS6 Safari supports up to 5 sockets per hostname, which means it can comfortably support applications that use long-polling.

17 Comments

Giovanni Spedito said

Including a timestamp in every POST request will work, but it will also pollute the cache with useless responses and in the case of long polling it will probably fill the browser cache in a matter of hours or even shorter, possibly evicting useful contents; please don’t advise that

    Guillermo Rauch said

    Thanks for the suggestion. I updated the post accordingly.

Ron said

I’m not sure if the limit is 1 to any domain at a time. But I too have experienced strange connectivity issues, which indicate a more aggressive connection limit. This needs more investigation.

    Guillermo Rauch said

    Let me know if you find out more. I’m curious since I was able to get I believe 4 sockets going without problems.

Ron said

There is one more bug I haven’t seen anybody writing about. If a touch event causes a setTimeout, but that same touch event causes scrolling, that timer will never fire.

More information and a workaround here: https://gist.github.com/3755461

russm said

you call the spinner a UX bug, I call it my device correctly informing me that you’re using my battery with background network traffic.

kl said

Pragma header is a bit ugly. Cache-Control: no-cache would be more elegant.

    Guillermo Rauch said

    HTTP/1.1 caches SHOULD treat “Pragma: no-cache” as if the client had sent “Cache-Control: no-cache” (RFC 2616 14.32).

    This is definitely the case for iOS6 as the testcase shows. Of course, Cache-Control is a good option as well.

Fernando Correia said

“With such a major software release it’s not surprise that a few issues have been introduced.”

No surprise for Apple customers, maybe. I would have you know that other OS providers will release beta and preview versions of a new OS so the ISV ecosystem can check the compatibility of their apps and find bugs BEFORE the new version is released to the public.

Christian said

When I inspect my ajax-request in chrome (Network XHR) I see both
Cache-Control:no-cache
and
Pragma:no-cache

Still iOS6-devices seem to cache it…..

Kevin said

I got this snippet of code and it works perfect.

$.ajaxPrefilter(function (options, originalOptions, jqXHR) {
// you can use originalOptions.type || options.type to restrict specific type of requests
options.data = jQuery.param($.extend(originalOptions.data||{}, {
timeStamp: new Date().getTime()
}));
});

Steve Souders said

I also was NOT able to reproduce the “1 connection per hostname” bug. This test page loads 6 images. Each image takes 5 seconds to download. If they were downloaded 1 at a time then it would take 30 seconds for window.onload to fire. But I ran this on iOS6 (iPhone 4) in mobile safari, FB app, and Twitter app and it takes 10 seconds to load. When I tail the server logs I see it making 5 requests, then the sixth.

So it appears that iOS 6 actually does FIVE CONNECTIONS PER HOSTNAME.

http://stevesouders.com/cuzillion/?c0=bi1hfff5_0_f&c1=bi1hfff5_0_f&c2=bi1hfff5_0_f&c3=bi1hfff5_0_f&c4=bi1hfff5_0_f&c5=bi1hfff5_0_f

Martyn said

For our web app we though we had fixed this aggressive cache issue with Cache-Control: no-load, but there remains a problem with home screen links. It looks like IOS6 has a secondary cache for home screen links such that, although the Safari cache has been cleared and the app works fine now from Safari directly, if the app is accessed via a home screen link it seems to access previously cached data.

Your thoughts?

About Guillermo Rauch:

CTO and co-founder of LearnBoost / Cloudup (acquired by Automattic in 2013). Argentine living in SF.