onUpdateCheckComplete failed to parse update manifest

I’m seeing the following error in browser console when I attempt to update an add-on I develop:

addons.update-checker	WARN	onUpdateCheckComplete failed to parse update manifest: [Exception... "Update manifest is missing a required addons property."  nsresult: "0x80004005 (NS_ERROR_FAILURE)"  location: "JS frame :: resource://gre/modules/addons/AddonUpdateChecker.jsm :: getRequiredProperty :: line 145"  data: no] Stack trace: getRequiredProperty()@resource://gre/modules/addons/AddonUpdateChecker.jsm:145
parseJSONManifest()@resource://gre/modules/addons/AddonUpdateChecker.jsm:161
onLoad()@resource://gre/modules/addons/AddonUpdateChecker.jsm:339
UpdateParser/<()@resource://gre/modules/addons/AddonUpdateChecker.jsm:282

More details:

I develop a Firefox add-on. It is hosted and deployed manually. As such, my add-on’s manifest includes a browser_specific_settings.gecko.update_url property, that points to a publicly accessible URL that serves a JSON file containing links to my add-on’s version updates.

This file has single property, addons.[MY_EXTENSION_ID].updates. This is an array of objects, each containing a version, and an update_link. Each version is simply a SEMVER version string; each update_link is a link to a publicly accessible .xpi file for that version of the extension. These files are signed by uploading to https://addons.mozilla.org/.

I’ve used all this without issue for over a year to successfully automatically update add-on users to the latest version of the add-on.

However, now it no longer works, and I can’t see why. The error says “Update manifest is missing a required addons property.” Things I’ve checked and confirmed:

  • the updates.json field is valid JSON
  • the extension ID matches the ID on https://addons.mozilla.org/
  • the updates.json is served as JSON, with an application/json content-type
  • I can manually load the .xpi file and have it work without issue
  • the .xpi file is served as binary data, and the browser will download it directly if you visit the URL.

Any help is appreciated—thank you!