commit 1cd1965649cd7dfe39772e662b7c7cd3c6bf9a95
parent 83a675a5416e6e905aa8eed1e995335f88d21eb3
Author: Mike Conley <mconley@mozilla.com>
Date: Wed, 10 Dec 2025 21:35:27 +0000
Bug 2005307 - Send pre-flight headers when communicating with MARS over OHTTP for sponsored topsites. r=home-newtab-reviewers,nbarrett
Differential Revision: https://phabricator.services.mozilla.com/D275865
Diffstat:
2 files changed, 138 insertions(+), 0 deletions(-)
diff --git a/browser/extensions/newtab/lib/TopSitesFeed.sys.mjs b/browser/extensions/newtab/lib/TopSitesFeed.sys.mjs
@@ -162,6 +162,12 @@ const DISPLAY_FAIL_REASON_UNRESOLVED = "unresolved";
const RS_FALLBACK_BASE_URL =
"https://firefox-settings-attachments.cdn.mozilla.net/";
+ChromeUtils.defineLazyGetter(lazy, "userAgent", () => {
+ return Cc["@mozilla.org/network/protocol;1?name=http"].getService(
+ Ci.nsIHttpProtocolHandler
+ ).userAgent;
+});
+
// Smart shortcuts
import { RankShortcutsProvider } from "resource://newtab/lib/SmartShortcutsRanker/RankShortcuts.mjs";
@@ -644,6 +650,32 @@ export class ContileIntegration {
const endpointBaseUrl = state.Prefs.values[PREF_UNIFIED_ADS_ENDPOINT];
+ const preFlightConfig =
+ state.Prefs.values?.trainhopConfig?.marsPreFlight || {};
+
+ // We need some basic data that we can pass along to the ohttp request.
+ // We purposefully don't use ohttp on this request. We also expect to
+ // mostly hit the HTTP cache rather than the network with these requests.
+ if (preFlightConfig.enabled) {
+ const preflightResponse = await this._topSitesFeed.fetch(
+ `${endpointBaseUrl}v1/ads-preflight`,
+ {
+ method: "GET",
+ }
+ );
+ const preFlight = await preflightResponse.json();
+
+ if (preFlight) {
+ // If we don't get a normalized_ua, it means it matched the default userAgent.
+ headers.append(
+ "X-User-Agent",
+ preFlight.normalized_ua || lazy.userAgent
+ );
+ headers.append("X-Geoname-ID", preFlight.geoname_id);
+ headers.append("X-Geo-Location", preFlight.geo_location);
+ }
+ }
+
let blockedSponsors =
this._topSitesFeed.store.getState().Prefs.values[
PREF_UNIFIED_ADS_BLOCKED_LIST
@@ -693,6 +725,17 @@ export class ContileIntegration {
);
return null;
}
+
+ // ObliviousHTTP.ohttpRequest only accepts a key/value object, and not
+ // a Headers instance. We normalize any headers to a key/value object.
+ //
+ // We use instanceof here since isInstance isn't available for
+ // Headers, it seems.
+ // eslint-disable-next-line mozilla/use-isInstance
+ if (options.headers && options.headers instanceof Headers) {
+ options.headers = Object.fromEntries(options.headers);
+ }
+
fetchPromise = lazy.ObliviousHTTP.ohttpRequest(
ohttpRelayURL,
config,
diff --git a/browser/extensions/newtab/test/xpcshell/test_TopSitesFeed.js b/browser/extensions/newtab/test/xpcshell/test_TopSitesFeed.js
@@ -11,6 +11,7 @@ ChromeUtils.defineESModuleGetters(this, {
FilterAdult: "resource:///modules/FilterAdult.sys.mjs",
NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs",
NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
+ ObliviousHTTP: "resource://gre/modules/ObliviousHTTP.sys.mjs",
PageThumbs: "resource://gre/modules/PageThumbs.sys.mjs",
PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
sinon: "resource://testing-common/Sinon.sys.mjs",
@@ -3395,5 +3396,99 @@ add_task(async function test_ContileIntegration() {
sandbox.restore();
}
+ {
+ info(
+ "TopSitesFeed._fetchSites should cast headers from a Headers object to JS object when using OHTTP"
+ );
+ let { feed } = prepFeed(getTopSitesFeedForTest(sandbox));
+
+ Services.prefs.setStringPref(
+ "browser.newtabpage.activity-stream.discoverystream.ohttp.relayURL",
+ "https://relay.url"
+ );
+ Services.prefs.setStringPref(
+ "browser.newtabpage.activity-stream.discoverystream.ohttp.configURL",
+ "https://config.url"
+ );
+ Services.prefs.setBoolPref(
+ "browser.newtabpage.activity-stream.unifiedAds.ohttp.enabled",
+ true
+ );
+ feed.store.state.Prefs.values["unifiedAds.tiles.enabled"] = true;
+ feed.store.state.Prefs.values["unifiedAds.adsFeed.enabled"] = false;
+ feed.store.state.Prefs.values["unifiedAds.endpoint"] =
+ "https://test.endpoint/";
+ feed.store.state.Prefs.values["discoverystream.placements.tiles"] = "1";
+ feed.store.state.Prefs.values["discoverystream.placements.tiles.counts"] =
+ "1";
+ feed.store.state.Prefs.values["unifiedAds.blockedAds"] = "";
+
+ const fakeOhttpConfig = { config: "config" };
+ sandbox.stub(ObliviousHTTP, "getOHTTPConfig").resolves(fakeOhttpConfig);
+
+ const ohttpRequestStub = sandbox
+ .stub(ObliviousHTTP, "ohttpRequest")
+ .resolves({
+ ok: true,
+ status: 200,
+ headers: new Map([
+ ["cache-control", "private, max-age=859, stale-if-error=10463"],
+ ]),
+ json: () =>
+ Promise.resolve({
+ 1: [
+ {
+ block_key: 12345,
+ name: "test",
+ url: "https://www.test.com",
+ image_url: "images/test-com.png",
+ callbacks: {
+ click: "https://www.test-click.com",
+ impression: "https://www.test-impression.com",
+ },
+ },
+ ],
+ }),
+ });
+
+ let fetched = await feed._contile._fetchSites();
+
+ Assert.ok(fetched);
+ Assert.ok(
+ ohttpRequestStub.calledOnce,
+ "ohttpRequest should be called once"
+ );
+ const callArgs = ohttpRequestStub.getCall(0).args;
+ Assert.equal(callArgs[0], "https://relay.url", "relay URL should match");
+ Assert.deepEqual(
+ callArgs[1],
+ fakeOhttpConfig,
+ "config should be passed through"
+ );
+ Assert.equal(
+ typeof callArgs[3].headers,
+ "object",
+ "headers should be a plain object"
+ );
+ Assert.ok(
+ // We use instanceof here since isInstance isn't available for
+ // Headers, it seems.
+ // eslint-disable-next-line mozilla/use-isInstance
+ !(callArgs[3].headers instanceof Headers),
+ "headers should not be a Headers instance"
+ );
+
+ Services.prefs.clearUserPref(
+ "browser.newtabpage.activity-stream.discoverystream.ohttp.relayURL"
+ );
+ Services.prefs.clearUserPref(
+ "browser.newtabpage.activity-stream.discoverystream.ohttp.configURL"
+ );
+ Services.prefs.clearUserPref(
+ "browser.newtabpage.activity-stream.unifiedAds.ohttp.enabled"
+ );
+ sandbox.restore();
+ }
+
Services.prefs.clearUserPref(TOP_SITES_BLOCKED_SPONSORS_PREF);
});