If you would prefer to watch a video instead, you can check it out below!

At Embrace, we help mobile companies solve their most difficult production issues. In this series, we’ll cover interesting bugs that mobile developers might come across, and we’ll provide tips and tricks for finding and fixing them.

In a previous post, we covered how a blank web view can occur as a result of the OS killing a content process in a WKWebView. After publishing that post, we ran into another interesting root cause of blank web views in a partner’s application that we wanted to share.

This one was caused by downgrading. If you don’t know what that is, don’t worry. We’ll explain it soon, but first, here’s a little context to set the stage.

How apps are installed on your iPhone

A mobile application isn’t just one file on your phone. Developers create a .ipa file and upload it to the App Store. When your users install your app, the OS actually unzips that file.

Inside the zip file is the program (i.e. the application binary) and any ancillary files that your program needs to run. These can be things like an icon, loading screens, UI specifications, configuration files, local content, etc. Anything that you ship alongside your app that isn’t the app binary is called the app bundle, and that also gets installed on disk.

Our partner’s app

In a more complex app, you’ll have your binary, some bundle content, and you might be using some frameworks. Our partner’s application was using a very popular framework called GCDWebServer, which is a local HTTP server that is inside your app.

That probably sounds like overkill, but it actually is one of the coolest things that you can put inside of your application if you need to ship hybrid content. The reason is because if you load your CSS, HTML, and JS over the internet from your web server inside of a native app, your users are going to know this is happening because of the delays in loading the content. When they click a button, instead of your app reacting instantly, it has to go all the way back to your server and then all the way back to the phone to figure out what to do.

However, if you use GCDWebServer, you can load that exact same content bundle of HTML, CSS, and JS that your server would have, but you can load it locally from disk. This is a way to make a hybrid app that feels almost native. Those web pages will react really quickly.

And that’s what our partner has done. They ship a bunch of this static HTML content on disk and have GCDWebServer load it. Since it is a full HTTP server, it also maintains a cache that obeys all of the HTTP caching rules.

GCDWebServer file-loading logic

When the application wants to load some content, it asks GCDWebServer to load the HTML into a WKWebView. GCDWebServer will first look for that page in its cache, and if it can’t find it, it will then look for it on disk.

Downgrading

In order to understand this bug, we must go over a concept called downgrading. You’re probably very familiar with upgrading your app. That happens every time you install a new version of your app. Downgrading is what happens when a user is running a new version of your app and for some reason ends up installing an older version on top of it without first deleting that newer version.

This will happen a lot when you’re in QA and Dev and you’re working with Apple’s TestFlight system. Your App Store build is probably older than your TestFlight build because that’s the one you’re about to release. So if someone in QA tests that TestFlight build and then installs the App Store build without deleting the TestFlight build first, they end up in a downgrade scenario. We’re gonna go through one of those scenarios and explain why that matters.

Let’s first go over what it looks like when the app is upgraded to a newer version. Then we’ll see how it differs when the app goes through a downgrade.

An example of upgrading

So here’s our partner’s app again, but now we’ve annotated the components with their versions. The tester is running v1 right now, but v2 is available in TestFlight. So we’re gonna go ahead and install v2, but first, notice that the cache content of GCDWebServer is not a part of the app’s  .ipa it is distributed with. That’s not something that the developer creates. That cache file gets created when the application runs.

When we install v2, it’s not going to overwrite the v1 GCDWebServer cache. After all, why would it overwrite that file if it is not in the .ipa? That file only exists at runtime.

So now if we run the program once, you see that the GCDWebServer cache gets updated. It now contains version 2 of our JS file.

Let’s see what happens if we instead downgrade from v2 to v1.

An example of downgrading

As you can see, our QA tester installed the latest version locally (v2). Now they’re going to install the App Store version (v1). We have to pay attention here because GCDWebServer has a v2 cache. This is the TestFlight version of the cache content.

So our tester has installed v1 from the App Store and has all the v1 components, but the cache is from v2. So what is going to happen next when we run the application?

GCDWebServer is going to load index.html from disk. The reason is because the filenames match, and the content on disk is the same as the content in the cache. GCDWebServer is going to load main.css from disk for the same reason.

But something different will happen when it comes to the JS file.

In GCDWebServer’s point of view, the JS file that is in its cache is actually a better version of that file than the one that’s on disk, assuming they have exactly the same filenames. So GCDWebServer is returning the cached version of the JavaScript while it returned the disk versions of HTML and CSS.

Unfortunately for our partner and for most websites, loading a different version of JS than you do HTML is often incompatible and will result in the page not loading at all. In a web browser, you could use Chrome developer tools to figure this out, but in a mobile app, often this results in the screen just being blank.

So how can we fix this problem?

Enter cache busting

Since we have a full HTTP server in our mobile client, we can use the same exact cache busting techniques that JavaScript developers have known and loved for years.

HTTP cache busting

Let’s go over what cache busting is. First, let’s go through a normal cache example. If the application asks to load main.js, if the HTTP cache has a newer version, it’s just going to return that. So even though the disk has a version of this file, the HTTP cache has a newer version, so it’s just going to go ahead and return the version from cache.

How can you make sure to load the on-disk version?

The way you can bust through that and make sure that you’re always loading the latest JS file and never loading the cached version is by adding a dynamic property to the query parameters of your request.

HTTP cache busting where application gets file from disk

In most cases what we do is we get the timestamp from the system and we set it to a query parameter. So the request would be to load main.js?v={ts}, where the timestamp is dynamically generated at runtime.

Because we’re sending a dynamic property, the HTTP cache cannot return the static copy of main.js. Instead, it has to query the main.js file on disk in order to evaluate what to do with this property because the HTTP server doesn’t know what to do with it.

This cache busting technique is very old and well-loved by JavaScript developers when you’re dealing with HTTP servers that have strong caching.

Summing up what we’ve learned

Summary slide

Blank web views have many root causes. We covered the iOS process model being a possible root cause in a previous post, and in this post, we went over how local web content and caching can also lead to blank web views.

So the first takeaway is that with a blank web view, you really can’t assume what the root cause is because there are many possible things that could be going wrong.

The second takeaway is that local web servers are a great way to boost performance. The purpose of this post is not to knock on GCDWebServer. It did everything it could to make this app successful. It did everything it should have according to the HTTP spec. The problem was just not understanding how an HTTP cache works. As a developer, you need to look out for these types of issues if you are going to include hybrid content in your app. This problem can be especially bad if your app has static content that you can downgrade as shown in this post.

The bright side is that cache busting works on mobile! If you have a full HTTP server that has a well-behaving cache, then you can bust through that cache using a dynamic query parameter.

I hope you enjoyed this exploration of another blank web view. Please send any interesting bugs you run across to eric dot lanz@embrace.io. We’re always looking for bugs to share, and hopefully, next month we will have something even more interesting to show you. Thanks for reading!

Who we are

Embrace is a data driven toolset to help mobile engineers build better experiences. If you’d like to learn more about Embrace, you can check out our website or request a demo.