Bug 1380278 - UpdateUtils.getLocale to Fetch API for async I/O. r?florian, r?whimboo draft
authorZibi Braniecki <zbraniecki@mozilla.com>
Wed, 12 Jul 2017 10:34:23 -0700
changeset 609038 a67f4cd015ee2c496a1621dc3d8eead335b35922
parent 607669 03bcd6d65af62c5e60a0ab9247ccce43885e707b
child 637489 c941b3e404f67c0c1e553ad653e449cc7bfb6c93
push id68489
push userbmo:gandalf@aviary.pl
push dateFri, 14 Jul 2017 17:15:24 +0000
reviewersflorian, whimboo
bugs1380278
milestone56.0a1
Bug 1380278 - UpdateUtils.getLocale to Fetch API for async I/O. r?florian, r?whimboo The NetUtils sync I/O shows up in the profile logs, so we want to switch the function that loads `update.locale` file to be async. MozReview-Commit-ID: AEYKiivsNl0
testing/marionette/puppeteer/firefox/firefox_puppeteer/api/software_update.py
toolkit/modules/GMPInstallManager.jsm
toolkit/modules/UpdateUtils.jsm
toolkit/modules/tests/xpcshell/test_GMPInstallManager.js
toolkit/modules/tests/xpcshell/test_UpdateUtils_url.js
toolkit/mozapps/extensions/internal/XPIProvider.jsm
toolkit/mozapps/update/nsUpdateService.js
--- a/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/software_update.py
+++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/software_update.py
@@ -371,20 +371,27 @@ class SoftwareUpdate(BaseLib):
 
     def get_formatted_update_url(self, force=False):
         """Retrieve the formatted AUS update URL the update snippet is retrieved from.
 
         :param force: Boolean flag to force an update check
 
         :returns: The URL of the update snippet
         """
-        # Format the URL by replacing placeholders
-        url = self.marionette.execute_script("""
-          Components.utils.import("resource://gre/modules/UpdateUtils.jsm")
-          return UpdateUtils.formatUpdateURL(arguments[0]);
+        url = self.marionette.execute_async_script("""
+          Components.utils.import("resource://gre/modules/UpdateUtils.jsm");
+          let res = UpdateUtils.formatUpdateURL(arguments[0]);
+          // Format the URL by replacing placeholders
+          // In 56 we switched the method to be async.
+          // For now, support both approaches.
+          if (res.then) {
+            res.then(marionetteScriptFinished);
+          } else {
+            marionetteScriptFinished(res);
+          }
         """, script_args=[self.update_url])
 
         if force:
             if '?' in url:
                 url += '&'
             else:
                 url += '?'
             url += 'force=1'
--- a/toolkit/modules/GMPInstallManager.jsm
+++ b/toolkit/modules/GMPInstallManager.jsm
@@ -54,79 +54,79 @@ function GMPInstallManager() {
 }
 /**
  * Temp file name used for downloading
  */
 GMPInstallManager.prototype = {
   /**
    * Obtains a URL with replacement of vars
    */
-  _getURL() {
+  async _getURL() {
     let log = getScopedLogger("GMPInstallManager._getURL");
     // Use the override URL if it is specified.  The override URL is just like
     // the normal URL but it does not check the cert.
     let url = GMPPrefs.getString(GMPPrefs.KEY_URL_OVERRIDE, "");
     if (url) {
       log.info("Using override url: " + url);
     } else {
       url = GMPPrefs.getString(GMPPrefs.KEY_URL);
       log.info("Using url: " + url);
     }
 
-    url = UpdateUtils.formatUpdateURL(url);
+    url = await UpdateUtils.formatUpdateURL(url);
 
     log.info("Using url (with replacement): " + url);
     return url;
   },
   /**
    * Performs an addon check.
    * @return a promise which will be resolved or rejected.
    *         The promise is resolved with an object with properties:
    *           gmpAddons: array of GMPAddons
    *           usedFallback: whether the data was collected from online or
    *                         from fallback data within the build
    *         The promise is rejected with an object with properties:
    *           target: The XHR request object
    *           status: The HTTP status code
    *           type: Sometimes specifies type of rejection
    */
-  checkForAddons() {
+  async checkForAddons() {
     let log = getScopedLogger("GMPInstallManager.checkForAddons");
     if (this._deferred) {
         log.error("checkForAddons already called");
         return Promise.reject({type: "alreadycalled"});
     }
     this._deferred = PromiseUtils.defer();
-    let url = this._getURL();
 
     let allowNonBuiltIn = true;
     let certs = null;
     if (!Services.prefs.prefHasUserValue(GMPPrefs.KEY_URL_OVERRIDE)) {
       allowNonBuiltIn = !GMPPrefs.getString(GMPPrefs.KEY_CERT_REQUIREBUILTIN, true);
       if (GMPPrefs.getBool(GMPPrefs.KEY_CERT_CHECKATTRS, true)) {
         certs = gCertUtils.readCertPrefs(GMPPrefs.KEY_CERTS_BRANCH);
       }
     }
 
+    let url = await this._getURL();
+
     let addonPromise = ProductAddonChecker
-      .getProductAddonList(url, allowNonBuiltIn, certs);
+        .getProductAddonList(url, allowNonBuiltIn, certs);
 
     addonPromise.then(res => {
       if (!res || !res.gmpAddons) {
         this._deferred.resolve({gmpAddons: []});
       } else {
         res.gmpAddons = res.gmpAddons.map(a => new GMPAddon(a));
         this._deferred.resolve(res);
       }
       delete this._deferred;
     }, (ex) => {
       this._deferred.reject(ex);
       delete this._deferred;
     });
-
     return this._deferred.promise;
   },
   /**
    * Installs the specified addon and calls a callback when done.
    * @param gmpAddon The GMPAddon object to install
    * @return a promise which will be resolved or rejected
    *         The promise will resolve with an array of paths that were extracted
    *         The promise will reject with an error object:
--- a/toolkit/modules/UpdateUtils.jsm
+++ b/toolkit/modules/UpdateUtils.jsm
@@ -4,26 +4,28 @@
 
 this.EXPORTED_SYMBOLS = ["UpdateUtils"];
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 Cu.import("resource://gre/modules/AppConstants.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource://gre/modules/ctypes.jsm");
+Cu.importGlobalProperties(["fetch"]); /* globals fetch */
 
 const FILE_UPDATE_LOCALE                  = "update.locale";
 const PREF_APP_DISTRIBUTION               = "distribution.id";
 const PREF_APP_DISTRIBUTION_VERSION       = "distribution.version";
 const PREF_APP_UPDATE_CUSTOM              = "app.update.custom";
 
 
 this.UpdateUtils = {
+  _locale: undefined,
+
   /**
    * Read the update channel from defaults only.  We do this to ensure that
    * the channel is tightly coupled with the application and does not apply
    * to other instances of the application that may use the same profile.
    *
    * @param [optional] aIncludePartners
    *        Whether or not to include the partner bits. Default: true.
    */
@@ -56,83 +58,87 @@ this.UpdateUtils = {
   /**
    * Formats a URL by replacing %...% values with OS, build and locale specific
    * values.
    *
    * @param  url
    *         The URL to format.
    * @return The formatted URL.
    */
-  formatUpdateURL(url) {
+  async formatUpdateURL(url) {
+    const locale = await this.getLocale();
+
     return url.replace(/%(\w+)%/g, (match, name) => {
       switch (name) {
         case "PRODUCT":
           return Services.appinfo.name;
         case "VERSION":
           return Services.appinfo.version;
         case "BUILD_ID":
           return Services.appinfo.appBuildID;
         case "BUILD_TARGET":
           return Services.appinfo.OS + "_" + this.ABI;
         case "OS_VERSION":
           return this.OSVersion;
         case "LOCALE":
-          return this.Locale;
+          return locale;
         case "CHANNEL":
           return this.UpdateChannel;
         case "PLATFORM_VERSION":
           return Services.appinfo.platformVersion;
         case "SYSTEM_CAPABILITIES":
           return getSystemCapabilities();
         case "CUSTOM":
           return Services.prefs.getStringPref(PREF_APP_UPDATE_CUSTOM, "");
         case "DISTRIBUTION":
           return getDistributionPrefValue(PREF_APP_DISTRIBUTION);
         case "DISTRIBUTION_VERSION":
           return getDistributionPrefValue(PREF_APP_DISTRIBUTION_VERSION);
       }
       return match;
     }).replace(/\+/g, "%2B");
+  },
+
+  /**
+   * Gets the locale from the update.locale file for replacing %LOCALE% in the
+   * update url. The update.locale file can be located in the application
+   * directory or the GRE directory with preference given to it being located in
+   * the application directory.
+   */
+  async getLocale() {
+    if (this._locale !== undefined) {
+      return this._locale;
+    }
+
+    for (let res of ["app", "gre"]) {
+      const url = "resource://" + res + "/" + FILE_UPDATE_LOCALE;
+      let data;
+      try {
+        data = await fetch(url);
+      } catch (e) {
+        continue;
+      }
+      const locale = await data.text();
+      if (locale) {
+        return this._locale = locale.trim();
+      }
+    }
+
+    Cu.reportError(FILE_UPDATE_LOCALE + " file doesn't exist in either the " +
+                   "application or GRE directories");
+
+    return this._locale = null;
   }
 };
 
 /* Get the distribution pref values, from defaults only */
 function getDistributionPrefValue(aPrefName) {
   return Services.prefs.getDefaultBranch(null).getCharPref(aPrefName, "default");
 }
 
-/**
- * Gets the locale from the update.locale file for replacing %LOCALE% in the
- * update url. The update.locale file can be located in the application
- * directory or the GRE directory with preference given to it being located in
- * the application directory.
- */
-XPCOMUtils.defineLazyGetter(UpdateUtils, "Locale", function() {
-  let channel;
-  let locale;
-  for (let res of ["app", "gre"]) {
-    channel = NetUtil.newChannel({
-      uri: "resource://" + res + "/" + FILE_UPDATE_LOCALE,
-      contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_XMLHTTPREQUEST,
-      loadUsingSystemPrincipal: true
-    });
-    try {
-      let inputStream = channel.open2();
-      locale = NetUtil.readInputStreamToString(inputStream, inputStream.available());
-    } catch (e) {}
-    if (locale)
-      return locale.trim();
-  }
-
-  Cu.reportError(FILE_UPDATE_LOCALE + " file doesn't exist in either the " +
-                 "application or GRE directories");
-
-  return null;
-});
-
 function getSystemCapabilities() {
   return gInstructionSet + "," + getMemoryMB();
 }
 
 /**
  * Gets the RAM size in megabytes. This will round the value because sysinfo
  * doesn't always provide RAM in multiples of 1024.
  */
--- a/toolkit/modules/tests/xpcshell/test_GMPInstallManager.js
+++ b/toolkit/modules/tests/xpcshell/test_GMPInstallManager.js
@@ -2,16 +2,17 @@
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 var {classes: Cc, interfaces: Ci, results: Cr, utils: Cu, manager: Cm} = Components;
 const URL_HOST = "http://localhost";
 
 var GMPScope = Cu.import("resource://gre/modules/GMPInstallManager.jsm", {});
 var GMPInstallManager = GMPScope.GMPInstallManager;
 
+Cu.import("resource://gre/modules/Timer.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://testing-common/httpd.js");
 Cu.import("resource://gre/modules/Preferences.jsm")
 Cu.import("resource://gre/modules/UpdateUtils.jsm");
 
 var ProductAddonCheckerScope = Cu.import("resource://gre/modules/addons/ProductAddonChecker.jsm", {});
@@ -141,17 +142,26 @@ add_test(function test_checkForAddons_40
 
 /**
  * Tests that a xhr abort() works as expected
  */
 add_test(function test_checkForAddons_abort() {
   let overriddenXhr = overrideXHR(200, "", { dropRequest: true} );
   let installManager = new GMPInstallManager();
   let promise = installManager.checkForAddons();
-  overriddenXhr.abort();
+
+  // Since the XHR is created in checkForAddons asynchronously,
+  // we need to delay aborting till the XHR is running.
+  // Since checkForAddons returns a Promise already only after
+  // the abort is triggered, we can't use that, and instead
+  // we'll use a fake timer.
+  setTimeout(() => {
+    overriddenXhr.abort();
+  }, 100);
+
   promise.then(res => {
     do_check_true(res.usedFallback);
     installManager.uninit();
     run_next_test();
   });
 });
 
 /**
--- a/toolkit/modules/tests/xpcshell/test_UpdateUtils_url.js
+++ b/toolkit/modules/tests/xpcshell/test_UpdateUtils_url.js
@@ -177,39 +177,39 @@ function getMemoryMB() {
   } catch (e) {
     do_throw("Error getting system info memsize property. Exception: " + e);
   }
   return memoryMB;
 }
 
 // Helper function for formatting a url and getting the result we're
 // interested in
-function getResult(url) {
-  url = UpdateUtils.formatUpdateURL(url);
+async function getResult(url) {
+  url = await UpdateUtils.formatUpdateURL(url);
   return url.substr(URL_PREFIX.length).split("/")[0];
 }
 
 // url constructed with %PRODUCT%
 add_task(async function test_product() {
   let url = URL_PREFIX + "%PRODUCT%/";
-  Assert.equal(getResult(url), gAppInfo.name,
+  Assert.equal(await getResult(url), gAppInfo.name,
                "the url param for %PRODUCT%" + MSG_SHOULD_EQUAL);
 });
 
 // url constructed with %VERSION%
 add_task(async function test_version() {
   let url = URL_PREFIX + "%VERSION%/";
-  Assert.equal(getResult(url), gAppInfo.version,
+  Assert.equal(await getResult(url), gAppInfo.version,
                "the url param for %VERSION%" + MSG_SHOULD_EQUAL);
 });
 
 // url constructed with %BUILD_ID%
 add_task(async function test_build_id() {
   let url = URL_PREFIX + "%BUILD_ID%/";
-  Assert.equal(getResult(url), gAppInfo.appBuildID,
+  Assert.equal(await getResult(url), gAppInfo.appBuildID,
                "the url param for %BUILD_ID%" + MSG_SHOULD_EQUAL);
 });
 
 // url constructed with %BUILD_TARGET%
 // XXX TODO - it might be nice if we tested the actual ABI
 add_task(async function test_build_target() {
   let url = URL_PREFIX + "%BUILD_TARGET%/";
 
@@ -230,57 +230,57 @@ add_task(async function test_build_targe
     if (macutils.isUniversalBinary) {
       abi += "-u-" + macutils.architecturesInBinary;
     }
   } else if (AppConstants.platform == "win") {
     // Windows build should report the CPU architecture that it's running on.
     abi += "-" + getProcArchitecture();
   }
 
-  Assert.equal(getResult(url), gAppInfo.OS + "_" + abi,
+  Assert.equal(await getResult(url), gAppInfo.OS + "_" + abi,
                "the url param for %BUILD_TARGET%" + MSG_SHOULD_EQUAL);
 });
 
 // url constructed with %LOCALE%
 // Bug 488936 added the update.locale file that stores the update locale
 add_task(async function test_locale() {
   // The code that gets the locale accesses the profile which is only available
   // after calling do_get_profile in xpcshell tests. This prevents an error from
   // being logged.
   do_get_profile();
 
   let url = URL_PREFIX + "%LOCALE%/";
-  Assert.equal(getResult(url), AppConstants.INSTALL_LOCALE,
+  Assert.equal(await getResult(url), AppConstants.INSTALL_LOCALE,
                "the url param for %LOCALE%" + MSG_SHOULD_EQUAL);
 });
 
 // url constructed with %CHANNEL%
 add_task(async function test_channel() {
   let url = URL_PREFIX + "%CHANNEL%/";
   setUpdateChannel("test_channel");
-  Assert.equal(getResult(url), "test_channel",
+  Assert.equal(await getResult(url), "test_channel",
                "the url param for %CHANNEL%" + MSG_SHOULD_EQUAL);
 });
 
 // url constructed with %CHANNEL% with distribution partners
 add_task(async function test_channel_distribution() {
   let url = URL_PREFIX + "%CHANNEL%/";
   gDefaultPrefBranch.setCharPref(PREF_APP_PARTNER_BRANCH + "test_partner1",
                                  "test_partner1");
   gDefaultPrefBranch.setCharPref(PREF_APP_PARTNER_BRANCH + "test_partner2",
                                  "test_partner2");
-  Assert.equal(getResult(url),
+  Assert.equal(await getResult(url),
                "test_channel-cck-test_partner1-test_partner2",
                "the url param for %CHANNEL%" + MSG_SHOULD_EQUAL);
 });
 
 // url constructed with %PLATFORM_VERSION%
 add_task(async function test_platform_version() {
   let url = URL_PREFIX + "%PLATFORM_VERSION%/";
-  Assert.equal(getResult(url), gAppInfo.platformVersion,
+  Assert.equal(await getResult(url), gAppInfo.platformVersion,
                "the url param for %PLATFORM_VERSION%" + MSG_SHOULD_EQUAL);
 });
 
 // url constructed with %OS_VERSION%
 add_task(async function test_os_version() {
   let url = URL_PREFIX + "%OS_VERSION%/";
   let osVersion;
   let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2);
@@ -310,42 +310,42 @@ add_task(async function test_os_version(
       osVersion += " (" + sysInfo.getProperty("secondaryLibrary") + ")";
     } catch (e) {
       // Not all platforms have a secondary widget library, so an error is
       // nothing to worry about.
     }
     osVersion = encodeURIComponent(osVersion);
   }
 
-  Assert.equal(getResult(url), osVersion,
+  Assert.equal(await getResult(url), osVersion,
                "the url param for %OS_VERSION%" + MSG_SHOULD_EQUAL);
 });
 
 // url constructed with %DISTRIBUTION%
 add_task(async function test_distribution() {
   let url = URL_PREFIX + "%DISTRIBUTION%/";
   gDefaultPrefBranch.setCharPref(PREF_DISTRIBUTION_ID, "test_distro");
-  Assert.equal(getResult(url), "test_distro",
+  Assert.equal(await getResult(url), "test_distro",
                "the url param for %DISTRIBUTION%" + MSG_SHOULD_EQUAL);
 });
 
 // url constructed with %DISTRIBUTION_VERSION%
 add_task(async function test_distribution_version() {
   let url = URL_PREFIX + "%DISTRIBUTION_VERSION%/";
   gDefaultPrefBranch.setCharPref(PREF_DISTRIBUTION_VERSION, "test_distro_version");
-  Assert.equal(getResult(url), "test_distro_version",
+  Assert.equal(await getResult(url), "test_distro_version",
                "the url param for %DISTRIBUTION_VERSION%" + MSG_SHOULD_EQUAL);
 });
 
 add_task(async function test_custom() {
   Services.prefs.setCharPref("app.update.custom", "custom");
   let url = URL_PREFIX + "%CUSTOM%/";
-  Assert.equal(getResult(url), "custom",
+  Assert.equal(await getResult(url), "custom",
                "the url query string for %CUSTOM%" + MSG_SHOULD_EQUAL);
 });
 
 // url constructed with %SYSTEM_CAPABILITIES%
 add_task(async function test_systemCapabilities() {
   let url = URL_PREFIX + "%SYSTEM_CAPABILITIES%/";
   let systemCapabilities = getInstructionSet() + "," + getMemoryMB();
-  Assert.equal(getResult(url), systemCapabilities,
+  Assert.equal(await getResult(url), systemCapabilities,
                "the url param for %SYSTEM_CAPABILITIES%" + MSG_SHOULD_EQUAL);
 });
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -2498,17 +2498,17 @@ this.XPIProvider = {
 
     // Download the list of system add-ons
     let url = Preferences.get(PREF_SYSTEM_ADDON_UPDATE_URL, null);
     if (!url) {
       await systemAddonLocation.cleanDirectories();
       return;
     }
 
-    url = UpdateUtils.formatUpdateURL(url);
+    url = await UpdateUtils.formatUpdateURL(url);
 
     logger.info(`Starting system add-on update check from ${url}.`);
     let res = await ProductAddonChecker.getProductAddonList(url);
 
     // If there was no list then do nothing.
     if (!res || !res.gmpAddons) {
       logger.info("No system add-ons list was returned.");
       await systemAddonLocation.cleanDirectories();
--- a/toolkit/mozapps/update/nsUpdateService.js
+++ b/toolkit/mozapps/update/nsUpdateService.js
@@ -2026,41 +2026,40 @@ UpdateService.prototype = {
         AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_IS_DOWNLOADED);
       } else {
         AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_IS_STAGED);
       }
       return;
     }
 
     let validUpdateURL = true;
-    try {
-      this.backgroundChecker.getUpdateURL(false);
-    } catch (e) {
+    this.backgroundChecker.getUpdateURL(false).catch(e => {
       validUpdateURL = false;
-    }
-    // The following checks are done here so they can be differentiated from
-    // foreground checks.
-    if (!UpdateUtils.OSVersion) {
-      AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_NO_OS_VERSION);
-    } else if (!UpdateUtils.ABI) {
-      AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_NO_OS_ABI);
-    } else if (!validUpdateURL) {
-      AUSTLMY.pingCheckCode(this._pingSuffix,
-                            AUSTLMY.CHK_INVALID_DEFAULT_URL);
-    } else if (!getPref("getBoolPref", PREF_APP_UPDATE_ENABLED, true)) {
-      AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_PREF_DISABLED);
-    } else if (!hasUpdateMutex()) {
-      AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_NO_MUTEX);
-    } else if (!gCanCheckForUpdates) {
-      AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_UNABLE_TO_CHECK);
-    } else if (!this.backgroundChecker._enabled) {
-      AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_DISABLED_FOR_SESSION);
-    }
-
-    this.backgroundChecker.checkForUpdates(this, false);
+    }).then(() => {
+      // The following checks are done here so they can be differentiated from
+      // foreground checks.
+      if (!UpdateUtils.OSVersion) {
+        AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_NO_OS_VERSION);
+      } else if (!UpdateUtils.ABI) {
+        AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_NO_OS_ABI);
+      } else if (!validUpdateURL) {
+        AUSTLMY.pingCheckCode(this._pingSuffix,
+                              AUSTLMY.CHK_INVALID_DEFAULT_URL);
+      } else if (!getPref("getBoolPref", PREF_APP_UPDATE_ENABLED, true)) {
+        AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_PREF_DISABLED);
+      } else if (!hasUpdateMutex()) {
+        AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_NO_MUTEX);
+      } else if (!gCanCheckForUpdates) {
+        AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_UNABLE_TO_CHECK);
+      } else if (!this.backgroundChecker._enabled) {
+        AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_DISABLED_FOR_SESSION);
+      }
+
+      this.backgroundChecker.checkForUpdates(this, false);
+    });
   },
 
   /**
    * Determine the update from the specified updates that should be offered.
    * If both valid major and minor updates are available the minor update will
    * be offered.
    * @param   updates
    *          An array of available nsIUpdate items
@@ -2813,76 +2812,83 @@ Checker.prototype = {
    * The nsIUpdateCheckListener callback
    */
   _callback: null,
 
   /**
    * The URL of the update service XML file to connect to that contains details
    * about available updates.
    */
-  getUpdateURL: function UC_getUpdateURL(force) {
+  getUpdateURL: async function UC_getUpdateURL(force) {
     this._forced = force;
 
     let url = Services.prefs.getDefaultBranch(null).
               getCharPref(PREF_APP_UPDATE_URL, "");
 
     if (!url) {
       LOG("Checker:getUpdateURL - update URL not defined");
       return null;
     }
 
-    url = UpdateUtils.formatUpdateURL(url);
+    url = await UpdateUtils.formatUpdateURL(url);
 
     if (force) {
       url += (url.indexOf("?") != -1 ? "&" : "?") + "force=1";
     }
 
     LOG("Checker:getUpdateURL - update URL: " + url);
     return url;
   },
 
   /**
    * See nsIUpdateService.idl
    */
   checkForUpdates: function UC_checkForUpdates(listener, force) {
     LOG("Checker: checkForUpdates, force: " + force);
-    if (!listener)
+    if (!listener) {
       throw Cr.NS_ERROR_NULL_POINTER;
-
-    var url = this.getUpdateURL(force);
-    if (!url || (!this.enabled && !force))
+    }
+
+    if (!this.enabled && !force) {
       return;
-
-    this._request = new XMLHttpRequest();
-    this._request.open("GET", url, true);
-    this._request.channel.notificationCallbacks = new gCertUtils.BadCertHandler(false);
-    // Prevent the request from reading from the cache.
-    this._request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
-    // Prevent the request from writing to the cache.
-    this._request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
-    // Disable cutting edge features, like TLS 1.3, where middleboxes might brick us
-    this._request.channel.QueryInterface(Ci.nsIHttpChannelInternal).beConservative = true;
-
-    this._request.overrideMimeType("text/xml");
-    // The Cache-Control header is only interpreted by proxies and the
-    // final destination. It does not help if a resource is already
-    // cached locally.
-    this._request.setRequestHeader("Cache-Control", "no-cache");
-    // HTTP/1.0 servers might not implement Cache-Control and
-    // might only implement Pragma: no-cache
-    this._request.setRequestHeader("Pragma", "no-cache");
-
-    var self = this;
-    this._request.addEventListener("error", function(event) { self.onError(event); });
-    this._request.addEventListener("load", function(event) { self.onLoad(event); });
-
-    LOG("Checker:checkForUpdates - sending request to: " + url);
-    this._request.send(null);
-
-    this._callback = listener;
+    }
+
+    this.getUpdateURL(force).then(url => {
+      if (!url) {
+        return;
+      }
+
+      this._request = new XMLHttpRequest();
+      this._request.open("GET", url, true);
+      this._request.channel.notificationCallbacks = new gCertUtils.BadCertHandler(false);
+      // Prevent the request from reading from the cache.
+      this._request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
+      // Prevent the request from writing to the cache.
+      this._request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
+      // Disable cutting edge features, like TLS 1.3, where middleboxes might brick us
+      this._request.channel.QueryInterface(Ci.nsIHttpChannelInternal).beConservative = true;
+
+      this._request.overrideMimeType("text/xml");
+      // The Cache-Control header is only interpreted by proxies and the
+      // final destination. It does not help if a resource is already
+      // cached locally.
+      this._request.setRequestHeader("Cache-Control", "no-cache");
+      // HTTP/1.0 servers might not implement Cache-Control and
+      // might only implement Pragma: no-cache
+      this._request.setRequestHeader("Pragma", "no-cache");
+
+      var self = this;
+      this._request.addEventListener("error", function(event) { self.onError(event); });
+      this._request.addEventListener("load", function(event) { self.onLoad(event); });
+
+      LOG("Checker:checkForUpdates - sending request to: " + url);
+      this._request.send(null);
+
+      this._callback = listener;
+    });
   },
 
   /**
    * Returns an array of nsIUpdate objects discovered by the update check.
    * @throws if the XML document element node name is not updates.
    */
   get _updates() {
     var updatesElement = this._request.responseXML.documentElement;
@@ -2908,17 +2914,17 @@ Checker.prototype = {
       updateElement.QueryInterface(Ci.nsIDOMElement);
       let update;
       try {
         update = new Update(updateElement);
       } catch (e) {
         LOG("Checker:_updates get - invalid <update/>, ignoring...");
         continue;
       }
-      update.serviceURL = this.getUpdateURL(this._forced);
+      update.serviceURL = this._request.responseURL;
       update.channel = UpdateUtils.UpdateChannel;
       updates.push(update);
     }
 
     return updates;
   },
 
   /**