4

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?

bkrypt
  • 83
  • 5
  • Running into exactly the same issue here. Have you managed to work around this somehow? – Florian Friedrich Feb 13 '23 at 08:33
  • Just found a workaround here: https://developer.apple.com/forums/thread/714225 Apparently, using `files` instead of `func` seems to work... The results are still returned directly (instead of an `InjectionResult` object), but at least _something_ is returned... – Florian Friedrich Feb 13 '23 at 08:39
  • Thanks for the suggestion. I unfortunately cannot use `files` in our case. My workaround was to essentially have wrapper code that is included only when targeting Safari, that explicitly uses `chrome.runtime.sendMessage` to return the result. I hate it. But it at least enabled me to have this Safari-specific wrapper code exist in its own single place, leaving all the existing **sane** code untouched. – bkrypt Feb 14 '23 at 09:39

2 Answers2

0

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.

AntonOfTheWoods
  • 809
  • 13
  • 17
  • Thanks for your input @AntonOfTheWoods. I was just on my way to update this question with an answer. It seems like this was an undocumented fix added sometime in 16.4.x (I'm assuming 16.4.1). I can confirm that prior to this, with all permissions in order, any return value was always received as `[null]`. But, even if the shape is still off, at least we get something now! So I can remove my gross workaround! \o/ – bkrypt Jun 02 '23 at 07:28
0

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.

bkrypt
  • 83
  • 5