I have a website which I don't want to make people create accounts. It is a news feed with each news article categorized. I want to allow people to tag the categories they are interested in so that next time they go to the site it only shows news for the categories that are tagged.
I'm saving the tags in an indexedDB which I understand is available in a service worker.
Hence in my service worker I want to "intercept" requests to www.my-url.com, check the indexDB for what categories this person is interested in, and add some headers like 'x-my-customer-header': 'technology,physics,sports' so that my server can respond with a dynamic html of those categories only.
However I'm struggling to get the service worker to properly cache my root response. In my serviceworker.js, I console log every event.request for the onFetch handler. There are no requests that are related to my root url. I'm testing right now on my localhost, but I only see fetch requests to css & js files.
Here is my onFetch:
function onFetch(event) {
console.log('onFetch',event.request.url);
event.request.headers["X-my-custom-header"] = "technology,sports";
event.respondWith(
// try to return untouched request from network first
fetch(event.request).catch(function() {
// if it fails, try to return request from the cache
caches.match(event.request).then(function(response) {
if (response) {
return response;
}
// if not found in cache, return default offline content for navigate requests
if (event.request.mode === 'navigate' ||
(event.request.method === 'GET' && event.request.headers.get('accept').includes('text/html'))) {
return caches.match('/offline.html');
}
})
})
);
}
I'm using rails so there is no index.html that exists to be cached, when a user hits my url, the page is dynamically served from my news#controller.
I'm actually using the gem serviceworker-rails
What am I doing wrong? How can I have my service worker save a root file and intercept the request to add headers? Is this even possible?
Credit here goes to Jeff Posnick for his answer on constructing a new Request. You'll need to respond with a fetch that creates a new Request to which you can add headers:
self.addEventListener('fetch', event => {
event.respondWith(customHeaderRequestFetch(event))
})
function customHeaderRequestFetch(event) {
// decide for yourself which values you provide to mode and credentials
// Copy existing headers
const headers = new Headers(event.request.headers);
// Set a new header
headers.set('x-my-custom-header', 'The Most Amazing Header Ever');
// Delete a header
headers.delete('x-request');
const newRequest = new Request(event.request, {
mode: 'cors',
credentials: 'omit',
headers: headers
})
return fetch(newRequest)
}
Related
So the other day, I asked this question about javascript webworkers:
Javascript Webworker how to put json information into array buffer. One of the answers I received was to use a SharedArrayBuffer to share memory between the main javascript and the webworker. I know that for a time, this was usable on microsoft edge, but for a security concern was disabled a while back. My edge version is 96.0.1054.62. Is there any way to enable using shared array buffers, in the browser configuration or settings? Currently, when I try to use it, it says that SharedArrayBuffer is undefined.
In order for Shared Array Buffer support to be enabled, your web page needs to be in a secure context. To do this, you need your server to give the following headers: Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp. You can read more about it on MDN
Changing the header on the server is the recommended way, but if you do not have the ability to manage headers on the server at all, then you can modify them through Service Workers. This blogpost describes enabling SharedArrayBuffer via header modification in ServiceWorker. It works in the following order:
When the page is loaded for the first time, a Service worker is registered
The page is reloaded
SharedArrayBuffer becomes available because ServiceWorker controls all CORS headers for all requests
Service Worker modifies all requests by adding CORS/COEP headers (The example is taken from the mentioned blogpost):
self.addEventListener("install", function() {
self.skipWaiting();
});
self.addEventListener("activate", (event) => {
event.waitUntil(self.clients.claim());
});
self.addEventListener("fetch", function(event) {
if (event.request.cache === "only-if-cached" && event.request.mode !== "same-origin") {
return;
}
event.respondWith(
fetch(event.request)
.then(function(response) {
// It seems like we only need to set the headers for index.html
// If you want to be on the safe side, comment this out
// if (!response.url.includes("index.html")) return response;
const newHeaders = new Headers(response.headers);
newHeaders.set("Cross-Origin-Embedder-Policy", "require-corp");
newHeaders.set("Cross-Origin-Opener-Policy", "same-origin");
const moddedResponse = new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: newHeaders,
});
return moddedResponse;
})
.catch(function(e) {
console.error(e);
})
);
});
I am trying to add a parameter to the body of a POST request in a service worker but the original body is send. I use the following code
let token = '';
self.addEventListener('message', function (event) {
if (event.data && event.data.type === 'SET_TOKEN') {
token = event.data.token;
}
});
self.addEventListener('fetch', function (event) {
const destURL = new URL(event.request.url);
const headers = new Headers(event.request.headers);
if (token) headers.append('Authorization', token);
if (destURL.pathname === '/logout/') {
const promiseChain = event.request.json().then((originalBody) => {
return fetch(event.request.url, {
method: event.request.method,
headers,
// this body is not send to the server but only the original body
body: JSON.stringify(Object.assign(originalBody, { token })),
});
});
event.respondWith(promiseChain);
return;
}
const authReq = new Request(event.request, {
headers,
mode: 'cors',
});
event.respondWith(fetch(authReq));
});
Generally speaking, that should work. Here's a very similar live example that you can run and confirm:
https://glitch.com/edit/#!/materialistic-meadow-rowboat?path=sw.js%3A18%3A7
It will just POST to https://httpbin.org/#/Anything/post_anything, which will in turn echo back the request body and headers.
If your code isn't working, I would suggest using that basic sample as a starting point and slowing customizing it with your own logic. Additionally, it would be a good idea to confirm that your service worker is properly in control of the client page when its makes that request. Using Chrome DevTool's debugger interface, you should be able to put breakpoints in your service worker's fetch event handler and confirm that everything is running as expected.
Taking a step back, you should make sure that your web app isn't coded in such a way that it requires the service worker to be in control in order to go things like expire auth tokens. It's fine to have special logic in the service worker to account for auth, but make sure your code paths work similarly when the service worker doesn't intercept requests, as might be the case when a user force-reloads a web page by holding down the Shift key.
I'm learning how SvelteKit works. There is a pretty nice "hello world PLUS" place to start with the "create-svelte" example. In that example there is a "todos" page. Source files here. It's a simple page where you can add an delete things to do tasks. And I'm trying to figure out how the individual tasks are managed.
I can see where an todos array is created in Javascript on the client; I can verify that via console.logs in index.svelte file. I can see via Chrome Inspect (dev tools, network tab) where the creation of a task generates a todos.json network Post request http://localhost:3000/todos.json The get/post request simply returns the original content back to the client.
What I'm not seeing is where the data is being stored on the browser. I was fully expecting to see something in local storage, session storage, cookies or even indexeddb.
What I'm not seeing is where that content is being stored. If I close the browser tab and reopen, the data is there. If I open a different browser on the same local computer, no task data exists. If I "empty cache and hard reload" the task data remains.
After doing some testing, I can see one cookie.
Name: userid Value: d38b9b44-1a8b-414e-9a85-1c2b2c0700f4 Domain: localhost Path: / Expires/MaxAge: Session Size: 42 HttpOnly: ✓ Priority: Medium
If I modify or delete this cookie in any way then the stored task data disappears.
So where is the todos task data being temporarily stored? What am I not seeing?
I read the header for this file _api.js multiple times. I tested "https://api.svelte.dev" in the browser and got back null responses.. I just assumed that this was a dead server.
It turns out that the folks at svelte do offer a fully functional api server, both receiving, deleting and storing todo task data. See my test notes within api function.
A browser request to https://api.svelte.dev/todos/d38b9b44-1a8b-414e-9a85-1c2b2c0700f4 absolutely returns my task data, and now I can see how this stuff works. Info offered here in case anybody else is wondering what's going on here.
Here is the complete _api.js file
/*
This module is used by the /todos.json and /todos/[uid].json
endpoints to make calls to api.svelte.dev, which stores todos
for each user. The leading underscore indicates that this is
a private module, _not_ an endpoint — visiting /todos/_api
will net you a 404 response.
(The data on the todo app will expire periodically; no
guarantees are made. Don't use it to organise your life.)
*/
const base = 'https://api.svelte.dev';
export async function api(request, resource, data) {
console.log("resource: ", resource);
// returns--> resource: todos/d38b9b44-1a8b-414e-9a85-1c2b2c0700f4
//https://api.svelte.dev/todos/d38b9b44-1a8b-414e-9a85-1c2b2c0700f4 <--- test this !!
// user must have a cookie set
if (!request.locals.userid) {
return { status: 401 };
}
const res = await fetch(`${base}/${resource}`, {
method: request.method,
headers: {
'content-type': 'application/json'
},
body: data && JSON.stringify(data)
});
// if the request came from a <form> submission, the browser's default
// behaviour is to show the URL corresponding to the form's "action"
// attribute. in those cases, we want to redirect them back to the
// /todos page, rather than showing the response
if (res.ok && request.method !== 'GET' && request.headers.accept !== 'application/json') {
return {
status: 303,
headers: {
location: '/todos'
}
};
}
return {
status: res.status,
body: await res.json()
};
}
I want to get the response headers of a cached response inside a service worker. The purpose of this is so that I can read a custom header called 'Modified' to see if it is necessary to fetch a new copy of the data by comparing it to the response headers of a 'HEAD' fetch for the same URL.
On install of the service worker, I populate a cache called v1::fundamentals with some responses. I then register a fetch listener which looks for the request in the cache and if its there, serves it. I then want to async update the cache with non-stale content but only if the 'Modified' header contains a newer timestamp than the one in the cache. In the simplified code below, I try to access the headers with headers.get() but I always get a null in return. Why is this?
When I look at the cache in Chrome devtools, the headers are very much there, I just can't get to them from within the service worker.
self.addEventListener('fetch', event => {
console.log('%c[SW] Fetch caught: ', 'color: #42d9f4', event.request.url);
// Let the browser do its default thing for non-GET requests.
if (event.request.method != 'GET') {
return;
} else {
// Prevent the default, and handle the request ourselves.
event.respondWith(async function() {
// Try to get the response from a cache.
const cache = await caches.open('v1::fundamentals');
const cachedResponse = await cache.match(event.request);
if (cachedResponse) {
// Try to get the headers
var cacheDate = cachedResponse.headers.get('Modified');
// Print header, returns 'null'
console.log(cacheDate);
event.waitUntil(cache.add(event.request));
return cachedResponse;
}
return fetch(event.request);
}());
}
});
I'm currently considering adding service workers to a Web app I'm building.
This app is, essentially, a collection manager. You can CRUD items of various types and they are usually tightly linked together (e.g. A hasMany B hasMany C).
sw-toolbox offers a toolbox.fastest handler which goes to the cache and then to the network (in 99% of the cases, cache will be faster), updating the cache in the background. What I'm wondering is how you can be notified that there's a new version of the page available. My intent is to show the cached version and, then, if the network fetch got a newer version, to suggest to the user to refresh the page in order to see the latest edits. I saw something in a YouTube video a while ago but the presenter gives no clue of how to deal with this.
Is that possible? Is there some event handler or promise that I could bind to the request so that I know when the newer version is retrieved? I would then post a message to the page to show a notification.
If not, I know I can use toolbox.networkFirst along with a reasonable timeout to make the pages available even on Lie-Fi, but it's not as good.
I just stumbled accross the Mozilla Service Worker Cookbooj, which includes more or less what I wanted: https://serviceworke.rs/strategy-cache-update-and-refresh.html
Here are the relevant parts (not my code: copied here for convenience).
Fetch methods for the worker
// On fetch, use cache but update the entry with the latest contents from the server.
self.addEventListener('fetch', function(evt) {
console.log('The service worker is serving the asset.');
// You can use respondWith() to answer ASAP…
evt.respondWith(fromCache(evt.request));
// ...and waitUntil() to prevent the worker to be killed until the cache is updated.
evt.waitUntil(
update(evt.request)
// Finally, send a message to the client to inform it about the resource is up to date.
.then(refresh)
);
});
// Open the cache where the assets were stored and search for the requested resource. Notice that in case of no matching, the promise still resolves but it does with undefined as value.
function fromCache(request) {
return caches.open(CACHE).then(function (cache) {
return cache.match(request);
});
}
// Update consists in opening the cache, performing a network request and storing the new response data.
function update(request) {
return caches.open(CACHE).then(function (cache) {
return fetch(request).then(function (response) {
return cache.put(request, response.clone()).then(function () {
return response;
});
});
});
}
// Sends a message to the clients.
function refresh(response) {
return self.clients.matchAll().then(function (clients) {
clients.forEach(function (client) {
// Encode which resource has been updated. By including the ETag the client can check if the content has changed.
var message = {
type: 'refresh',
url: response.url,
// Notice not all servers return the ETag header. If this is not provided you should use other cache headers or rely on your own means to check if the content has changed.
eTag: response.headers.get('ETag')
};
// Tell the client about the update.
client.postMessage(JSON.stringify(message));
});
});
}
Handling of the "resource was updated" message
navigator.serviceWorker.onmessage = function (evt) {
var message = JSON.parse(evt.data);
var isRefresh = message.type === 'refresh';
var isAsset = message.url.includes('asset');
var lastETag = localStorage.currentETag;
// ETag header usually contains the hash of the resource so it is a very effective way of check for fresh content.
var isNew = lastETag !== message.eTag;
if (isRefresh && isAsset && isNew) {
// Escape the first time (when there is no ETag yet)
if (lastETag) {
// Inform the user about the update.
notice.hidden = false;
}
//For teaching purposes, although this information is in the offline cache and it could be retrieved from the service worker, keeping track of the header in the localStorage keeps the implementation simple.
localStorage.currentETag = message.eTag;
}
};