Repository: electron/electron (language: cpp)

Issue:
[Bug]: Possibly broken user agent client hint
### Preflight Checklist

- [X] I have read the [Contributing Guidelines](https://github.com/electron/electron/blob/master/CONTRIBUTING.md) for this project.
- [X] I agree to follow the [Code of Conduct](https://github.com/electron/electron/blob/master/CODE_OF_CONDUCT.md) that this project adheres to.
- [X] I have searched the [issue tracker](https://www.github.com/electron/electron/issues) for a feature request that matches the one I want to file, without success.

### Electron Version

13.1.7

### What operating system are you using?

Windows

### Operating System Version

Windows 10 version 20H2

### What arch are you using?

x64

### Last Known Working Electron version

12.0.15

### Expected Behavior

Checking `navigator.userAgentData` in Chrome 91 returns an array of brands appropriately. Electron 13/14 do not return anything.

This appears to then become a problem for my situation. I am posting a form with hidden variables to a third-party vendor I work with. This form post ultimately loads an iframe.

Expected: As it does in Chrome 91, load the iframe from this third-party vendor.

### Actual Behavior

A header of "sec-ch-ua" is included in the request to the third-party vendor and it is blank. They are rejecting the post because of an invalid header. I suspect this is because Chrome 13/14 does not make this hint data made available correctly.

I tried loading various sites in Chrome 91 that do load brand names in Electron 13/14 and they are empty if you review `navigator.userAgentData`.

### Testcase Gist URL

_No response_

### Additional Information

_No response_


Discussion / hints:

Fix patch:
diff --git a/BUILD.gn b/BUILD.gn
index 9b62fb12029d4..fe9b7f3655740 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -371,6 +371,7 @@ source_set("electron_lib") {
     "//chrome/app/resources:platform_locale_settings",
     "//components/autofill/core/common:features",
     "//components/certificate_transparency",
+    "//components/embedder_support:browser_util",
     "//components/language/core/browser",
     "//components/net_log",
     "//components/network_hints/browser",
diff --git a/shell/browser/api/electron_api_web_contents.cc b/shell/browser/api/electron_api_web_contents.cc
index f719bcbd7cbf4..017897b29b7d1 100644
--- a/shell/browser/api/electron_api_web_contents.cc
+++ b/shell/browser/api/electron_api_web_contents.cc
@@ -29,6 +29,7 @@
 #include "chrome/browser/ui/exclusive_access/exclusive_access_manager.h"
 #include "chrome/browser/ui/views/eye_dropper/eye_dropper.h"
 #include "chrome/common/pref_names.h"
+#include "components/embedder_support/user_agent_utils.h"
 #include "components/prefs/pref_service.h"
 #include "components/prefs/scoped_user_pref_update.h"
 #include "components/security_state/content/content_utils.h"
@@ -661,9 +662,10 @@ WebContents::WebContents(v8::Isolate* isolate,
   auto session = Session::CreateFrom(isolate, GetBrowserContext());
   session_.Reset(isolate, session.ToV8());
 
-  web_contents->SetUserAgentOverride(blink::UserAgentOverride::UserAgentOnly(
-                                         GetBrowserContext()->GetUserAgent()),
-                                     false);
+  absl::optional<std::string> user_agent_override =
+      GetBrowserContext()->GetUserAgentOverride();
+  if (user_agent_override)
+    SetUserAgent(*user_agent_override);
   web_contents->SetUserData(kElectronApiWebContentsKey,
                             std::make_unique<UserDataLink>(GetWeakPtr()));
   InitZoomController(web_contents, gin::Dictionary::CreateEmpty(isolate));
@@ -869,9 +871,10 @@ void WebContents::InitWithSessionAndOptions(
 
   AutofillDriverFactory::CreateForWebContents(web_contents());
 
-  web_contents()->SetUserAgentOverride(blink::UserAgentOverride::UserAgentOnly(
-                                           GetBrowserContext()->GetUserAgent()),
-                                       false);
+  absl::optional<std::string> user_agent_override =
+      GetBrowserContext()->GetUserAgentOverride();
+  if (user_agent_override)
+    SetUserAgent(*user_agent_override);
 
   if (IsGuest()) {
     NativeWindow* owner_window = nullptr;
@@ -2149,8 +2152,7 @@ void WebContents::LoadURL(const GURL& url,
 
   std::string user_agent;
   if (options.Get("userAgent", &user_agent))
-    web_contents()->SetUserAgentOverride(
-        blink::UserAgentOverride::UserAgentOnly(user_agent), false);
+    SetUserAgent(user_agent);
 
   std::string extra_headers;
   if (options.Get("extraHeaders", &extra_headers))
@@ -2365,8 +2367,12 @@ void WebContents::ForcefullyCrashRenderer() {
 }
 
 void WebContents::SetUserAgent(const std::string& user_agent) {
-  web_contents()->SetUserAgentOverride(
-      blink::UserAgentOverride::UserAgentOnly(user_agent), false);
+  blink::UserAgentOverride ua_override;
+  ua_override.ua_string_override = user_agent;
+  if (!user_agent.empty())
+    ua_override.ua_metadata_override = embedder_support::GetUserAgentMetadata();
+
+  web_contents()->SetUserAgentOverride(ua_override, false);
 }
 
 std::string WebContents::GetUserAgent() {
diff --git a/shell/browser/electron_browser_client.cc b/shell/browser/electron_browser_client.cc
index 813f70799813a..fe5a2da4a87a7 100644
--- a/shell/browser/electron_browser_client.cc
+++ b/shell/browser/electron_browser_client.cc
@@ -30,6 +30,7 @@
 #include "chrome/common/chrome_paths.h"
 #include "chrome/common/chrome_switches.h"
 #include "chrome/common/chrome_version.h"
+#include "components/embedder_support/user_agent_utils.h"
 #include "components/net_log/chrome_net_log.h"
 #include "components/network_hints/common/network_hints.mojom.h"
 #include "content/browser/keyboard_lock/keyboard_lock_service_impl.h"  // nogncheck
@@ -1159,6 +1160,10 @@ void ElectronBrowserClient::SetUserAgent(const std::string& user_agent) {
   user_agent_override_ = user_agent;
 }
 
+blink::UserAgentMetadata ElectronBrowserClient::GetUserAgentMetadata() {
+  return embedder_support::GetUserAgentMetadata();
+}
+
 void ElectronBrowserClient::RegisterNonNetworkNavigationURLLoaderFactories(
     int frame_tree_node_id,
     ukm::SourceIdObj ukm_source_id,
diff --git a/shell/browser/electron_browser_client.h b/shell/browser/electron_browser_client.h
index b4b2ee87bee12..ed2447b4312e2 100644
--- a/shell/browser/electron_browser_client.h
+++ b/shell/browser/electron_browser_client.h
@@ -93,6 +93,7 @@ class ElectronBrowserClient : public content::ContentBrowserClient,
 
   std::string GetUserAgent() override;
   void SetUserAgent(const std::string& user_agent);
+  blink::UserAgentMetadata GetUserAgentMetadata() override;
 
   content::SerialDelegate* GetSerialDelegate() override;
 
diff --git a/shell/browser/electron_browser_context.cc b/shell/browser/electron_browser_context.cc
index a23f6a9c7fa09..66643c4731e99 100644
--- a/shell/browser/electron_browser_context.cc
+++ b/shell/browser/electron_browser_context.cc
@@ -107,8 +107,6 @@ ElectronBrowserContext::ElectronBrowserContext(const std::string& partition,
       protocol_registry_(base::WrapUnique(new ProtocolRegistry)),
       in_memory_(in_memory),
       ssl_config_(network::mojom::SSLConfig::New()) {
-  user_agent_ = ElectronBrowserClient::Get()->GetUserAgent();
-
   // Read options.
   base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
   use_cache_ = !command_line->HasSwitch(switches::kDisableHttpCache);
@@ -305,6 +303,11 @@ ElectronBrowserContext::GetSpecialStoragePolicy() {
 }
 
 std::string ElectronBrowserContext::GetUserAgent() const {
+  return user_agent_.value_or(ElectronBrowserClient::Get()->GetUserAgent());
+}
+
+absl::optional<std::string> ElectronBrowserContext::GetUserAgentOverride()
+    const {
   return user_agent_;
 }
 
diff --git a/shell/browser/electron_browser_context.h b/shell/browser/electron_browser_context.h
index 3a646aad02ce4..c585859ce038f 100644
--- a/shell/browser/electron_browser_context.h
+++ b/shell/browser/electron_browser_context.h
@@ -85,6 +85,7 @@ class ElectronBrowserContext : public content::BrowserContext {
 
   void SetUserAgent(const std::string& user_agent);
   std::string GetUserAgent() const;
+  absl::optional<std::string> GetUserAgentOverride() const;
   bool CanUseHttpCache() const;
   int GetMaxCacheSize() const;
   ResolveProxyHelper* GetResolveProxyHelper();
@@ -170,7 +171,7 @@ class ElectronBrowserContext : public content::BrowserContext {
   std::unique_ptr<predictors::PreconnectManager> preconnect_manager_;
   std::unique_ptr<ProtocolRegistry> protocol_registry_;
 
-  std::string user_agent_;
+  absl::optional<std::string> user_agent_;
   base::FilePath path_;
   bool in_memory_ = false;
   bool use_cache_ = true;
diff --git a/spec-main/chromium-spec.ts b/spec-main/chromium-spec.ts
index d6bc5cd897a85..f47c1b9b2a63d 100644
--- a/spec-main/chromium-spec.ts
+++ b/spec-main/chromium-spec.ts
@@ -1508,6 +1508,68 @@ describe('chromium features', () => {
       expect(focus).to.be.false();
     });
   });
+
+  describe('navigator.userAgentData', () => {
+    // These tests are done on an http server because navigator.userAgentData
+    // requires a secure context.
+    let server: http.Server;
+    let serverUrl: string;
+    before(async () => {
+      server = http.createServer((req, res) => {
+        res.setHeader('Content-Type', 'text/html');
+        res.end('');
+      });
+      await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
+      serverUrl = `http://localhost:${(server.address() as any).port}`;
+    });
+    after(() => {
+      server.close();
+    });
+
+    describe('is not empty', () => {
+      it('by default', async () => {
+        const w = new BrowserWindow({ show: false });
+        await w.loadURL(serverUrl);
+        const platform = await w.webContents.executeJavaScript('navigator.userAgentData.platform');
+        expect(platform).not.to.be.empty();
+      });
+
+      it('when there is a session-wide UA override', async () => {
+        const ses = session.fromPartition(`${Math.random()}`);
+        ses.setUserAgent('foobar');
+        const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
+        await w.loadURL(serverUrl);
+        const platform = await w.webContents.executeJavaScript('navigator.userAgentData.platform');
+        expect(platform).not.to.be.empty();
+      });
+
+      it('when there is a WebContents-specific UA override', async () => {
+        const w = new BrowserWindow({ show: false });
+        w.webContents.setUserAgent('foo');
+        await w.loadURL(serverUrl);
+        const platform = await w.webContents.executeJavaScript('navigator.userAgentData.platform');
+        expect(platform).not.to.be.empty();
+      });
+
+      it('when there is a WebContents-specific UA override at load time', async () => {
+        const w = new BrowserWindow({ show: false });
+        await w.loadURL(serverUrl, {
+          userAgent: 'foo'
+        });
+        const platform = await w.webContents.executeJavaScript('navigator.userAgentData.platform');
+        expect(platform).not.to.be.empty();
+      });
+    });
+
+    describe('brand list', () => {
+      it('contains chromium', async () => {
+        const w = new BrowserWindow({ show: false });
+        await w.loadURL(serverUrl);
+        const brands = await w.webContents.executeJavaScript('navigator.userAgentData.brands');
+        expect(brands.map((b: any) => b.brand)).to.include('Chromium');
+      });
+    });
+  });
 });
 
 describe('font fallback', () => {


Test patch: