Sending a fetched file from service worker to content script

533 views
Skip to first unread message

Moe Bazzi

unread,
Sep 1, 2022, 11:56:38 AM9/1/22
to Chromium Extensions
Hey everyone,

This is my use case: I am fetching a file from a remote URL in the background service worker, and want to pass that file (ie. its contents) to a content script. In MV2, this was done by setting the fetched blob to a blob URL (via URL.createObjectURL) in the bg script, then having the content script fetch that blob URL, but in MV3, service workers dont have access to the URL.createObjectURL API. So what do you think is the best way to send the file contents to the content script? Currently, I'm breaking up the file into chunks and sending them chunk by chunk via messaging. Are there better methods of doing this ?

Thanks

T S

unread,
Sep 2, 2022, 6:35:36 AM9/2/22
to Chromium Extensions, bazz...@gmail.com
Content scripts can access chrome.storage, so maybe temporarily saving it in storage and sending an identifier is an option here?

wOxxOm

unread,
Sep 2, 2022, 10:44:35 AM9/2/22
to Chromium Extensions, T S, bazz...@gmail.com
Both in MV2 and MV3 the fastest method of delivering a large Blob is to add an iframe to the web page (exposed in web_accessible_resources), then postMessage from SW through iframe into the content script. Ownership transfer is instantaneous e.g. a 1GB Blob in 1ms.

There may be existing examples and libraries, try looking.
Here's my quick untested proof-of-concept demo.
  • content script usage example

    bgFetch(url).then(r => r.blob()).then(blob => { /* do something */ })
    bgFetch(url, {headers: {Authorization: 'basic foo'}}).then(.........)

  • content script

    var reqs, host, shadow, iframe, channel;
    /**
     * @param {string} url
     * @param {RequestInit} [init]
     * @return {Promise<Request>}
     */
    async function bgFetch(url, init) {
      const id = Math.random();
      const {stack} = new Error();
      const {headers} = init || {};
      if (headers instanceof Headers) {
        init = {...init, headers: Object.fromEntries(headers)};
      }
      if (!iframe?.contentWindow) {
        await initFrame();
      }
      return new Promise((resolve, reject) => {
        reqs[id] = {resolve, reject, stack};
        channel.port1.postMessage({id, url, init});
        // TODO: autocreate `transfer` from `init`
      });
    }
    async function initFrame() {
      if (reqs) Object.values(reqs).forEach(req => req.reject('iframeKilled'));
      reqs = {};
      host = document.createElement('div');
      shadow = host.attachShadow({mode: 'closed'});
      (shadow.adoptedStyleSheets = [new CSSStyleSheet()])
        [0].replaceSync(':host { display: none !important }');
      iframe = shadow.appendChild(document.createElement('iframe'));
      iframe.src = chrome.runtime.getURL('sender.html?id=' + Math.random());
      shadow.appendChild(iframe);
      (document.body || document.documentElement).appendChild(host);
      await (new Promise(resolve => (iframe.onload = resolve)));
      channel = new MessageChannel();
      channel.port1.onmessage = onFrameMessage;
      iframe.contentWindow.postMessage(iframe.src, '*', [channel.port2]);
    }
    function onFrameMessage(e) {
      const {id, result, error} = e.data;
      const {resolve, reject, stack} = reqs[id];
      delete reqs[id];
      if (error) reject(Object.assign(new Error(error), {stack}));
      else resolve(new Response(...result));
    }


  • iframe.html

    <script src=iframe.js></script>

  • iframe.js

    let swPort, tabPort;
    window.onmessage = e => {
      if (e.data === location.href) {
        tabPort = e.ports[0];
        tabPort.onmessage = onTabMessage;
      }
    };
    async function onTabMessage(e) {
      const swr = await navigator.serviceWorker.ready;
      const [port1, port2] = new MessageChannel();
      port1.onmessage = onSWMessage;
      swr.active.postMessage(e.data, [port2]);
      // TODO: autocreate `transfer` from `e.data`
    }
    async function onSWMessage(e) {
      tabPort.postMessage(e.data, [e.data.result?.[0]]);
    }

  • service worker

    // sw
    self.onmessage = async e => {
      const {id, url, init} = e.data;
      try {
        const req = await fetch(url, init);
        const buf = await req.arrayBuffer();
        const {headers, statusText, status} = req;
        const bufInit = {headers: Object.fromEntries(headers), status, statusText};
        const result = [buf, bufInit];
        e.ports[0].postMessage({id, result}, [buf]);
      } catch (err) {
        e.ports[0].postMessage({id, error: err.message});
      }
    };

  • manifest.json

      "web_accessible_resources": [{
        "resources": ["iframe.html"],
        "matches": ["<all_urls>"],
        "use_dynamic_url": true
      }]

Moe Bazzi

unread,
Sep 4, 2022, 11:10:46 AM9/4/22
to wOxxOm, Chromium Extensions, T S
Wow thank you so much! I completely forgot about owner transfership of the array buffer. Will use and test this, thanks again ! 
Reply all
Reply to author
Forward
0 new messages