Can service workers cache POST requests?
Asked Answered
G

6

38

I tried to cache a POST request in a service worker on fetch event.

I used cache.put(event.request, response), but the returned promise was rejected with TypeError: Invalid request method POST..

When I tried to hit the same POST API, caches.match(event.request) was giving me undefined.

But when I did the same for GET methods, it worked: caches.match(event.request) for a GET request was giving me a response.

Can service workers cache POST requests? In case they can't, what approach can we use to make apps truly offline?

Grasping answered 8/2, 2016 at 13:20 Comment(1)
It should be very easy to simply try it :)Senskell
S
41

You can't cache POST requests using the Cache API. See https://w3c.github.io/ServiceWorker/#cache-put (point 4).

There's a related discussion in the spec repository: https://github.com/slightlyoff/ServiceWorker/issues/693

An interesting solution is the one presented in the ServiceWorker Cookbook: https://serviceworke.rs/request-deferrer.html Basically, the solution serializes requests to IndexedDB.

Senskell answered 8/2, 2016 at 14:39 Comment(3)
I haven't seen anything mentioned about PATCH or DELETE request. Is it the same ?Recant
Patch and delete should work the same. You'll get a "fetch" event in your service worker, you can check the method -- GET, POST, PATCH, DELETE -- and do whatever you need to do.Bluegrass
The Servoce worker cookbook example can be found here: github.com/mozilla/serviceworker-cookbook/tree/master/…Jess
L
13

I've used the following solution in a recent project with a GraphQL API: I cached all responses from API routes in an IndexedDB object store using a serialized representation of the Request as cache key. Then I used the cache as a fallback if the network was unavailable:

// ServiceWorker.js
self.addEventListener('fetch', function(event) {
    // We will cache all POST requests to matching URLs
    if(event.request.method === "POST" || event.request.url.href.match(/*...*/)){
        event.respondWith(
            // First try to fetch the request from the server
        fetch(event.request.clone())
            // If it works, put the response into IndexedDB
            .then(function(response) {
                // Compute a unique key for the POST request
                var key = getPostId(request);
                // Create a cache entry
                var entry = {
                    key: key,
                    response: serializeResponse(response),
                    timestamp: Date.now()
                };

                /* ... save entry to IndexedDB ... */

                // Return the (fresh) response
                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
                var key = getPostId(request);
                var cachedResponse = /* query IndexedDB using the key */;
                return response;
            })
        );
    }
})

function getPostId(request) {
    /* ... compute a unique key for the request incl. it's body: e.g. serialize it to a string */
}

Here is the full code for my specific solution using Dexie.js as IndexedDB-wrapper. Feel free to use it!

Liability answered 7/9, 2018 at 13:10 Comment(4)
While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes. - From ReviewConnote
Sorry for that. I've changed the answer to include a code summary (the entire code is pretty long). Or am I expected to post the entire code regardless of it's length?Liability
A snippet should be fine. If you just post a link to your own blog it looks a lot like spam. Seems a lot better to me nowConnote
I'd probably suggest you have a read though the following if you want to promote your blog Limits for self-promotion in answersConnote
E
2

If you are talking about form data, then you could intercept the fetch event and read the form data in a similar way as below and then save the data in indexedDB.

//service-worker.js
self.addEventListener('fetch', function(event) {
      if(event.request.method === "POST"){
         var newObj = {};

               event.request.formData().then(formData => {

                for(var pair of formData.entries()) {
                  var key = pair[0];
                  var value =  pair[1];
                  newObj[key] = value;
                }

              }).then( ...save object in indexedDB... )
      }
})
Edify answered 27/6, 2018 at 22:3 Comment(0)
F
2

Another approach to provide a full offline experience can be obtained by using Cloud Firestore offline persistence.

POST / PUT requests are executed on the local cached database and then automatically synchronised to the server as soon as the user restores its internet connectivity (note though that there is a limit of 500 offline requests).

Another aspect to be taken into account by following this solution is that if multiple users have offline changes that get concurrently synchronised, there is no warranty that the changes will be executed in the right chronological order on the server as Firestore uses a first come first served logic.

Foil answered 3/6, 2019 at 11:33 Comment(0)
I
2

According to https://w3c.github.io/ServiceWorker/#cache-put (point 4).

        if(request.method !== "GET") {
            return Promise.reject('no-match')
        }
Ioannina answered 5/8, 2019 at 17:54 Comment(0)
U
1

Although according to the accepted answer "You can't cache POST requests using the Cache API"... it seems that actually you can.

Maybe there are good reasons to avoid doing so, as a routine, because of the nature of POST requests... but if you have to, then it seems to be perfectly possible. In my case, I'd rather be using GET (with relevant info for the GET operation in the URL) but instead I'm having to post the relevant info via the body of a POST to avoid (sometimes) running into URL length limits. But effectively I'm just using POST as a workaround here, and in my eyes I'm still really using it as a GET, so it seems reasonable that in this situation I should be able to cache responses to my POST requests... just using a cache key based on what is in the POST body rather than what is in the GET URL.

All you have to do is to clone the POST request and convert it into a GET request... used the cloned GET request as the basis for caching in the service worker cache, but use the original POST request to get the response that is to be cached.

Something along the lines of the following:

  if (request.method.toUpperCase() === "POST") {
    // get the body text...
    const body = await request.clone().text();
    // create a new URL for the purposes of a cache key...
    const cacheUrl = new URL(request.url);
    // create an augmented URL by appending the body to the original pathname...
    cacheUrl.pathname = cacheUrl.pathname + body;
    // convert the request to a GET to be able to cache it...
    const cacheRequest = new Request(cacheUrl.toString(), {
      headers: request.headers,
      method: "GET"
    });
    // get cache...
    const cache = caches.default;
    // check if there is a cached response in the cache based on the cloned GET request (for the cache key) NOT the original POST request...
    let response = await cache.match(cacheRequest);
    // if not, fetch the response using the original POST request...
    if (!response) {
      response = await fetch(request);
      // put the response into the cache using the cloned GET request (for the cache key) NOT the original POST request...
      event.waitUntil(cache.put(cacheRequest, response.clone()));
    }
    return response;
  }

In my case I'm not actually passing a cloned request into the cache API, but instead just passing a string cache key, so it's not actually necessary to create a dummy GET request at all... I'm effectively just passing the derived string cacheUrl.pathname directly to cache.match() and cache.put()... they don't reject this as a POST request because it's just a string, not a request.

Unsay answered 6/5, 2023 at 14:15 Comment(1)
I've done almost the same, cloned the POST request, changed the method to GET, but cached the cloned request. It's a hack but it worked well for us.Prevost

© 2022 - 2024 — McMap. All rights reserved.