safariwebkitbrowser-extensionsafari-web-extension

Why does `scripting.executeScript` in Safari return a `null` element array instead of `InjectionResult` element array?


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?


Solution

  • 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.