Extensions API broken in Mac Safari

August 23 2022 by Jeff Johnson

I'm frustrated because an important feature of the cross-platform WebExtensions API for web browser extensions has been broken in Mac Safari — and only in Mac Safari — for two years. It works everywhere else: Firefox, Google Chrome, other Chromium-based web browsers, and even in Mobile Safari! The feature I'm talking about is run_at document_start. If the value "run_at": "document_start" is specified for a content script in an extension's manifest file, then web pages will run the extension's content script during their "loading" state, as opposed to during their "interactive" or "complete" state. (See the Document.readyState API for more details.) This is crucial because with document_start, scripts are injected "before any other DOM is constructed or any other script is run." In order to achieve their functionality, some web browser extensions need to run their scripts before the web page gets a chance to run its scripts, otherwise it's too late.

One extension that needs document_start is my own StopTheScript, which allows site-specific blocking of both inline and external JavaScript in Safari. There's a working version of StopTheScript for Mobile Safari in the iOS App Store, but the document_start bug prevents StopTheScript from working properly in Mac Safari, and thus I can't release a Mac version. I've been sitting on the Mac version for two years just waiting for a bug fix in Mac Safari. I actually blogged about the bug two years ago, a year before Apple enabled Safari web extensions on iOS. I've personally discussed this bug with an Apple Safari engineer, and I've also filed a bug report (FB10033445) with Apple's Feedback Assistant. At this point I'm feeling despair that Apple might never fix it. The bug still exists in the current versions of Safari (15.6.1) and Safari Technology Preview (16.0).

You can download a sample Xcode project that demonstrates the bug. The project contains both Mac and iOS app targets. The web extension is located inside the folder "Shared (Extension)/Resources/" and is shared between the two apps. You can test the same extension in Firefox by opening the page about:debugging#/runtime/this-firefox and using the "Load Temporary Add-on" button. In Google Chrome, you can open the Extensions window, enable "Developer mode", and use the "Load unpacked" button.

Here are the steps to reproduce the bug in Mac Safari:

  1. Build and run the RunAtTest2 app.
  2. If you haven't already, open Safari Preferences to the Advanced pane, and enable "Show Develop menu in menu bar".
  3. Open the "Develop" menu, enable "Allow Unsigned Extensions" at the bottom, and enter your keychain password when prompted. (Otherwise Apple only allows Apple-signed extensions to run in Safari.)
  4. Open Safari Preferences to the Extensions pane, and enable the RunatAtTest2 extension.
  5. Select RunatAtTest2 in the extensions list, and enable "Always Allow on Every Website".
  6. Load any web page, such as apple.com.

The extension adds a red box in the upper left corner of each web page that displays the readyState of the page, as well as the number of child elements, if any, of the HTML <head> and <body> elements. When document_start works correctly, you should just see "loading" in the box, with no child elements. When it's buggy, you may see "interactive" or "complete", along with a number of children.

Appendix: Preload Top Hit

In Mobile Safari on iOS there's a related bug (FB9157626) that can cause run_at document_start to fail if Preload Top Hit is enabled in Safari Settings. Unfortunately, Preload Top Hit is enabled by default. Fortunately, you can easily disable Preload Top Hit, and the StopTheScript install instructions recommend disabling it.

Besides causing extension problems, Preload Top Hit is also privacy nightmare, as I've blogged about before, so everyone ought to disable it anyway.

Jeff Johnson (My apps, PayPal.Me)