A word of caution
There are, of course, reasons, why POST requests are not part of the current Service Worker specifications. Here are some of them:
- Theoretically, POSTs are expected to modify data. You cant's use a cached version of data, that your are trying to modify, of course. So never cache modifying requests of any kind!
- Those APIs, using POSTs for reading, usually do not have a separate URL for every possible result. Instead, they will have a single URL to retrieve different data sets and accept queries of some sort in the request's body. This means, you can't cache responses based on the URL (as the cache API does), but rather need to use your own combined keys including the body and, possibly, some headers.
So think carefully before caching POST requests in your PWA! If your are looking for a way of make modifying POST requests work offline, I recommend looking at the Mozilla code-recipe for deferred POSTs.
How to cache POST requests in a Service Worker
Luckily, the Service Worker can intercept any asynchronous request, so there is not problem getting our hands on POST requests. What's missing, is a possibility to cache them and retrieve the respective response while being offline.
In the following example, we will create a simple cache in IndexedDB and use it as a fallback (NetworkFirst-strategy). Note, that we are going to use a stringified version of the entire request as the cache key. This is pretty safe, but will produce big keys for requests with long bodies and/or headers. If this is a problem for you, you can pick another way to build the key or simply compress it with lz-string or a similar library.
For a working example caching all POST requests, place this code in your ServiceWorker.js
. I'm using Dexie.js to access IndexedDB here, but any other way would work too, of course.
importScripts('your/path/to/dexie/dist/dexie.min.js'); // Listen to fetch requests self.addEventListener('fetch', function(event) { // We will cache all POST requests, but in the real world, you will probably filter for // specific URLs like if(... || event.request.url.href.match(...)) if(event.request.method === "POST"){ // Init the cache. We use Dexie here to simplify the code. You can use any other // way to access IndexedDB of course. var db = new Dexie("post_cache"); db.version(1).stores({ post_cache: 'key,response,timestamp' }) event.respondWith( // First try to fetch the request from the server fetch(event.request.clone()) .then(function(response) { // If it works, put the response into IndexedDB cachePut(event.request.clone(), response.clone(), db.post_cache); return response; }) .catch(function() { // If it does not work, return the cached response. If the cache does not // contain a response for our request, it will give us a 503-response return cacheMatch(event.request.clone(), db.post_cache); }) ); } }) /** * Serializes a Request into a plain JS object. * * @param request * @returns Promise */ function serializeRequest(request) { var serialized = { url: request.url, headers: serializeHeaders(request.headers), method: request.method, mode: request.mode, credentials: request.credentials, cache: request.cache, redirect: request.redirect, referrer: request.referrer }; // Only if method is not `GET` or `HEAD` is the request allowed to have body. if (request.method !== 'GET' && request.method !== 'HEAD') { return request.clone().text().then(function(body) { serialized.body = body; return Promise.resolve(serialized); }); } return Promise.resolve(serialized); } /** * Serializes a Response into a plain JS object * * @param response * @returns Promise */ function serializeResponse(response) { var serialized = { headers: serializeHeaders(response.headers), status: response.status, statusText: response.statusText }; return response.clone().text().then(function(body) { serialized.body = body; return Promise.resolve(serialized); }); } /** * Serializes headers into a plain JS object * * @param headers * @returns object */ function serializeHeaders(headers) { var serialized = {}; // `for(... of ...)` is ES6 notation but current browsers supporting SW, support this // notation as well and this is the only way of retrieving all the headers. for (var entry of headers.entries()) { serialized[entry[0]] = entry[1]; } return serialized; } /** * Creates a Response from it's serialized version * * @param data * @returns Promise */ function deserializeResponse(data) { return Promise.resolve(new Response(data.body, data)); } /** * Saves the response for the given request eventually overriding the previous version * * @param data * @returns Promise */ function cachePut(request, response, store) { var key, data; getPostId(request.clone()) .then(function(id){ key = id; return serializeResponse(response.clone()); }).then(function(serializedResponse) { data = serializedResponse; var entry = { key: key, response: data, timestamp: Date.now() }; store .add(entry) .catch(function(error){ store.update(entry.key, entry); }); }); } /** * Returns the cached response for the given request or an empty 503-response for a cache miss. * * @param request * @return Promise */ function cacheMatch(request) { return getPostId(request.clone()) .then(function(id) { return store.get(id); }).then(function(data){ if (data) { return deserializeResponse(data.response); } else { return new Response('', {status: 503, statusText: 'Service Unavailable'}); } }); } /** * Returns a string identifier for our POST request. * * @param request * @return string */ public function getPostId(request) { return JSON.stringify(serializeRequest(request.clone())); }
Feel free to use this code under the MIT license