commit 2c518d40a8da389566b749fb397430f43b54eb58
parent c7f62c9b514cc7ae291f9d91270142f658e0d1ff
Author: Alex Cottner <acottner@mozilla.com>
Date: Mon, 29 Dec 2025 16:39:08 +0000
Bug 2007208 - Adding v2 service support for remote-settings client r=leplatrem
Adding v2 service support for remote-settings client.
Removing quotes around _expected timestamps for remote-settings service calls.
This will match up with what the v2 reader endpoints are expecting.
v1 reader endpoints are being made forwards compatible.
Updated remote-settings constants to allow release to switch between v1 and v2.
Updated tests.
Differential Revision: https://phabricator.services.mozilla.com/D277258
Diffstat:
12 files changed, 97 insertions(+), 77 deletions(-)
diff --git a/services/settings/RemoteSettingsClient.sys.mjs b/services/settings/RemoteSettingsClient.sys.mjs
@@ -1153,7 +1153,7 @@ export class RemoteSettingsClient extends EventEmitter {
const hasLocalData = localTimestamp !== null;
const { retry = false } = options;
// On retry, we fully re-fetch the collection (no `?_since`).
- const since = retry || !hasLocalData ? undefined : `"${localTimestamp}"`;
+ const since = retry || !hasLocalData ? undefined : localTimestamp;
// Define an executor that will verify the signature of the local data.
const verifySignatureLocalData = (resolve, reject) => {
diff --git a/services/settings/SyncHistory.sys.mjs b/services/settings/SyncHistory.sys.mjs
@@ -30,20 +30,19 @@ export class SyncHistory {
}
/**
- * Store the synchronization status. The ETag is converted and stored as
+ * Store the synchronization status. The timestamp is converted and stored as
* a millisecond epoch timestamp.
* The entries with the oldest timestamps will be deleted to maintain the
* history size under the configured maximum.
*
- * @param {string} etag the ETag value from the server (eg. `"1647961052593"`)
+ * @param {int} timestamp the timestamp value from the server (eg. 1647961052593)
* @param {string} status the synchronization status (eg. `"success"`)
* @param {object} infos optional additional information to keep track of
*/
- async store(etag, status, infos = {}) {
+ async store(timestamp, status, infos = {}) {
const rkv = await this.#init();
- const timestamp = parseInt(etag.replace('"', ""), 10);
- if (Number.isNaN(timestamp)) {
- throw new Error(`Invalid ETag value ${etag}`);
+ if (!Number.isInteger(timestamp)) {
+ throw new Error(`Invalid timestamp value ${timestamp}`);
}
const key = `v1-${this.source}\t${timestamp}`;
const value = { timestamp, status, infos };
@@ -51,8 +50,8 @@ export class SyncHistory {
// Trim old entries.
const allEntries = await this.list();
for (let i = this.size; i < allEntries.length; i++) {
- let { timestamp } = allEntries[i];
- await rkv.delete(`v1-${this.source}\t${timestamp}`);
+ let { timestamp: entryTimestamp } = allEntries[i];
+ await rkv.delete(`v1-${this.source}\t${entryTimestamp}`);
}
}
diff --git a/services/settings/Utils.sys.mjs b/services/settings/Utils.sys.mjs
@@ -50,7 +50,7 @@ ChromeUtils.defineLazyGetter(lazy, "isRunningTests", () => {
// Overriding the server URL is normally disabled on Beta and Release channels,
// except under some conditions.
-ChromeUtils.defineLazyGetter(lazy, "allowServerURLOverride", () => {
+ChromeUtils.defineLazyGetter(lazy, "allowServerURL", () => {
if (!AppConstants.RELEASE_OR_BETA) {
// Always allow to override the server URL on Nightly/DevEdition.
return true;
@@ -65,13 +65,14 @@ ChromeUtils.defineLazyGetter(lazy, "allowServerURLOverride", () => {
return true;
}
- if (lazy.gServerURL != AppConstants.REMOTE_SETTINGS_SERVER_URL) {
- log.warn("Ignoring preference override of remote settings server");
- log.warn(
- "Allow by setting MOZ_REMOTE_SETTINGS_DEVTOOLS=1 in the environment"
- );
+ if (AppConstants.REMOTE_SETTINGS_SERVER_URLS.includes(lazy.gServerURL)) {
+ return true;
}
+ log.warn("Ignoring preference override of remote settings server");
+ log.warn(
+ "Allow by setting MOZ_REMOTE_SETTINGS_DEVTOOLS=1 in the environment"
+ );
return false;
});
@@ -79,7 +80,7 @@ XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"gServerURL",
"services.settings.server",
- AppConstants.REMOTE_SETTINGS_SERVER_URL
+ AppConstants.REMOTE_SETTINGS_SERVER_URLS
);
XPCOMUtils.defineLazyPreferenceGetter(
@@ -97,9 +98,9 @@ const _cdnURLs = {};
export var Utils = {
get SERVER_URL() {
- return lazy.allowServerURLOverride
+ return lazy.allowServerURL
? lazy.gServerURL
- : AppConstants.REMOTE_SETTINGS_SERVER_URL;
+ : AppConstants.REMOTE_SETTINGS_SERVER_URLS[0];
},
CHANGES_PATH: "/buckets/monitor/collections/changes/changeset",
@@ -139,7 +140,7 @@ export var Utils = {
get LOAD_DUMPS() {
// Load dumps only if pulling data from the production server, or in tests.
return (
- this.SERVER_URL == AppConstants.REMOTE_SETTINGS_SERVER_URL ||
+ AppConstants.REMOTE_SETTINGS_SERVER_URLS.includes(this.SERVER_URL) ||
lazy.isRunningTests
);
},
@@ -147,7 +148,7 @@ export var Utils = {
get PREVIEW_MODE() {
// We want to offer the ability to set preview mode via a preference
// for consumers who want to pull from the preview bucket on startup.
- if (_isUndefined(this._previewModeEnabled) && lazy.allowServerURLOverride) {
+ if (_isUndefined(this._previewModeEnabled) && lazy.allowServerURL) {
return lazy.gPreviewEnabled;
}
return !!this._previewModeEnabled;
@@ -490,7 +491,7 @@ export var Utils = {
return {
changes,
- currentEtag: `"${timestamp}"`,
+ timestamp,
serverTimeMillis,
backoffSeconds,
ageSeconds,
diff --git a/services/settings/remote-settings.sys.mjs b/services/settings/remote-settings.sys.mjs
@@ -482,13 +482,8 @@ function remoteSettingsFunction() {
throw new Error(`Polling for changes failed: ${e.message}.`);
}
- const {
- serverTimeMillis,
- changes,
- currentEtag,
- backoffSeconds,
- ageSeconds,
- } = pollResult;
+ const { serverTimeMillis, changes, timestamp, backoffSeconds, ageSeconds } =
+ pollResult;
// Report age of server data in Telemetry.
pollTelemetryArgs = { age: ageSeconds, ...pollTelemetryArgs };
@@ -565,7 +560,7 @@ function remoteSettingsFunction() {
const syncTelemetryArgs = {
source: TELEMETRY_SOURCE_SYNC,
duration: durationMilliseconds,
- timestamp: `${currentEtag}`,
+ timestamp,
trigger,
};
@@ -579,7 +574,7 @@ function remoteSettingsFunction() {
);
// Keep track of sync failure in history.
await lazy.gSyncHistory
- .store(currentEtag, status, {
+ .store(timestamp, status, {
expectedTimestamp,
errorName: firstError.name,
})
@@ -611,7 +606,7 @@ function remoteSettingsFunction() {
}
// Save current Etag for next poll.
- lazy.gPrefs.setStringPref(PREF_SETTINGS_LAST_ETAG, currentEtag);
+ lazy.gPrefs.setStringPref(PREF_SETTINGS_LAST_ETAG, timestamp);
// Report the global synchronization success.
const status = lazy.UptakeTelemetry.STATUS.SUCCESS;
@@ -622,7 +617,7 @@ function remoteSettingsFunction() {
);
// Keep track of sync success in history.
await lazy.gSyncHistory
- .store(currentEtag, status)
+ .store(timestamp, status)
.catch(error => console.error(error));
lazy.console.info(
@@ -662,7 +657,7 @@ function remoteSettingsFunction() {
if (!localOnly) {
// Make sure we fetch the latest server info, use a random cache bust value.
const randomCacheBust = 99990000 + Math.floor(Math.random() * 9999);
- ({ changes, currentEtag: serverTimestamp } =
+ ({ changes, timestamp: serverTimestamp } =
await lazy.Utils.fetchLatestChanges(lazy.Utils.SERVER_URL, {
expected: randomCacheBust,
}));
@@ -795,7 +790,7 @@ export var remoteSettingsBroadcastHandler = {
);
return RemoteSettings.pollChanges({
- expectedTimestamp: version.replace('"', ""),
+ expectedTimestamp: version.replaceAll('"', ""),
trigger: isStartup ? "startup" : "broadcast",
});
},
diff --git a/services/settings/test/unit/test_remote_settings.js b/services/settings/test/unit/test_remote_settings.js
@@ -810,7 +810,7 @@ add_task(async function test_inspect_method() {
equal(mainBucket, "main");
equal(serverURL, `http://localhost:${server.identity.primaryPort}/v1`);
equal(defaultSigner, rsSigner);
- equal(serverTimestamp, '"5000"');
+ equal(serverTimestamp, "5000");
// A collection is listed in .inspect() if it has local data or if there
// is a JSON dump for it.
@@ -1527,7 +1527,7 @@ wNuvFqc=
],
},
},
- "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=3001&_since=%223000%22":
+ "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=3001&_since=3000":
{
sampleHeaders: [
"Access-Control-Allow-Origin: *",
@@ -1558,7 +1558,7 @@ wNuvFqc=
],
},
},
- "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=4001&_since=%224000%22":
+ "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=4001&_since=4000":
{
sampleHeaders: [
"Access-Control-Allow-Origin: *",
@@ -1581,7 +1581,7 @@ wNuvFqc=
],
},
},
- "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=10000&_since=%229999%22":
+ "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=10000&_since=9999":
{
sampleHeaders: [
"Access-Control-Allow-Origin: *",
@@ -1596,7 +1596,7 @@ wNuvFqc=
error: "Service Unavailable",
},
},
- "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=10001&_since=%2210000%22":
+ "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=10001&_since=10000":
{
sampleHeaders: [
"Access-Control-Allow-Origin: *",
@@ -1608,7 +1608,7 @@ wNuvFqc=
status: { status: 200, statusText: "OK" },
responseBody: "<invalid json",
},
- "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=11001&_since=%2211000%22":
+ "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=11001&_since=11000":
{
sampleHeaders: [
"Access-Control-Allow-Origin: *",
@@ -1695,7 +1695,7 @@ wNuvFqc=
],
},
},
- "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=1337&_since=%223000%22":
+ "GET:/v1/buckets/main/collections/password-fields/changeset?_expected=1337&_since=3000":
{
sampleHeaders: [
"Access-Control-Allow-Origin: *",
@@ -1795,7 +1795,7 @@ wNuvFqc=
],
},
},
- "GET:/v1/buckets/main/collections/with-local-fields/changeset?_expected=3000&_since=%222000%22":
+ "GET:/v1/buckets/main/collections/with-local-fields/changeset?_expected=3000&_since=2000":
{
sampleHeaders: [
"Access-Control-Allow-Origin: *",
diff --git a/services/settings/test/unit/test_remote_settings_older_than_local.js b/services/settings/test/unit/test_remote_settings_older_than_local.js
@@ -41,7 +41,7 @@ add_setup(() => {
],
},
// Client fetches with _expected=222&_since=333
- "/v1/buckets/main/collections/some-cid/changeset?_expected=222&_since=%22333%22":
+ "/v1/buckets/main/collections/some-cid/changeset?_expected=222&_since=333":
{
timestamp: 222,
metadata: {
diff --git a/services/settings/test/unit/test_remote_settings_poll.js b/services/settings/test/unit/test_remote_settings_poll.js
@@ -73,7 +73,7 @@ add_task(async function test_an_event_is_sent_on_start() {
server.registerPathHandler(CHANGES_PATH, (request, response) => {
response.write(JSON.stringify({ timestamp: 42, changes: [] }));
response.setHeader("Content-Type", "application/json; charset=UTF-8");
- response.setHeader("ETag", '"42"');
+ response.setHeader("ETag", "42");
response.setHeader("Date", new Date().toUTCString());
response.setStatusLine(null, 200, "OK");
});
@@ -173,7 +173,7 @@ add_task(async function test_check_success() {
Assert.ok(maybeSyncCalled, "maybeSync was called");
Assert.ok(notificationObserved, "a notification should have been observed");
// Last timestamp was saved. An ETag header value is a quoted string.
- Assert.equal(Services.prefs.getStringPref(PREF_LAST_ETAG), '"1100"');
+ Assert.equal(Services.prefs.getStringPref(PREF_LAST_ETAG), "1100");
// check the last_update is updated
Assert.equal(Services.prefs.getIntPref(PREF_LAST_UPDATE), serverTime / 1000);
@@ -238,7 +238,7 @@ add_task(async function test_update_timer_interface() {
});
// Everything went fine.
- Assert.equal(Services.prefs.getStringPref(PREF_LAST_ETAG), '"42"');
+ Assert.equal(Services.prefs.getStringPref(PREF_LAST_ETAG), "42");
Assert.equal(Services.prefs.getIntPref(PREF_LAST_UPDATE), serverTime / 1000);
});
add_task(clear_state);
@@ -253,7 +253,7 @@ add_task(async function test_check_up_to_date() {
const serverTime = 4000;
server.registerPathHandler(CHANGES_PATH, serveChangesEntries(serverTime, []));
- Services.prefs.setStringPref(PREF_LAST_ETAG, '"1100"');
+ Services.prefs.setStringPref(PREF_LAST_ETAG, "1100");
// Ensure that the remote-settings:changes-poll-end notification is sent.
let notificationObserved = false;
@@ -414,7 +414,7 @@ add_task(async function test_age_of_data_is_reported_in_uptake_status() {
source: TELEMETRY_SOURCE_SYNC,
duration: () => true,
trigger: "manual",
- timestamp: `"${recordsTimestamp}"`,
+ timestamp: `${recordsTimestamp}`,
},
],
],
@@ -493,7 +493,7 @@ add_task(async function test_success_with_partial_list() {
collection: "poll-test-collection",
},
];
- if (request.queryString.includes(`_since=${encodeURIComponent('"42"')}`)) {
+ if (request.queryString.includes(`_since=42`)) {
response.write(
JSON.stringify({
timestamp: 43,
@@ -1308,7 +1308,7 @@ add_task(
timestamp: 42,
})
);
- response.setHeader("ETag", '"42"');
+ response.setHeader("ETag", "42");
response.setStatusLine(null, 200, "OK");
response.setHeader("Content-Type", "application/json; charset=UTF-8");
response.setHeader("Date", new Date().toUTCString());
diff --git a/services/settings/test/unit/test_remote_settings_release_prefs.js b/services/settings/test/unit/test_remote_settings_release_prefs.js
@@ -57,7 +57,7 @@ add_task(
Assert.equal(
Utils.SERVER_URL,
- AppConstants.REMOTE_SETTINGS_SERVER_URL,
+ AppConstants.REMOTE_SETTINGS_SERVER_URLS[0],
"Server url pref was not read in release"
);
}
@@ -65,6 +65,31 @@ add_task(
add_task(
{
+ skip_if: () => !AppConstants.RELEASE_OR_BETA,
+ },
+ async function test_server_url_can_be_changed_to_another_valid_option() {
+ Services.prefs.setStringPref(
+ "services.settings.server",
+ AppConstants.REMOTE_SETTINGS_SERVER_URLS[1]
+ );
+
+ const Utils = getNewUtils();
+
+ Assert.equal(
+ Utils.SERVER_URL,
+ AppConstants.REMOTE_SETTINGS_SERVER_URLS[1],
+ "Server url pref was read as second option in release"
+ );
+
+ Services.prefs.setStringPref(
+ "services.settings.server",
+ AppConstants.REMOTE_SETTINGS_SERVER_URLS[0]
+ );
+ }
+);
+
+add_task(
+ {
skip_if: () => AppConstants.RELEASE_OR_BETA,
},
async function test_server_url_cannot_be_toggled_in_dev_nightly() {
@@ -77,7 +102,7 @@ add_task(
Assert.notEqual(
Utils.SERVER_URL,
- AppConstants.REMOTE_SETTINGS_SERVER_URL,
+ AppConstants.REMOTE_SETTINGS_SERVER_URLS[0],
"Server url pref was read in nightly/dev"
);
}
@@ -126,7 +151,7 @@ add_task(
Assert.equal(
Utils.SERVER_URL,
- AppConstants.REMOTE_SETTINGS_SERVER_URL,
+ AppConstants.REMOTE_SETTINGS_SERVER_URLS[0],
"Server url pref was not read"
);
Assert.ok(Utils.LOAD_DUMPS, "Dumps will always be loaded");
@@ -147,7 +172,7 @@ add_task(
Assert.notEqual(
Utils.SERVER_URL,
- AppConstants.REMOTE_SETTINGS_SERVER_URL,
+ AppConstants.REMOTE_SETTINGS_SERVER_URLS[0],
"Server url pref was read"
);
Assert.ok(!Utils.LOAD_DUMPS, "Dumps are not loaded if server is not prod");
@@ -167,7 +192,7 @@ add_task(
Assert.notEqual(
Utils.SERVER_URL,
- AppConstants.REMOTE_SETTINGS_SERVER_URL,
+ AppConstants.REMOTE_SETTINGS_SERVER_URLS[0],
"Server url pref was read"
);
}
@@ -241,7 +266,7 @@ add_task(
Services.env.set("MOZ_REMOTE_SETTINGS_DEVTOOLS", "1");
Services.prefs.setStringPref(
"services.settings.server",
- AppConstants.REMOTE_SETTINGS_SERVER_URL
+ AppConstants.REMOTE_SETTINGS_SERVER_URLS[0]
);
const Utils = getNewUtils();
diff --git a/services/settings/test/unit/test_remote_settings_signatures.js b/services/settings/test/unit/test_remote_settings_signatures.js
@@ -441,7 +441,7 @@ add_task(async function test_check_synchronization_with_signatures() {
};
const twoItemsResponses = {
- "GET:/v1/buckets/main/collections/signed/changeset?_expected=3000&_since=%221000%22":
+ "GET:/v1/buckets/main/collections/signed/changeset?_expected=3000&_since=1000":
[RESPONSE_TWO_ADDED],
};
registerHandlers(twoItemsResponses);
@@ -482,7 +482,7 @@ add_task(async function test_check_synchronization_with_signatures() {
};
const oneAddedOneRemovedResponses = {
- "GET:/v1/buckets/main/collections/signed/changeset?_expected=4000&_since=%223000%22":
+ "GET:/v1/buckets/main/collections/signed/changeset?_expected=4000&_since=3000":
[RESPONSE_ONE_ADDED_ONE_REMOVED],
};
registerHandlers(oneAddedOneRemovedResponses);
@@ -520,7 +520,7 @@ add_task(async function test_check_synchronization_with_signatures() {
};
const noOpResponses = {
- "GET:/v1/buckets/main/collections/signed/changeset?_expected=4100&_since=%224000%22":
+ "GET:/v1/buckets/main/collections/signed/changeset?_expected=4100&_since=4000":
[RESPONSE_EMPTY_NO_UPDATE],
};
registerHandlers(noOpResponses);
@@ -581,7 +581,7 @@ add_task(async function test_check_synchronization_with_signatures() {
// The first collection state is the three item collection (since
// there was sync with no updates before) - but, since the signature is wrong,
// another request will be made...
- "GET:/v1/buckets/main/collections/signed/changeset?_expected=5000&_since=%224000%22":
+ "GET:/v1/buckets/main/collections/signed/changeset?_expected=5000&_since=4000":
[RESPONSE_EMPTY_NO_UPDATE_BAD_SIG],
// Subsequent signature returned is a valid one for the three item
// collection.
@@ -639,7 +639,7 @@ add_task(async function test_check_synchronization_with_signatures() {
const badSigGoodOldResponses = {
// The first collection state is the current state (since there's no update
// - but, since the signature is wrong, another request will be made)
- "GET:/v1/buckets/main/collections/signed/changeset?_expected=5000&_since=%224000%22":
+ "GET:/v1/buckets/main/collections/signed/changeset?_expected=5000&_since=4000":
[RESPONSE_EMPTY_NO_UPDATE_BAD_SIG],
// The next request is for the full collection. This will be
// checked against the valid signature and last_modified times will be
@@ -691,7 +691,7 @@ add_task(async function test_check_synchronization_with_signatures() {
};
const badLocalContentGoodSigResponses = {
- "GET:/v1/buckets/main/collections/signed/changeset?_expected=5000&_since=%223900%22":
+ "GET:/v1/buckets/main/collections/signed/changeset?_expected=5000&_since=3900":
[RESPONSE_COMPLETE_BAD_SIG],
"GET:/v1/buckets/main/collections/signed/changeset?_expected=5000": [
RESPONSE_COMPLETE_INITIAL,
@@ -824,7 +824,7 @@ add_task(async function test_check_synchronization_with_signatures() {
}),
};
const allBadSigResponses = {
- "GET:/v1/buckets/main/collections/signed/changeset?_expected=6000&_since=%224000%22":
+ "GET:/v1/buckets/main/collections/signed/changeset?_expected=6000&_since=4000":
[RESPONSE_EMPTY_NO_UPDATE_BAD_SIG_6000],
"GET:/v1/buckets/main/collections/signed/changeset?_expected=6000": [
RESPONSE_ONLY_RECORD4_BAD_SIG,
diff --git a/services/settings/test/unit/test_remote_settings_sync_history.js b/services/settings/test/unit/test_remote_settings_sync_history.js
@@ -7,9 +7,9 @@ add_task(clear_state);
add_task(async function test_entries_are_stored_by_source() {
const history = new SyncHistory();
- await history.store("42", "success", { pi: "3.14" });
+ await history.store(42, "success", { pi: "3.14" });
// Check that history is isolated by source.
- await new SyncHistory("main/cfr").store("88", "error");
+ await new SyncHistory("main/cfr").store(88, "error");
const l = await history.list();
@@ -29,15 +29,15 @@ add_task(
const history = new SyncHistory("settings-sync", { size: 3 });
const anotherHistory = await new SyncHistory("main/cfr");
- await history.store("42", "success");
- await history.store("41", "sync_error");
- await history.store("43", "up_to_date");
+ await history.store(42, "success");
+ await history.store(41, "sync_error");
+ await history.store(43, "up_to_date");
let l = await history.list();
Assert.equal(l.length, 3);
- await history.store("44", "success");
- await anotherHistory.store("44", "success");
+ await history.store(44, "success");
+ await anotherHistory.store(44, "success");
l = await history.list();
Assert.equal(l.length, 3);
@@ -51,9 +51,9 @@ add_task(clear_state);
add_task(async function test_entries_are_sorted_by_timestamp_desc() {
const history = new SyncHistory("settings-sync");
- await history.store("42", "success");
- await history.store("41", "sync_error");
- await history.store("44", "up_to_date");
+ await history.store(42, "success");
+ await history.store(41, "sync_error");
+ await history.store(44, "up_to_date");
const l = await history.list();
diff --git a/toolkit/modules/AppConstants.sys.mjs b/toolkit/modules/AppConstants.sys.mjs
@@ -207,11 +207,11 @@ export var AppConstants = Object.freeze({
ENABLE_WEBDRIVER: @ENABLE_WEBDRIVER_BOOL@,
- REMOTE_SETTINGS_SERVER_URL:
+ REMOTE_SETTINGS_SERVER_URLS:
#ifdef MOZ_THUNDERBIRD
- "https://thunderbird-settings.thunderbird.net/v1",
+ [ "https://thunderbird-settings.thunderbird.net/v1" ],
#else
- "https://firefox.settings.services.mozilla.com/v1",
+ [ "https://firefox.settings.services.mozilla.com/v1", "https://firefox.settings.services.mozilla.com/v2" ],
#endif
REMOTE_SETTINGS_VERIFY_SIGNATURE:
diff --git a/tools/@types/subs/AppConstants.sys.d.mts b/tools/@types/subs/AppConstants.sys.d.mts
@@ -154,7 +154,7 @@ export const AppConstants: Readonly<{
ENABLE_WEBDRIVER: boolean;
// #ifdef !MOZ_THUNDERBIRD
- REMOTE_SETTINGS_SERVER_URL: "https://firefox.settings.services.mozilla.com/v1";
+ REMOTE_SETTINGS_SERVER_URLS: [ "https://firefox.settings.services.mozilla.com/v1" ];
// #ifdef !MOZ_THUNDERBIRD
REMOTE_SETTINGS_VERIFY_SIGNATURE: boolean;