Bug 252544 - Initial ServiceWorkerWindowClient in a Home Screen web app launched to handle notificationclick handler is inert for a short period
Summary: Initial ServiceWorkerWindowClient in a Home Screen web app launched to handle...
Status: RESOLVED FIXED
Alias: None
Product: WebKit
Classification: Unclassified
Component: WebKit API (show other bugs)
Version: Safari 16
Hardware: iPhone / iPad iOS 16
: P2 Normal
Assignee: Brady Eidson
URL:
Keywords: InRadar
Depends on:
Blocks:
 
Reported: 2023-02-19 04:15 PST by lehu
Modified: 2024-08-03 00:50 PDT (History)
6 users (show)

See Also:


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description lehu 2023-02-19 04:15:46 PST
Overview: 

I made a Home Screen web app, which register a service worker to handle the push events
triggered by the WebPush. And in the notificationclick callback function, I try to open
the web app calling the clients.openWindow() function. Then, I try to post some message
with the postMessage method. In my web app's js, I have registered the "message" event
handler for navigator.serviceWorker, which should be called when receive the message
from the notificationclick callback.

If the Home Screen web app is not open, it can be opened by touch the APNS message.
However, the web app's js can not receive the posted message from the service worker.

After it has been opened, the following push which will post message from the service
worker will be received.

Steps to Reproduce:

1) Upgrade iOS to 16.4 beta1

2) Open the following experimental features of Safari:
  a) Notifications
  b) Push API

3) Make a web app and add it to the Home Screen

4) Register Service Worker and handle the events

In the service worker:

async function openPushNotification(event) {
  event.notification.close();
  event.waitUntil((async () => {
    let w = await clients.openWindow(event.notification.data);
    w.postMessage(event.notification.data);
  })());
}
self.addEventListener("notificationclick", openPushNotification);

In the web app:

navigator.serviceWorker.addEventListener("message", (event) => {
  console.log(event.data);
  let wn = document.getElementById("web-notification");
  wn.innerHTML = JSON.stringify(event.data, null, " ");
});

5) Close the Home Screen web app.

6) Click the APNS notification.

Actual Results:

The "message" event callback will not triggered if the web app is not opened.

Expected Results:

Every time we click the WebPush notification, which will open the Home Screen
web app, it should always receive the post message sent by the notificationclick handler.

Build Date & Hardware:

iOS 16.4 (20E5212f) beta1

Additional Builds and Platforms:

iPhone SE 3rd

Additional Information:

I have tested the Firefox 110 and Safari 16.3 on macOS 13.2.1, both of which
support receiving the post message.
Comment 1 Radar WebKit Bug Importer 2023-02-20 10:13:32 PST
<rdar://problem/105684663>
Comment 2 Brady Eidson 2023-03-06 22:51:36 PST
I tried this out from a test app to debug.
It actually worked expected.
I not only verified the behavior of the web content, but also that the relevant WebKit code was working as expected.

One thing that tripped me up while trying to reproduce was some syntax errors that started out with direct copy-and-paste of your code.

Have you verified via Web Inspector that your JS code is working as expected and not throwing any errors?

If you're still having trouble with this, I think we'll need an actual end-to-end test to interact with - and not code snippets here - because I can verify at least one incarnation of the test works fine.
Comment 3 lehu 2023-03-06 23:16:34 PST
Hi, Brady,

First, thank you for testing this issue.

The code snippet may have syntax error. It is a little hard to paste all code needed to reproduce this bug. What I try hard to do is to demonstrate how the issue is, and the snippet is just a clue.

However, I have finished a complete demo. And its URL is

https://taoshu.in/web/push-demo/

In the demo page, there is a form to display the subscription. And at the bottom of that page, there is a section named "Web Push Custom Data". Every time we push some message with some custom data and click the notification, the custom data will be displayed in that section. But if the Home Screen web app have not booted, click the notification will not let the bottom display custom data.

And I also made a Push API. If you use httpie, here is the command needed to push some message:


```
http -f https://taoshu.in/+/push title=WebPush body="Ping from taoshu.in" data="娃哈哈😄" subs="web-push-subscription-data-in-json"
```

In the above command, the *data* argument is used to send custom data to web app. And the *subs* argument is used to set the web push subscription. You can just copy the subscription in the demo page form.

Feel free to comment this bug if you have any problem.

Thanks.
Comment 4 Brady Eidson 2023-03-07 10:47:15 PST
Inside `openPushNotification`, the Notification does have the data intact. So that's great.

But through direct debugging I can also see that ServiceWorkerClient.postMessage is never actually called.

Seems like some part of a promise chain or something is getting dropped in all of your async/await code, causing none of the postMessages to actually fire.

I'll try copying your notificationclick handler verbatim over to mine and go from there.
Comment 5 Brady Eidson 2023-03-07 14:28:43 PST
(In reply to Brady Eidson from comment #4)
> Inside `openPushNotification`, the Notification does have the data intact.
> So that's great.
> 
> But through direct debugging I can also see that
> ServiceWorkerClient.postMessage is never actually called.
> 
> Seems like some part of a promise chain or something is getting dropped in
> all of your async/await code, causing none of the postMessages to actually
> fire.
> 
> I'll try copying your notificationclick handler verbatim over to mine and go
> from there.

postMessage is never getting called because the call to focus() results in a rejected promise, mostly aborting the rest of your SW code.

I'm digging deeper in to why it rejects now, but you can also change your code to handle the rejection and then postMessage successfully.
Comment 6 Brady Eidson 2023-03-07 14:29:47 PST
Retitling:
If a Home Screen Web App is backgrounded and tries to focus() a ServiceWorkerWindowClient in a notificationclick handler, the focus() promise rejects
Comment 7 lehu 2023-03-07 16:55:05 PST
It seems not relate to the focus() call. I have tried

a) use try catch to wrap the focus() call
b) delete the focus() call totally

None of which works 😂
Comment 8 Brady Eidson 2023-03-07 18:29:26 PST
(In reply to lehu from comment #7)
> It seems not relate to the focus() call. I have tried
> 
> a) use try catch to wrap the focus() call
> b) delete the focus() call totally
> 
> None of which works 😂


I guarantee through direct debugging and observation that the focus call is failing.
You can wrap it and see that the promise rejects, instead of fulfills.

*BUT*, the issue causing focus() to fail very well might be causing later things to fail.
Comment 9 Brady Eidson 2023-03-07 18:48:53 PST
Setting aside postMessage not working, focus() is also not working.

Here's what happens:
1 - The app launches and starts a page load for the root URL of the web app - https://taoshu.in/web/push-demo/ in this case. This happens in web content process A.
2 - That load has a ScriptExecutionContextIdentifier associated with it. Importantly, this identifier is what the eventual Document *will* have, but that document does not exist yet.
3 - The networking process is asked to load https://taoshu.in/web/push-demo/. As part of that resource load it is given the context identifier. So the networking process remembers "There is a window client for this URL with this identifier.
4 - The service worker is fired up in web content process B to handle the notification click. This happens right away, before the main page document has really started loading.
5 - In the notification click handler, the SW does a matchAll on clients looking for window clients
6 - That matchAll goes to the networking process, which knows about the eventual client. So it returns that UUID
7 - Back in web content B, the SW has the results of the matchAll and tries to call focus() on the window client.
8 - That focus call is routed to the networking process based on UUID, which knows that the document *should be* in Web Content process A. So it forwards the call there
9 - In Web Content process A, the focus() attempt tries to look up the Document for that UUID and fails. Remember from step 2, the eventual Document that will be created will have that UUID, but that Document doesn't exist yet.
10 - The failure is routed to Networking, which then routes to Web Content B, and then rejects the focus() promise. Whoops!
11 - Almost immediately after that rejection, data *does* come in for the main page document, therefore *actually creating the Document object in Web Content A*, ensuring that a future focus() call would actually work.


Your experimenting suggests removing the focus call still causes failure. I'd need to see the exact code you changed it to to take a look.

(Note: Please do *not* change the live site I'm debugging with now! Make a second copy and mention it if that is something you'd like to do)

Whatever you changed it to may be affected by a very similar issue, or something different, but: the focus() issue is definitely real.

(FWIW, I still have a private test with postMessage working just fine, but it's simply different than yours)
Comment 10 lehu 2023-03-07 19:01:24 PST
I will not change the live demo site since now. 

And here is the current code of https://taoshu.in/web/push-demo/sw.js, in which I just commented out three lines of code.

function receivePushNotification(event) {
  console.log("[Service Worker] Push Received.");

  const { title, body, data } = event.data.json();

  const options = {
    body: body,
    data: data, 
  };

  event.waitUntil(self.registration.showNotification(title, options));
}

async function openPushNotification(event) {
  console.log("[Service Worker] Notification click Received.", event);

  event.notification.close();
  event.waitUntil((async () => {
    const allClients = await clients.matchAll({ type: 'window' });

    for (const client of allClients) {
      // if (!client.focused) {
      //   await client.focus().catch(e => { console.log(e) });
      // }
      client.postMessage(event.notification.data);
      return;
    }

    let url = event.target.registration.scope;
    let w = await clients.openWindow(url);
    w.postMessage(event.notification.data);
  })());
}

self.addEventListener("push", receivePushNotification);
self.addEventListener("notificationclick", openPushNotification);
Comment 11 Brady Eidson 2023-03-07 19:08:01 PST
Yup, this will run into the exact same issue as mentioned above.

(In reply to lehu from comment #10)
> 
> async function openPushNotification(event) {
>   console.log("[Service Worker] Notification click Received.", event);
> 
>   event.notification.close();
>   event.waitUntil((async () => {
>     const allClients = await clients.matchAll({ type: 'window' });
> 
>     for (const client of allClients) {
>       // if (!client.focused) {
>       //   await client.focus().catch(e => { console.log(e) });
>       // }
>       client.postMessage(event.notification.data);
>       return;
>     }
> ...

Because of the weird mismatch in Networking's idea of the truth with Web Content's idea of the truth, the matchAll() call returns a client...  But that client doesn't *ACTUALLY* exist yet. It is "just about to exist"

So it's a different way of triggering the exact same issue.
Comment 12 Brady Eidson 2023-03-07 19:08:25 PST
CC'ing Youenn and Chris (And I'll ping them on Slack)
Comment 13 Brady Eidson 2023-03-07 20:34:10 PST
Retitling:

Initial ServiceWorkerWindowClient in a Home Screen web app launched to handle notificationclick handler is inert for a short period

If your promise chain holds out long enough such that the document networking request actually gets a response, it will start working.
Comment 14 Brady Eidson 2023-03-07 20:35:48 PST
In my test case it's working because I'm postMessaging to a *new* client opened via clients.openWindow

If I attempted to matchAll and reuse an existing window client, I would fall into this same trap.
Comment 15 Brady Eidson 2023-03-08 09:39:08 PST
With ServiceWorkerClient and ServiceWorkerWindowClient combined, there are 3 APIs we need to handle in this case:
postMessage(), focus(), and navigate()

All 3 of these fail because the context's Document isn't fully live yet.

The navigate() steps - https://w3c.github.io/ServiceWorker/#client-navigate - clearly state:
"If browsingContext’s associated document is not fully active, queue a task to reject promise with a TypeError"

navigate() already fails today per the spec.


focus() is a little trickier - https://w3c.github.io/ServiceWorker/#client-focus - As you have to cross reference with some other specs and concepts.
But basically, since the document doesn't exist, focus will not be true after running focus steps, and therefore the promise will reject.

So focus() also fails correctly today!

Now we're back to postMessage().
We clearly intended to queue messages sent to a "not fully active" context and deliver them once it becomes active.

And we're failing that case here.

Let's fix.
Comment 16 lehu 2023-03-14 20:32:12 PDT
BTW, it seems iOS Safari does not play sound when showing notifications. Is it a intentional design?

I have checked the Safari on macos, it does not allow play sound as well.

Without the notification sound, it is had to make app like chat or social network based on standard web technologies.

What a sad.

PS

I also wrote an article about web push on ios 16.4

https://taoshu.in/web/push-on-ios.html
Comment 17 Brady Eidson 2023-03-22 20:55:07 PDT
Pull request: https://github.com/WebKit/WebKit/pull/11848
Comment 18 decademoon.bugzilla 2023-03-28 02:59:21 PDT
I am also experiencing a similar issue.

In my case, if the app is closed and a notification is clicked, then it seems the service worker doesn't actually handle the notificationclick event at all and just opens a window at the service worker origin URL. In this case, I cannot open the Safari devtools for the service worker running on my phone, it simply isn't listed in the Develop menu under my phone submenu, which is really strange. If I open the app manually then the service worker appears in the menu as expected. So basically I have no way of debugging this.
Comment 19 Brady Eidson 2023-03-30 17:17:21 PDT
(In reply to decademoon.bugzilla from comment #18)
> I am also experiencing a similar issue.
> 
> In my case, if the app is closed and a notification is clicked, then it
> seems the service worker doesn't actually handle the notificationclick event
> at all and just opens a window at the service worker origin URL. In this
> case, I cannot open the Safari devtools for the service worker running on my
> phone, it simply isn't listed in the Develop menu under my phone submenu,
> which is really strange. If I open the app manually then the service worker
> appears in the menu as expected. So basically I have no way of debugging
> this.

This bug report (and incoming fix) are well understood.

What you describe is almost certainly a different issue, and we need a new, clean bug report with steps to reproduce.
Comment 20 EWS 2023-04-07 09:42:39 PDT
Committed 262711@main (6833b7d7f7be): <https://commits.webkit.org/262711@main>

Reviewed commits have been landed. Closing PR #11848 and removing active labels.
Comment 21 EWS 2023-04-12 15:02:37 PDT
Committed 259548.634@safari-7615-branch (013db85952ab): <https://commits.webkit.org/259548.634@safari-7615-branch>

Reviewed commits have been landed. Closing PR #545 and removing active labels.
Comment 22 Peter 2023-06-07 05:08:55 PDT
Which iOs version is this fixed in?
Comment 23 youenn fablet 2023-06-07 06:03:25 PDT
(In reply to Peter from comment #22)
> Which iOs version is this fixed in?

Have you tried the latest iOS 16 and 17 betas?