I'm working on StopTheMadness for iOS, and I ran into my first bug already. The bug involves the Safari setting Preload Top Hit, which I blogged about recently. I encourage you to disable Preload Top Hit, but unfortunately that setting is enabled by default in Safari on both iOS and macOS, so my extension can't avoid it. The bug is that Safari's Preload Top Hit breaks run_at document_start
for injected content scripts in web extensions.
The default value of run_at
is document_idle
, but a web extension can specify "run_at":"document_start"
in its manifest.json
file to override the default. The cross-platform web extensions API that Safari now uses is based on Google Chrome extensions, and the best explanation of document_start
is in the Chrome developer documentation: "Scripts are injected after any files from css
, but before any other DOM is constructed or any other script is run." When an extension content script is run at document start, the web page is nothing but an empty <html>
document. At that point there's not even a <head>
or <body>
element yet. This is crucial, because it allows StopTheMadness to take precedence over any of the scripts in the web page, which are the source of the "madness" that my extension is trying to stop.
StopTheMadness for Google Chrome on macOS specifies "run_at":"document_start"
in its manifest.json
file, and that always works as expected. StopTheMadness for Safari on macOS is a Safari app extension, an Apple-specific extension format that preceded Safari's adoption of the cross-platform web extension format. Nonetheless, Safari app extensions are very similar to web extensions in many ways. Safari app extensions can inject content scripts into web pages, and the scripts are always run at document start, exactly like a Chrome extension with "run_at":"document_start"
. Thus, I've never experienced this problem with StopTheMadness and Preload Top Hit until I started porting the extension to iOS.
To demonstrate the bug, I've created a sample Xcode project that you can download. The project contains an iOS app with a Safari web extension and a macOS app with a Safari app extension. You may need to set DEVELOPMENT_TEAM
in the project to your team in order to build the apps. The macOS extension is there just to demonstrate how it ought to work on iOS, if there wasn't a bug. Running the iOS app requires the Xcode 13 beta version with the iOS 15 SDK, but you should be able to run the Mac app with an earlier version of Xcode.
There's an interesting twist to the Preload Top Hit bug: opening the Safari web inspector causes the behavior to change, and the bug may no longer occur. That's the worst kind of bug, one that disappears when you're trying to debug! This is why my sample extension adds a visual indicator to the web page instead of logging to the web inspector console. If you're trying to reproduce the bug, make sure to keep the Safari web inspector closed.
To reproduce the bug, build and run the iOS app from Xcode. Then switch to the Settings app and enable the Safari extension for all websites.
Now Switch to Safari, and start typing in the Safari address bar until you see a dropdown list with Top Hits.
Select the top hit to open the web page. The red box in the upper left corner is added by the PreloadBug extension. The status, in this case "complete" is the document.readyState
, and the numbers represent the number of child elements of the HTML head and body.
Now disable the Preload Top Hit setting and try again. What you see is the expected state, "loading", and there are no numbers, because the HTML head and body do not exist yet. This is also what you'd see with the PreloadBug extension in Safari on macOS.
I've reported this bug to Apple as FB9157626 in Feedback Assistant. Hopefully this blog post is helpful to other extension developers in porting their extensions to Safari on iOS, and also to the public in explaining what issues we face in trying to develop your favorite extensions! And as a reminder, everyone should disable Preload Top Hit, because it's also a privacy problem.