16

I have this content script that downloads some binary data using XHR, which is sent later to the background script:

var self = this;
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.responseType = 'arraybuffer';
xhr.onload = function(e) {
  if (this.status == 200) {
     self.data = {
        data: xhr.response,
        contentType: xhr.getResponseHeader('Content-Type')
     };
  }
};
xhr.send();

... later ...
sendResponse({data: self.data});

After receiving this data in background script, I'd like to form another XHR request that uploads this binary data to my server, so I do:

var formData = new FormData();
var bb = new WebKitBlobBuilder();
bb.append(data.data);
formData.append("data", bb.getBlob(data.contentType));
var req = new XMLHttpRequest();
req.open("POST", serverUrl);
req.send(formData);

The problem is that the file uploaded to the server contains just this string: "[object Object]". I guess this happens because ArrayBuffer type is lost somehow while transferring it from content process to the background? How can I solve that?

Rob W
  • 341,306
  • 83
  • 791
  • 678
Michael Spector
  • 36,723
  • 6
  • 60
  • 88

2 Answers2

21

Messages passed between a Content Script and a background page are JSON-serialized.

If you want to transfer an ArrayBuffer object through a JSON-serialized channel, wrap the buffer in a view, before and after transferring.

I show an isolated example, so that the solution is generally applicable, and not just in your case. The example shows how to pass around ArrayBuffers and typed arrays, but the method can also be applied to File and Blob objects, by using the FileReader API.

// In your case: self.data = { data: new Uint8Array(xhr.response), ...
// Generic example:
var example = new ArrayBuffer(10);
var data = {
    // Create a view
    data: Array.apply(null, new Uint8Array(example)),
    contentType: 'x-an-example'
};

// Transport over a JSON-serialized channel. In your case: sendResponse
var transportData = JSON.stringify(data);
//"{"data":[0,0,0,0,0,0,0,0,0,0],"contentType":"x-an-example"}"

// At the receivers end. In your case: chrome.extension.onRequest
var receivedData = JSON.parse(transportData);

// data.data is an Object, NOT an ArrayBuffer or Uint8Array
receivedData.data = new Uint8Array(receivedData.data).buffer;
// Now, receivedData is the expected ArrayBuffer object

This solution has been tested successfully in Chrome 18 and Firefox.

  • new Uint8Array(xhr.response) is used to create a view of the ArrayBuffer, so that the individual bytes can be read.
  • Array.apply(null, <Uint8Array>) is used to create a plain array, using the keys from the Uint8Array view. This step reduces the size of the serialized message. WARNING: This method only works for small amounts of data. When the size of the typed array exceeds 125836, a RangeError will be thrown. If you need to handle large pieces of data, use other methods to do the conversion between typed arrays and plain arrays.

  • At the receivers end, the original buffer can be obtained by creating a new Uint8Array, and reading the buffer attribute.

Implementation in your Google Chrome extension:

// Part of the Content script
    self.data = {
        data: Array.apply(null, new Uint8Array(xhr.response)),
        contentType: xhr.getResponseHeader('Content-Type')
    };
...
sendResponse({data: self.data});

// Part of the background page
chrome.runtime.onMessage.addListener(function(data, sender, callback) {
    ...
    data.data = new Uint8Array(data.data).buffer;

Documentation

Community
  • 1
  • 1
Rob W
  • 341,306
  • 83
  • 791
  • 678
  • This `Array.apply(null, new Uint8Array(xhr.response))` doesn't work for big buffers (bigger than 150Kb) raising `Maximum call stack size exceeded`error. Even for small buffers probably it is better to pass binary strings that JS arrays. – Konstantin Smolyanin Sep 15 '13 at 15:42
  • 2
    For bigger arrays `Array.from(new Uint8Array(xhr.response))` worked for me. – Al Amin Nov 05 '20 at 06:37
4

For Chromium Extensions manifest v3, URL.createObjectURL() approach doesn't work anymore because it is prohibited in the service workers.

The best (easiest) way to pass data from a service worker to a content script (and vice-versa), is to convert the blob into a base64 representation.

const fetchBlob = async url => {
    const response = await fetch(url);
    const blob = await response.blob();
    const base64 = await convertBlobToBase64(blob);
    return base64;
};

const convertBlobToBase64 = blob => new Promise(resolve => {
    const reader = new FileReader();
    reader.readAsDataURL(blob);
    reader.onloadend = () => {
        const base64data = reader.result;
        resolve(base64data);
    };
});

Then send the base64 to the content script.

Service worker:

chrome.tabs.sendMessage(sender.tab.id, { type: "LOADED_FILE", base64: base64 });

Content script:

chrome.runtime.onMessage.addListener(async (request, sender) => {
    if (request.type == "LOADED_FILE" && sender.id == '<your_extension_id>') {
        // do anything you want with the data from the service worker.
        // e.g. convert it back to a blob
        const response = await fetch(request.base64);
        const blob = await response.blob();
    }
});
Matthew C
  • 616
  • 3
  • 10
  • 18
  • Can confirm, any other seemingly sensible approach is a dead end. Until we get an easy way to pass files to the background script, serializing it ourselves seems to be the way to go. – mukunda Jan 27 '23 at 02:56