I've recently noticed a strange behaviour in Safari when injecting a content script with scripting.executeScript.

The expectation here is that an array of InjectionResult objects will be returned (one for each frame the script was executed in). In all other browsers our extension supports (Chrome, Firefox*, Edge, Brave and Opera) this is indeed the behaviour I've observed.

Safari however, at least on both 15 and 16 that I've been able to test, returns an array containing a null element. I have confirmed that the scripts execute as expected without error, but even if they did error the expectation would be to have the error field of an InjectionResult object populated.

Below are two simple scripts I used to confirm this across all of the mentioned browsers. I made sure to test using both a script expecting arguments and one without, just in case that was somehow the cause. I also tried both a void return and returning a primitive value from the scripts.

Manifest Version 3
const foo = await browser.scripting.executeScript({
    target: { tabId },
    func: () => {
        console.log('Hello, from an injected script! o/');
    },
});
console.log({ foo });

const bar = await browser.scripting.executeScript({
    target: { tabId },
    args: [1337],
    func: (param: number) => {
        console.log(`Hello, from an injected script! o/ With '${param}' argument!`);
        return param;
    },
});
console.log({ bar });
*Manifest Version 2 (for Firefox)
const foo = await browser.tabs.executeScript(tabId, {
    code: 'console.log("Hello, from an injected script! o/")',
});
console.log({ foo });

const param = 1337;
const bar = await browser.tabs.executeScript(tabId, {
    code: `console.log('Hello, from an injected script! o/ With "${param}" argument!'); ${param};`,
});
console.log({ bar });

The outputs for the console.log statements above are as follows :-

Chrome | Brave | Edge | Opera
foo: [{documentId: '...', frameId: 0, result: null}]
bar: [{documentId: '...', frameId: 0, result: 1337}]
*Firefox
foo: [undefined]
bar: [1337]
Safari
foo: [null]
bar: [null]

Since Safari's output more closely resembles that of Firefox under MV2, it feels like Safari supports MV3 but is still kind of stuck in this weird limbo between MV2.

Desperate attempt snippet

The observation of the above MV3/MV2 limbo led me to try drop the return statement, and instead just have param; as the last evaluated statement. In line with how returns work under MV2 in Firefox as follows:-

func: (param: number) => {
    console.log(`Hello, from an injected script! o/ With '${param}' argument!`);
    param;
},

This, thank goodness, didn't work.

This behaviour feels like it should almost surely be a bug in WebKit, or am I completely missing something?

2

There are 2 answers

0
bkrypt On BEST ANSWER

It looks like this was resolved, albeit undocumented, in a Safari 16.4.x release (either 16.4.0 or 16.4.1).

So to confirm, this now works across both ISOLATED and MAIN scripts:

await browser.scripting.executeScript({
    target: { tabId: /* tab ID here */ },
    world: 'ISOLATED', // or 'MAIN'
    args: [1337],
    func: (param) => {
        console.log(`Hello, from an injected script! o/ With '${param}' argument!`);
        return param;
    },
});

Resulting in the following now:

Chrome | Brave | Edge | Opera
[{documentId: '...', frameId: 0, result: 1337}]

Safari

[1337]

The shape of the return is still unaligned with the other browsers, but I'll take that over [null] any day.

1
AntonOfTheWoods On

This could be due to host_permissions not being set for the site you are trying to run on. I can confirm that, after significant hair-pulling, I was able to get my results back, even when running with world: "MAIN" on Safari 16.4.1 (I had bugs that stopped it working in previous versions, though might still have actually worked with them too).

However, remember that Safari doesn't return InjectionResults, it returns results directly. You should still be getting bar: [1337] though, so probably check your host_permissions.