Offline POSTs with Progressive Web Apps

I’m in my eleventh week of twelve as a Developer Content Experience intern at Microsoft. That means that I’m a technical writer — I write about code to teach developers about Microsoft software. I spent my time here working on Progressive Web Apps, which are arguably the coolest thing happening on the web right now.

The features of PWAs

PWAs are web apps that look and feel like native apps — they’re installable, they load quickly, they can deliver push notifications, and they work offline. How do they do all that?

Service workers are the key. A service worker is a JavaScript file that runs in the background of a website. It intercepts resource requests and other events and responds to them in unique ways that you define. This gives you fine-tuned control over your app’s user experience, even under conditions of limited or no internet connectivity.

A typical example of a service worker in action is responding to a GET request from the cache. This allows for a faster load for static content. Code for serving a resource from the cache might look like this:

// Event listener for retrieving data
self.addEventListener('fetch', function(event) {
event.respondWith(
	// Get the request from the cache, then return it
caches.match(event.request))
.then(function(response) {
return response;
});
);
});

Another typical example of a service worker in action is fetching a requested resource from the network, then storing it in the cache for later use. Storing the resource in the cache will allow it to be served more quickly the next time it’s requested. We can expand on our previous code sample to show this.

// Event listener for retrieving data
self.addEventListener('fetch', function(event) {
var req = event.request.clone();
    if (req.clone().method == "GET") {
event.respondWith(

// Get the response from the network
return fetch(req.clone()).then(function(response) {
	       // And store it in the cache for later
return cache.put(req.clone(), response);
});
);
}
});

You may have noticed that before we actually respond to the event, we check to see if the request method is “GET.” We do that because right now, you can’t store POSTs with the Cache API. If you’re interested in building a truly seamless offline experience for your users, this is a little disappointing. (But don’t worry — it doesn’t have to be inhibitive to providing that great offline experience!)

Caching POST requests is, at the time of this writing, an open issue in the Service Worker spec. A lot of people are requesting the ability to cache POSTs, but until it arrives, we have to rely on workarounds. As part of my internship this summer, I came up with a way to store POST data locally if the user is offline (without using the Cache API), and then send the data to the server as soon as internet connection is regained.

The first thing you need to do if a POST request fails is send a message from the service worker to the client to alert it to the failure.

event.respondWith(
// Try to get the response from the network
fetch(event.request.clone()).catch(function() {
	// If it doesn't work, post a failure message to the client
self.clients.match(thisClient).then(function(client) {
client.postMessage({
message: "Post unsuccessful.",
alert: alert // A string we instantiated earlier
});
});
	// Respond with the page that the request originated from
return caches.match(event.request.clone().referrer);
});
);

If you’re posting a message, you also need a message event listener. This will be in the client-side JavaScript running in your app. In the event handler, you can let your user know that their request didn’t go through, but that it will as soon as they go back online.

navigator.serviceWorker.addEventListener('message', function(event) {
alert(event.data.alert);
});

Instead of using alert(), you could define your own way of talking to the user that’s more consistent with your app’s UI.

Next, you need to store the POST data in local storage so it can be sent to the server later. To do that, we extend on our message event listener.

navigator.serviceWorker.addEventListener('message', function(event) {
alert(event.data.alert);
store();
});

The store function gets the values the user inputted and saves them in a string called “newPost” in local storage. You could also use IndexedDB. We use one of these options so the data is saved even if the browsing session ends. The string is called “newPost” to simplify the code — if your app supports multiple new POST requests, then you would want to give each POST a unique identifier in local storage or IndexedDB. (Once my code becomes publicly available, you can see how this might work in a more complicated situation.)

function store() {
var newPost = ""; // Inputted values
    // Iterate through the inputs
$("input").each(function() {
newPost += $(this).val() + ",";
});
    // Get rid of the last comma
newPost = newPost.substr(0, newPost.length - 1);
    // Store the data
localStorage.setItem('newPost', newPost);
}

Once this function is finished, you can inspect local storage and see that it might look something like this:

The newPost variable, shown in F12 Developer Tools

Now that you’ve saved your POST data, you just need to submit it once the user goes back online. You can do this by appending inputs to a hidden form in the page’s HTML and then submitting the form. Here’s what the hidden form might look like:

<!-- Empty form for voting for your favorite article -->
<form method="post" action="/vote" id="offlinePostForm"></form>

And here’s how you might append the inputs to the form.

// Call the function whenever the page is refreshed
$(function () {
if (localStorage.getItem('newPost')) {
	// Set keys and values for voting for your favorite article	var names = ['first', 'last', 'article'];
var values = localStorage.getItem('newPost').split(',');
	// Form to append inputs to
var form = $("#offlinePostForm");
	// Create and append inputs
for (var i = 0; i < names.length; i++) {
var input = '<input type="hidden" name="'
+ names[i] + '" value="'
+ values[i] + '">';
$(input).appendTo(form);
}
	// Clear newPost from localStorage and submit the form
localStorage.removeItem('newPost');
form.submit();
}
});

Once the form is submitted, the page will reload, and the inputs will be removed from the form automatically.

With that last code snippet, you have a way to implement support for offline POSTs in your PWA!

This workaround will only be necessary temporarily, until the Service Worker spec (and browsers) support caching POSTs. However, your users don’t care about the Service Worker spec. They just want to have an uninterrupted experience using your PWA. Offline functionality is one of the key components of a great app-like experience, and one of the things that makes PWAs so powerful. If you’re interested in providing that to your users right now, you should consider building a PWA and using this approach to create a truly seamless offline experience.

You can try out Service Worker today in Microsoft Edge preview builds by enabling it under about:flags. Look for more about Service Worker and Progressive Web Apps at Microsoft Edge Web Summit in September!