web-push icon indicating copy to clipboard operation
web-push copied to clipboard

Unsubscribed due to error DELIVERY_PERMISSION_DENIED

Open mhabsaoui opened this issue 1 year ago • 0 comments
trafficstars

NOTE: Please test in a least two browsers (i.e. Chrome and Firefox). This helps with diagnosing problems quicker.

Setup

Operating System: Linux (Ubuntu 22.04 LTS) Node Version: 21.5.0 web-push Version: ^3.6.6

  • [x] Chrome
  • [x] Other

Chrome : Version 121.0.6167.160 (Official Build) (64-bit) Edge: Version 121.0.2277.106 (Official build) (64-bit)

Problem

We have a published a Chrome web addon (on Chrome Store) and we wanted to send Push notifications to users having installed on their browser.

So, to add this new feature:

  • On BO, I used the web-push library in our hosted NodeJS server (Express + MongoDB) to be able to subscribe all our users' addon instances (through FCM Vapid keys). The mongoDB persists all subscriptions JSON objects ({endpoint, keys})

  • On FO, i.e. on our web addon, we are relying on a registered ServiceWorker (which will act as a proxy between Push server and web addon) listening to "push" events from server.

In a normal scenario, all works like a charm and the notifications are gracefully poping on desktop.

Now, when I decide to deactivate my web addon (i.e. from Extensions manage page in Dev Mode), and my ServiceWorker remains registered, and I ignit a notification Push from server, I encounter errors on both sides :

  • on BO, this WebPushError saying that the 'push subscription has unsubscribed or expired
WebPushError: Received unexpected response code
    at IncomingMessage.<anonymous> (/app/node_modules/web-push/src/web-push-lib.js:378:20)
    at IncomingMessage.emit (node:events:530:35)
    at endReadableNT (node:internal/streams/readable:1696:12)
    at processTicksAndRejections (node:internal/process/task_queues:82:21) {
  statusCode: 410,
  headers: {
    'content-type': 'text/plain; charset=utf-8',
    'x-content-type-options': 'nosniff',
    'x-frame-options': 'SAMEORIGIN',
    'x-xss-protection': '0',
    date: 'Thu, 08 Feb 2024 16:37:48 GMT',
    'content-length': '47',
    'alt-svc': 'h3=":443"; ma=2592000,h3-29=":443"; ma=2592000'
  },
  body: 'push subscription has unsubscribed or expired.\n',
  endpoint: 'https://fcm.googleapis.com/fcm/send/f7MxvCg_.....
  • on FO side, I get this error on "Application - push messaging" menu in devtools, saying Unsubscribed due to error - DELIVERY_PERMISSION_DENIED

Screenshot from 2024-02-09 16-42-32

So, from the push server point of view I understand the problem because the subscription has been unsubscribed by the PushManager associated with the ServiceWorker.

But I don't understand why this unsubscription happens ?? Is it because the ServiceWorker can't dispatch the push message to the deactivated addon, without a subscription's endpoint, and this causes the unsubscription due to this error ??

Expected

To have the push dispatched and have the notification, as usual when the web addon is activated.

Features Used

  • [x] VAPID Support
  • [x] Sending with Payload

Example / Reproduce Case

  • ServiceWorker.js (addon)
// Event listener for push event.
self.addEventListener("push", (event) => {
  console.debug("Push event", event);
  const { title = "Test title", body = "Test message." } = event.data?.json();
  const icon = "icons/icon_1024x1024.png";
  event.waitUntil(
    self?.registration?.showNotification(title, {
      body,
      icon,
    }) // Show a notification with title and message
  ); // Keep the service worker alive until the notification is created
});

self.addEventListener("pushsubscriptionchange", (event) => {
  console.debug(event);
});

  • background-script.js (addon)
export const registerServiceWorker = async () => {
  try {
    await navigator?.serviceWorker.register("./serviceWorker.js"); // Register a Service Worker
  } catch (error) {
    console.error(`Service worker registration failed: ${error}`);
  }
};

export const subscribeToNotification = async () => {
  await navigator?.serviceWorker?.ready
    .then(async (registration) => {
      const subscription = await registration?.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY),
      });
      if (subscription) {
        const subscriptionJson = subscription.toJSON();
        currentBrowser.runtime.setUninstallURL(
          `${SERVER_URL}/subscription/remove/${subscriptionJson.keys.auth}`,
          () =>
            console.debug(
              "UninstallURL",
              `${SERVER_URL}/subscription/remove/${subscriptionJson.keys.auth}`
            )
        ); // Set uninstall URL to remove notification subscription on addon uninstall 
        await fetchApi({
          url: `${SERVER_URL}/subscription`,
          reqInit: {
            body: JSON.stringify(subscription.toJSON()),
            method: "POST",
            headers: {
              "Content-Type": "application/json",
            },
          },
        });
      }
      registration.active.onstatechange = (event) =>
        console.info(`SW state change: ${event}`); // ServiceWorker state change event
      registration.active.onerror = (event) =>
        console.error(`SW error: ${event}`); // ServiceWorker error event
    })
    .catch((error) => console.error(error));
};
  • subscriptionRouter.js (server)
  subscriptionRouter
  .post('/subscription', async (req, res) => {
    try {
      await (await db())
        ?.collection(`${SUBSCRIPTION_DB_COLL_NAME}`)
        ?.insertOne(req.body);
      res.status(201).json({ message: 'Subscription created' });
    } catch (error) {
      console.error(error);
      res.json({ message: 'Subscription creation error', error });
    }
  })
  • notificationRouter.js (server)
notificationRouter
 .get('/notification/push', async (req, res) => {
    try {
      const notifications =
        (await (await db())
          ?.collection(`${NOTIFICATION_DB_COLL_NAME}`)
          ?.find({})
          .toArray()) ?? [];
      const subscriptions = await getSubscriptions();
      if (notifications?.length > 0 && subscriptions?.length > 0) {
        subscriptions?.map(async ({ endpoint, keys }) => {
          console.debug({
            endpoint,
            keys,
          });
          await webPush
            .sendNotification(
              { endpoint, keys },
              JSON.stringify(notifications.pop()),
              pushOptions
            )
            .then((response) => {
              console.debug(response);
              res.json({
                message: `Notification Push done for ${subscriptions?.length} subscriptions`,
              });
            })
            .catch((error) => {
              console.error(error);
              if (error?.statusCode === 410) deleteSubscription(keys?.auth);
            });
        });
      } else
        res.json({
          message: `Notification Push not done because no notification exists`,
        });
    } catch (error) {
      console.error(error);
      res.json({ message: `Notification push error`, error });
    }
  });

Other

I have tried to debug using either ServiceWorker registration onerror or pushsubscriptionchange events, but in vain...

mhabsaoui avatar Feb 09 '24 17:02 mhabsaoui