我有一个带有 ServiceWorker 的 React 应用程序,它的目的是拦截某些请求(API 调用和资源获取)并添加一个带有适当 token 的 Authorization
header 。在大多数情况下,它工作得很好,但在某些情况下,service worker 不处理获取事件,导致 401 响应。
react 方面 ⚛️
我们有以下函数来注册 service worker 并等待它被激活:
function initializeServiceWorker() {
return new Promise((resolve, reject) => {
navigator
.serviceWorker
.register("/tom.js")
.then((registration) => {
// checks the state of the service worker until it's activated
// and move on with the then-chain
return pollUntil(
(registration) => registration.active?.state === "activated",
POLL_EVERY, // 40ms
registration,
POLL_TIMEOUT // 6000ms
);
})
.then((registration) => {
// here we send the authentication token to the service worker
// to be stored in the IndexedDB overriding the old one
// and persist across termination/reactivation
registration.postMessage(<NewAuthToken>{ type: "AUTH_TOKEN", token });
})
.catch(reject);
});
}
Service Worker 端 🤖
我们的 service worker 为 active、install、message 和 fetch 事件设置处理程序如下:
// eslint-disable-next-line no-restricted-globals
const worker = self as any as ServiceWorkerGlobalScope;
// NOTE: we refer to self as `worker` after performing a couple of type casts
worker.addEventListener("install", () => {
// this ensure that updates to the underlying service worker take effect
// immediately for both the current client and all other active clients
worker.skipWaiting();
});
worker.addEventListener("activate", async (event: ExtendableEvent) => {
// https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle#activate
// The first activation does not connect / intercept already running clients.
// "claim" claims any already running clients.
event.waitUntil(worker.clients.claim());
});
worker.addEventListener("message", (event: ExtendableMessageEvent) => {
// here we store the token to IndexedDB
});
worker.addEventListener("fetch", (event: FetchEvent) => {
event.respondWith(
(async function respond() {
try {
const token = await DB.getToken();
// no token - no party
// just send the request as is
if (!token) return fetch(event.request);
// going somewhere else?
// no need to add the token to the request
const sourceHost = new URL(worker.location.origin).host;
const destinationHost = new URL(event.request.url).host;
if (destinationHost !== sourceHost) {
return fetch(event.request);
}
// already authorized?
// ready to go as it is
if (event.request.headers.get("Authorization")) {
return fetch(event.request);
}
/**
* When the `destination` property of the request is empty, it means that the
* request is an API call, otherwise it would have contained the DOM element
* that initialized the request (image, script, style, etc.)
*
* In this case, we can smoothly add the token in the Headers.
*/
if (event.request.destination === "") {
const headers = new Headers(event.request.headers);
headers.append("Authorization", token);
const enhancedRequest = new Request(event.request, { headers });
return await fetch(enhancedRequest);
}
// when this get executed it means the browser wants
// the content of an <img> tag or some script or styling
// ... but this is not the point of this SO question ...
return requestResource(event.request, token);
} catch (error) {
// something horrible happen?
// let the original request to reach it's destination freely
return fetch(event.request);
}
})()
);
});
那么,问题是什么? 🤔
在执行我们的第一个 API 调用之前,我们等待 initializeServiceWorker
方法,如下所示:
// in an async function...
await initializeServiceWorker();
const homeModel = await api.environment.getClientSettings();
在某些情况下(谢天谢地,比例很小,如 <5%),getClientSettings
api 调用以 401 结束,没有添加 Authorization header 。
那么,这怎么可能呢? initializeServiceWorker
promise 在调用 api 之前解决并且 token 仍然不存在怎么可能?
这个初始化/service worker 代码有意义吗?你有没有看到任何已经从 6 只眼睛中漏过的重大或微妙的错误?我们是不是在做一些疯狂的事情? 🤷♂️
让我知道🤞
你可能想知道的奇怪事情 🤨
为什么要等到 service worker 被激活?
我们注意到,有时激活需要的时间比必要的多一点,我们确实需要等待它准备就绪,然后再继续我们的代码。
--
你怎么知道它根本没有拦截?
为了确定我们是否没有添加 Authorization
header 或者我们根本没有拦截请求,我们决定始终添加一个额外的 X-SW
header 并在后端跟踪失败与此新“标记” header 的存在之间的关系。
我们注意到,当我们收到 401 时,没有 X-SW
标记 header ,因此 service worker 根本没有触及这些请求。
注意:为清楚起见,已从之前的代码片段中删除了添加的标记 header 。
--
在 IndexedDB 中存储 token 和首次 API 调用之间是否存在竞争条件?
虽然这可能是个问题,但这里不是这种情况,因为请求甚至没有 X-SW
标记 header 。让我们假设 token 还没有出现在服务 worker 端,请求无论如何都应该有标记头,但我们跟踪了这一点,但事实并非如此……
--
您怎么知道 initializeServiceWorker
正在解析?
该调用以及 api 调用位于一个 try catch block 中,该 block 根据失败情况区分用户在失败时看到的内容。然后,我们知道初始化进行得很顺利,因为我们看到了 401 相关的错误消息。
最佳答案
据我所知,您的代码中有两个竞争条件。
首先,initializeServiceWorker()
的第一部分中的逻辑并未等待正确的事情。您正在轮询以试图找出已注册的服务 worker 何时被激活,但即使您调用 clients.claim()
,服务 worker 被激活和被激活之间也存在时间间隔当它控制范围内的所有客户端时。
如果您想确保所有网络请求都将触发服务 worker 的 fetch
处理程序,您应该等待的是有一个服务 worker 控制当前页面。还有更多背景in this issue ,它包含您可以使用的以下代码段:
const controlledPromise = new Promise((resolve) => {
if (navigator.serviceWorker.controller) {
resolve();
} else {
navigator.serviceWorker.addEventListener(
'controllerchange', () => resolve());
}
});
一旦 controlledPromise
解析,您就会知道来自当前页面的网络请求将触发受控制的 service worker 的 fetch
处理程序。
第二个问题是,一旦 message
事件在 service worker 上触发,您的 postMessage()
调用就会完成,但在 IndexedDB 操作实际完成之前。
您需要从服务 worker 向客户端页面发回一条消息,指示 IndexedDB 写入已完成。您可以使用 MessagePort
s 使用“vanilla”JavaScript 执行此操作, 但我建议使用 Comlink library相反,因为这将包含使用 postMessage()
和在更好的、基于 promise 的界面中协调 MessagePort
的细节。
关于javascript - Active ServiceWorker 并不总是拦截请求,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/69796320/