Etiting Angular service worker for sharing files to PWA via share_target API

134 views Asked by At

The App

I'm building a PWA as a frontend for OpenAI's Whisper, using this API. The goal is to be able to share WhatsApp voice messages directly to the PWA to transcribe them. I am using Angular 16.2.2 with the @angular/pwa package.

Here is my codebase in its current state

The Problem

I cannot get the share_target to work. When I press share in WhatsApp, my PWA shows up correctly but when I select it, it opens and displays a HTTP ERROR 504 page.

According the answer to this question, all I need to do is replace the ngsw-worker.js with my own custom service worker which listens for the POST request and imports the ngsw-worker at the bottom. However, this seems to break the ngsw-worker somehow, giving me a 504 error.

The Relevant Code

custom-service-worker.js

(copied almost exactly from the answer in the post linked above)

self.addEventListener('fetch', event => {

  // Handle the share_target shares
  if (event.request.method === 'POST') {

    // Make sure we're only getting shares to the share-target route
    const path = event.request.url.split("/").pop();

    if(path === "share-target"){

        //Get the audio from the share request
        event.request.formData().then(formData => {

            // Find the correct client in order to share the results.
            const clientId = event.resultingClientId !== "" ? event.resultingClientId : event.clientId;
            self.clients.get(clientId).then(client => {

                // Get the audio
                const audio = formData.getAll('audio');
                // Send it to the client
                client.postMessage(
                    {
                        message: "newMedia",
                        audio: audio
                    }
                );
            });
        });
    }
  }
});

importScripts('./ngsw-worker.js');
manifest.webmanifest (excerpt)
"share_target": {
  "action": "/app/share-target",
  "method": "POST",
  "enctype": "multipart/form-data",
  "params": {
    "title": "name",
    "text": "description",
    "url": "link",
    "files": [
      {
        "name": "audio",
        "accept": ["audio/*", ".mp3", ".wav", ".ogg"]
      }
    ]
  }
}
angular.json (excerpt)
"assets": [
  "src/favicon.ico",
  "src/assets",
  "src/manifest.webmanifest",
  "src/custom-service-worker.js"
]
app.module.ts (excerpt)
@NgModule({
  declarations: [
    (...)
  ],
  imports: [
    (...),
    ServiceWorkerModule.register('custom-service-worker.js', {
      enabled: !isDevMode(),
      // Register the ServiceWorker as soon as the application is stable
      // or after 30 seconds (whichever comes first).
      registrationStrategy: 'registerWhenStable:30000'
    }),
    (...)
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
share-target.component.ts (excerpt)
ngOnInit(): void {

  navigator.serviceWorker.addEventListener('message', (event: MessageEvent) => {
    this.test_outputs.push("Got message \n" + JSON.stringify(event.data));
    if (event.data.hasOwnProperty('audio')) {
      this.api.transcribe(event.data).subscribe(res => {
        this.test_outputs.push("Got response \n" + JSON.stringify(res));
      });
    }
  });
}

What I tried

I tried importing the ngsw-worker at the top instead of the bottom of my custom service worker (as done here and here for example), but that resulted in the POST request created by the share_target API to be sent directly to the server, which resulted in my app showing nginx's 405 METHOD NOT ALLOWED error. This made me think that the problem here could somehow be connected to the Angular router, as it shouldn't try to load any html from the server in this situation per my understanding.

1

There are 1 answers

0
user23741559 On

I had the same problem when trying to share images to the application. I was able to solve the problem comparing my function with this MDN POST example. It seems the lack of a response to the fetch request intercepted in the eventListener caused the service worker to return 504 (Gateway Timeout).

The solution would be to send a Response to the fetch event received. Example from the link (comments removed):

self.addEventListener("fetch", (event) => {
  if (event.request.method !== "POST") {
    event.respondWith(fetch(event.request));
    return;
  }

  event.respondWith(
    (async () => {
      const formData = await event.request.formData();
      const link = formData.get("link") || "";

      const responseUrl = await saveBookmark(link);
      return Response.redirect(responseUrl, 303);
    })(),
  );
});

In the example event.respondWith returns a response, which will avoid the timeout. The Response is a redirect response which will redirect to "responseUrl", as the MDN page says that

The POST request is then ideally replied with an HTTP 303 See Other redirect to avoid multiple POST requests from being submitted if a page refresh was initiated by the user, for example.

I believe any response would avoid the error, but a redirect response should be the best option, considering the information above and that you will probably want ro redirect the user to the component where the data is going to be used.