Reland "Add Sec-CH-UA-Reduced client hint header when Origin Trial token present"

This reverts commit f6370c35ee4f448247485c6e875d3c60679d0454.

Reason for revert: The original CL (crrev.com/c/3042810) started the EmbeddedTestServer on a specific port in the UaReducedOriginTrialBrowserTest, since the test used an Origin Trial token that is bound to a specific origin.  This caused the test to fail on CI for linux-chromeos-chrome, due to being unable to start EmbeddedTestServer as the specific port was already in-use, and the original CL was reverted in crrev.com/c/3061119.  This CL relands the original CL and fixes the problem by using URLLoaderInterceptor instead of EmbeddedTestServer in the UaReducedOriginTrialBrowserTest tests.

Original change's description:
> Revert "Add Sec-CH-UA-Reduced client hint header when Origin Trial token present"
>
> This reverts commit d9c057ea37355847f0c9530eada84aca335c96b4.
>
> Reason for revert: Browser tests is failing on linux-chromeos-chrome.
> See [1].
>
> [1] https://ci.chromium.org/ui/p/chrome/builders/ci/linux-chromeos-chrome/16242/overview
>
> Original change's description:
> > Add Sec-CH-UA-Reduced client hint header when Origin Trial token present
> >
> > In this CL, we add logic to parse and accept the Sec-CH-UA-Reduced
> > client hint in the Accept-CH header and and only send the
> > Sec-CH-UA-Reduced client hint only in the presence of a valid
> > UserAgentReduction Origin Trial token.
> >
> > If the Origin Trial token is present and valid, and the origin
> > contains Sec-CH-UA-Reduced in the Accept-CH cache, then on all
> > subsequent requests to the origin, the Sec-CH-UA-Reduced request
> > header will be set with a value of "?1" (sh-boolean).  If the
> > Accept-CH cache does not contain Sec-CH-UA-Reduced, or the Origin
> > Trial token is not valid, then Sec-CH-UA-Reduced will not be sent
> > in the request headers.
> >
> > NB: A subsequent CL will change the User-Agent request header to
> > the reduced UA string if the Sec-CH-UA-Reduced header is sent.
> >
> > Chrome Platform Status: https://chromestatus.com/feature/5704553745874944
> > Design Doc: https://docs.google.com/document/d/1feIxK9S7oNgT2oGGebbxE9X0O-4wTKcsP_gRaY99tq4
> >
> > Bug: 1222742
> > Change-Id: If855d4bb393540d49de3be80aa9bc7c80f861c50
> > Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3042810
> > Commit-Queue: Ali Beyad <[email protected]>
> > Reviewed-by: Aaron Tagliaboschi <[email protected]>
> > Reviewed-by: Robert Kroeger <[email protected]>
> > Reviewed-by: Mike West <[email protected]>
> > Cr-Commit-Position: refs/heads/master@{#906810}
>
> Bug: 1222742
> Change-Id: I0d8aab1d56d78f8323d4a3e3ed88b6eef370e1b9
> No-Presubmit: true
> No-Tree-Checks: true
> No-Try: true
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3061119
> Auto-Submit: Daniel Hosseinian <[email protected]>
> Commit-Queue: Rubber Stamper <[email protected]>
> Bot-Commit: Rubber Stamper <[email protected]>
> Owners-Override: Daniel Hosseinian <[email protected]>
> Cr-Commit-Position: refs/heads/master@{#906884}

Bug: 1222742
Change-Id: I3ed39697255116fe1f2ab5203331e8786e9d66a3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3064532
Commit-Queue: Ali Beyad <[email protected]>
Reviewed-by: Kinuko Yasuda <[email protected]>
Reviewed-by: Aaron Tagliaboschi <[email protected]>
Reviewed-by: Daniel Hosseinian <[email protected]>
Cr-Commit-Position: refs/heads/master@{#909169}
diff --git a/chrome/browser/client_hints/client_hints_browsertest.cc b/chrome/browser/client_hints/client_hints_browsertest.cc
index c44f7f87..5f3ef9fd 100644
--- a/chrome/browser/client_hints/client_hints_browsertest.cc
+++ b/chrome/browser/client_hints/client_hints_browsertest.cc
@@ -11,8 +11,10 @@
 #include "base/containers/contains.h"
 #include "base/metrics/field_trial_param_associator.h"
 #include "base/run_loop.h"
+#include "base/strings/strcat.h"
 #include "base/strings/string_util.h"
 #include "base/synchronization/lock.h"
+#include "base/test/bind.h"
 #include "base/test/metrics/histogram_tester.h"
 #include "build/build_config.h"
 #include "chrome/browser/content_settings/cookie_settings_factory.h"
@@ -28,17 +30,22 @@
 #include "components/content_settings/core/browser/cookie_settings.h"
 #include "components/content_settings/core/browser/host_content_settings_map.h"
 #include "components/content_settings/core/common/pref_names.h"
+#include "components/embedder_support/switches.h"
 #include "components/embedder_support/user_agent_utils.h"
 #include "components/metrics/content/subprocess_metrics_provider.h"
 #include "components/page_load_metrics/browser/page_load_metrics_test_waiter.h"
 #include "components/prefs/pref_service.h"
 #include "content/public/browser/browser_thread.h"
 #include "content/public/browser/navigation_entry.h"
+#include "content/public/browser/navigation_handle.h"
 #include "content/public/browser/render_view_host.h"
+#include "content/public/browser/web_contents_observer.h"
 #include "content/public/common/content_features.h"
 #include "content/public/common/content_switches.h"
 #include "content/public/test/browser_test.h"
 #include "content/public/test/browser_test_utils.h"
+#include "content/public/test/content_browser_test.h"
+#include "content/public/test/content_browser_test_utils.h"
 #include "content/public/test/test_utils.h"
 #include "content/public/test/url_loader_interceptor.h"
 #include "net/dns/mock_host_resolver.h"
@@ -51,8 +58,11 @@
 #include "services/network/public/cpp/cors/cors.h"
 #include "services/network/public/cpp/features.h"
 #include "services/network/public/cpp/network_switches.h"
+#include "services/network/public/cpp/resource_request.h"
 #include "services/network/public/mojom/web_client_hints_types.mojom-shared.h"
+#include "testing/gmock/include/gmock/gmock-matchers.h"
 #include "testing/gmock/include/gmock/gmock.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
 #include "third_party/blink/public/common/client_hints/client_hints.h"
 #include "third_party/blink/public/common/web_preferences/web_preferences.h"
 
@@ -692,8 +702,10 @@
         continue;
       }
 
-      // TODO(crbug.com/1226193): Remove this block when support for
-      // `Sec-CH-UA-Reduced` has been added.
+      // Skip over the `Sec-CH-UA-Reduced` client hint because it is only added
+      // in the presence of a valid "UserAgentReduction" Origin Trial token.
+      // `Sec-CH-UA-Reduced` is tested via UaReducedOriginTrialBrowserTest
+      // below.
       if (std::string(blink::kClientHintsHeaderMapping[i]) ==
           "sec-ch-ua-reduced") {
         continue;
@@ -2475,3 +2487,184 @@
   ui_test_utils::NavigateToURL(browser(), test_url());
   EXPECT_EQ(prefers_color_scheme_observed(), "dark");
 }
+
+// Tests that the Sec-CH-UA-Reduced client hint is sent if and only if the
+// UserAgentReduction Origin Trial token is present and valid in the response
+// headers.
+//
+// The test Origin Trial token found in the test files was generated by running
+// (in tools/origin_trials):
+// generate_token.py https://127.0.0.1:44444 UserAgentReduction
+// --expire-timestamp=2000000000
+//
+// The Origin Trial token expires in 2033.  Generate a new token by then, or
+// find a better way to re-generate a test trial token.
+class UaReducedOriginTrialBrowserTest : public InProcessBrowserTest {
+ public:
+  UaReducedOriginTrialBrowserTest() = default;
+
+  // The URL that was used to register for the Origin Trial token.
+  static constexpr const char kOriginUrl[] = "https://127.0.0.1:44444";
+
+  void SetUpCommandLine(base::CommandLine* command_line) override {
+    // The public key for the default privatey key used by the
+    // tools/origin_trials/generate_token.py tool.
+    static constexpr char kOriginTrialTestPublicKey[] =
+        "dRCs+TocuKkocNKa0AtZ4awrt9XKH2SQCI6o4FY6BNA=";
+    command_line->AppendSwitchASCII(embedder_support::kOriginTrialPublicKey,
+                                    kOriginTrialTestPublicKey);
+  }
+
+  void SetUp() override {
+    std::unique_ptr<base::FeatureList> feature_list =
+        std::make_unique<base::FeatureList>();
+    feature_list->InitializeFromCommandLine("CriticalClientHint", "");
+    scoped_feature_list_.InitWithFeatureList(std::move(feature_list));
+
+    InProcessBrowserTest::SetUp();
+  }
+
+  void SetUpOnMainThread() override {
+    InProcessBrowserTest::SetUpOnMainThread();
+
+    // We use a URLLoaderInterceptor, rather than the EmbeddedTestServer, since
+    // the origin trial token in the response is associated with a fixed
+    // origin, whereas EmbeddedTestServer serves content on a random port.
+    url_loader_interceptor_ =
+        content::URLLoaderInterceptor::ServeFilesFromDirectoryAtOrigin(
+            "chrome/test/data/client_hints", GURL(kOriginUrl));
+  }
+
+  void TearDownOnMainThread() override {
+    url_loader_interceptor_.reset();
+    InProcessBrowserTest::TearDownOnMainThread();
+  }
+
+  GURL ua_reduced_with_valid_origin_trial_token_url() const {
+    return GURL(base::StrCat(
+        {kOriginUrl, "/accept_ch_ua_reduced_with_valid_origin_trial.html"}));
+  }
+
+  GURL ua_reduced_with_invalid_origin_trial_token_url() const {
+    return GURL(base::StrCat(
+        {kOriginUrl, "/accept_ch_ua_reduced_with_invalid_origin_trial.html"}));
+  }
+
+  GURL ua_reduced_with_no_origin_trial_token_url() const {
+    return GURL(base::StrCat(
+        {kOriginUrl, "/accept_ch_ua_reduced_with_no_origin_trial.html"}));
+  }
+
+  GURL ua_reduced_missing_with_valid_origin_trial_token_url() const {
+    return GURL(base::StrCat(
+        {kOriginUrl, "/accept_ch_ua_reduced_missing_valid_origin_trial.html"}));
+  }
+
+  GURL critical_ch_ua_reduced_with_valid_origin_trial_token_url() const {
+    return GURL(base::StrCat(
+        {kOriginUrl, "/critical_ch_ua_reduced_with_valid_origin_trial.html"}));
+  }
+
+  GURL critical_ch_ua_reduced_with_invalid_origin_trial_token_url() const {
+    return GURL(base::StrCat(
+        {kOriginUrl,
+         "/critical_ch_ua_reduced_with_invalid_origin_trial.html"}));
+  }
+
+  void NavigateAndCheckHeader(const GURL& url,
+                              const bool ch_ua_reduced_expected) {
+    ui_test_utils::NavigateToURL(browser(), url);
+    base::RunLoop().RunUntilIdle();
+
+    std::string header_value;
+    const bool ch_ua_reduced =
+        url_loader_interceptor_->GetLastRequestHeaders().GetHeader(
+            "sec-ch-ua-reduced", &header_value);
+
+    EXPECT_EQ(ch_ua_reduced, ch_ua_reduced_expected);
+    if (ch_ua_reduced_expected) {
+      EXPECT_EQ(header_value, "?1");
+    }
+  }
+
+  void NavigateTwiceAndCheckHeader(const GURL& url,
+                                   const bool ch_ua_reduced_expected,
+                                   const bool critical_ch_ua_reduced_expected) {
+    // If Critical-CH is set, we expect Sec-CH-UA-Reduced in the first
+    // navigation request header.  If Critical-CH is not set, we don't expect
+    // Sec-CH-UA-Reduced in the first navigation request.
+    NavigateAndCheckHeader(
+        url, critical_ch_ua_reduced_expected && ch_ua_reduced_expected);
+
+    // Regardless of the Critical-CH setting, we expect the Sec-CH-UA-Reduced
+    // client hint sent on the second request, if Sec-CH-UA-Reduced is set and
+    // the Origin Trial token is valid.
+    NavigateAndCheckHeader(url, ch_ua_reduced_expected);
+  }
+
+ private:
+  std::unique_ptr<content::URLLoaderInterceptor> url_loader_interceptor_;
+  base::test::ScopedFeatureList scoped_feature_list_;
+};
+
+constexpr const char UaReducedOriginTrialBrowserTest::kOriginUrl[];
+
+IN_PROC_BROWSER_TEST_F(UaReducedOriginTrialBrowserTest,
+                       AcceptChUaReducedWithValidOriginTrialToken) {
+  NavigateTwiceAndCheckHeader(ua_reduced_with_valid_origin_trial_token_url(),
+                              /*ch_ua_reduced_expected=*/true,
+                              /*critical_ch_ua_reduced_expected=*/false);
+}
+
+IN_PROC_BROWSER_TEST_F(UaReducedOriginTrialBrowserTest,
+                       AcceptChUaReducedWithInvalidOriginTrialToken) {
+  // The response contained Sec-CH-UA-Reduced in the Accept-CH header, but the
+  // origin trial token is invalid.
+  NavigateTwiceAndCheckHeader(ua_reduced_with_invalid_origin_trial_token_url(),
+                              /*ch_ua_reduced_expected=*/false,
+                              /*critical_ch_ua_reduced_expected=*/false);
+}
+
+IN_PROC_BROWSER_TEST_F(UaReducedOriginTrialBrowserTest,
+                       AcceptChUaReducedWithNoOriginTrialToken) {
+  // The response contained Sec-CH-UA-Reduced in the Accept-CH header, but the
+  // origin trial token is not present.
+  NavigateTwiceAndCheckHeader(ua_reduced_with_no_origin_trial_token_url(),
+                              /*ch_ua_reduced_expected=*/false,
+                              /*critical_ch_ua_reduced_expected=*/false);
+}
+
+IN_PROC_BROWSER_TEST_F(UaReducedOriginTrialBrowserTest,
+                       NoAcceptChUaReducedWithValidOriginTrialToken) {
+  // The response contained a valid Origin Trial token, but no Sec-CH-UA-Reduced
+  // in the Accept-CH header.
+  NavigateTwiceAndCheckHeader(
+      ua_reduced_missing_with_valid_origin_trial_token_url(),
+      /*ch_ua_reduced_expected=*/false,
+      /*critical_ch_ua_reduced_expected=*/false);
+}
+
+IN_PROC_BROWSER_TEST_F(UaReducedOriginTrialBrowserTest,
+                       CriticalChUaReducedWithValidOriginTrialToken) {
+  // The initial navigation also contains the Critical-CH header, so the
+  // Sec-CH-UA-Reduced header should be set after the first navigation.
+  NavigateTwiceAndCheckHeader(
+      critical_ch_ua_reduced_with_valid_origin_trial_token_url(),
+      /*ch_ua_reduced_expected=*/true,
+      /*critical_ch_ua_reduced_expected=*/true);
+}
+
+IN_PROC_BROWSER_TEST_F(UaReducedOriginTrialBrowserTest,
+                       CriticalChUaReducedWithInvalidOriginTrialToken) {
+  // The Origin Trial token is invalid, so the Critical-CH should not have
+  // resulted in the Sec-CH-UA-Reduced header being sent.
+  NavigateTwiceAndCheckHeader(
+      critical_ch_ua_reduced_with_invalid_origin_trial_token_url(),
+      /*ch_ua_reduced_expected=*/false,
+      /*critical_ch_ua_reduced_expected=*/false);
+}
+
+IN_PROC_BROWSER_TEST_F(UaReducedOriginTrialBrowserTest,
+                       ThirdPartyUaReducedWithValidOriginTrialToken) {
+  // TODO(crbug.com/1222742): Implement this test.
+}
diff --git a/chrome/test/data/client_hints/accept_ch_ua_reduced_missing_valid_origin_trial.html b/chrome/test/data/client_hints/accept_ch_ua_reduced_missing_valid_origin_trial.html
new file mode 100644
index 0000000..c340ceb
--- /dev/null
+++ b/chrome/test/data/client_hints/accept_ch_ua_reduced_missing_valid_origin_trial.html
@@ -0,0 +1,5 @@
+<html>
+<link rel="icon" href="data:;base64,=">
+<head></head>
+The Origin Trial token is valid, but the Sec-CH-UA-Reduced header is not present.
+</html>
diff --git a/chrome/test/data/client_hints/accept_ch_ua_reduced_missing_valid_origin_trial.html.mock-http-headers b/chrome/test/data/client_hints/accept_ch_ua_reduced_missing_valid_origin_trial.html.mock-http-headers
new file mode 100644
index 0000000..00c98741
--- /dev/null
+++ b/chrome/test/data/client_hints/accept_ch_ua_reduced_missing_valid_origin_trial.html.mock-http-headers
@@ -0,0 +1,4 @@
+HTTP/1.1 200 OK
+Content-Type: text/html
+Accept-CH: sec-ch-ua-full-version
+Origin-Trial: A93QtcQ0CRKf5ioPasUwNbweXQWgbI4ZEshiz+YS7dkQEWVfW9Ua2pTnA866sZwRzuElkPwsUdGdIaW0fRUP8AwAAABceyJvcmlnaW4iOiAiaHR0cHM6Ly8xMjcuMC4wLjE6NDQ0NDQiLCAiZmVhdHVyZSI6ICJVc2VyQWdlbnRSZWR1Y3Rpb24iLCAiZXhwaXJ5IjogMjAwMDAwMDAwMH0=
diff --git a/chrome/test/data/client_hints/accept_ch_ua_reduced_with_invalid_origin_trial.html b/chrome/test/data/client_hints/accept_ch_ua_reduced_with_invalid_origin_trial.html
new file mode 100644
index 0000000..d291bbe2
--- /dev/null
+++ b/chrome/test/data/client_hints/accept_ch_ua_reduced_with_invalid_origin_trial.html
@@ -0,0 +1,7 @@
+<html>
+<link rel="icon" href="data:;base64,=">
+<head></head>
+Empty file which uses link-rel to disable favicon fetches. The corresponding
+.mock-http-headers sets client hints.  The Origin Trial token in the headers
+file is a corruption of the valid Origin Trial token.
+</html>
diff --git a/chrome/test/data/client_hints/accept_ch_ua_reduced_with_invalid_origin_trial.html.mock-http-headers b/chrome/test/data/client_hints/accept_ch_ua_reduced_with_invalid_origin_trial.html.mock-http-headers
new file mode 100644
index 0000000..f9543f9
--- /dev/null
+++ b/chrome/test/data/client_hints/accept_ch_ua_reduced_with_invalid_origin_trial.html.mock-http-headers
@@ -0,0 +1,4 @@
+HTTP/1.1 200 OK
+Content-Type: text/html
+Accept-CH: sec-ch-ua-full-version,sec-ch-ua-reduced
+Origin-Trial: A23QtcQ0CRKf5ioPasUwNbweXQWgbI4ZEshiz+YS7dkQEWVfW9Ua2pTnA866sZwRzuElkPwsUdGdIaW0fRUP8AwAAABceyJvcmlnbW4iOiAiaHR0cHM6Ly8xMjcuMC4wLjE6NDQ0NDQiLCAiZmVhdHVyZSI6ICJVc2VyQWdlbnRSZWR1Y3Rpb24iLCAiZXhwaXJ5IjogMjAwMDAwMDAwMH0=
diff --git a/chrome/test/data/client_hints/accept_ch_ua_reduced_with_no_origin_trial.html b/chrome/test/data/client_hints/accept_ch_ua_reduced_with_no_origin_trial.html
new file mode 100644
index 0000000..8988d58
--- /dev/null
+++ b/chrome/test/data/client_hints/accept_ch_ua_reduced_with_no_origin_trial.html
@@ -0,0 +1,8 @@
+<html>
+<link rel="icon" href="data:;base64,=">
+<head></head>
+Empty file which uses link-rel to disable favicon fetches. The corresponding
+.mock-http-headers sets client hints.  The Origin Trial token in the headers
+file is missing, even though the Sec-CH-UA-Reduced client hint is present in
+the Accept-CH header. 
+</html>
diff --git a/chrome/test/data/client_hints/accept_ch_ua_reduced_with_no_origin_trial.html.mock-http-headers b/chrome/test/data/client_hints/accept_ch_ua_reduced_with_no_origin_trial.html.mock-http-headers
new file mode 100644
index 0000000..ec93464
--- /dev/null
+++ b/chrome/test/data/client_hints/accept_ch_ua_reduced_with_no_origin_trial.html.mock-http-headers
@@ -0,0 +1,3 @@
+HTTP/1.1 200 OK
+Content-Type: text/html
+Accept-CH: sec-ch-ua-full-version,sec-ch-ua-reduced
diff --git a/chrome/test/data/client_hints/accept_ch_ua_reduced_with_valid_origin_trial.html b/chrome/test/data/client_hints/accept_ch_ua_reduced_with_valid_origin_trial.html
new file mode 100644
index 0000000..db1b984
--- /dev/null
+++ b/chrome/test/data/client_hints/accept_ch_ua_reduced_with_valid_origin_trial.html
@@ -0,0 +1,8 @@
+<html>
+<link rel="icon" href="data:;base64,=">
+<head></head>
+Empty file which uses link-rel to disable favicon fetches. The corresponding
+.mock-http-headers sets client hints.  The Origin Trial token was generated by
+running (in tools/origin_trials):
+generate_token.py https://127.0.0.1:44444 UserAgentReduction --expire-timestamp=2000000000
+</html>
diff --git a/chrome/test/data/client_hints/accept_ch_ua_reduced_with_valid_origin_trial.html.mock-http-headers b/chrome/test/data/client_hints/accept_ch_ua_reduced_with_valid_origin_trial.html.mock-http-headers
new file mode 100644
index 0000000..1d3bf6e
--- /dev/null
+++ b/chrome/test/data/client_hints/accept_ch_ua_reduced_with_valid_origin_trial.html.mock-http-headers
@@ -0,0 +1,4 @@
+HTTP/1.1 200 OK
+Content-Type: text/html
+Accept-CH: sec-ch-ua-full-version,sec-ch-ua-reduced
+Origin-Trial: A93QtcQ0CRKf5ioPasUwNbweXQWgbI4ZEshiz+YS7dkQEWVfW9Ua2pTnA866sZwRzuElkPwsUdGdIaW0fRUP8AwAAABceyJvcmlnaW4iOiAiaHR0cHM6Ly8xMjcuMC4wLjE6NDQ0NDQiLCAiZmVhdHVyZSI6ICJVc2VyQWdlbnRSZWR1Y3Rpb24iLCAiZXhwaXJ5IjogMjAwMDAwMDAwMH0=
diff --git a/chrome/test/data/client_hints/critical_ch_ua_reduced_with_invalid_origin_trial.html b/chrome/test/data/client_hints/critical_ch_ua_reduced_with_invalid_origin_trial.html
new file mode 100644
index 0000000..9666ba7
--- /dev/null
+++ b/chrome/test/data/client_hints/critical_ch_ua_reduced_with_invalid_origin_trial.html
@@ -0,0 +1,7 @@
+<html>
+<link rel="icon" href="data:;base64,=">
+<head></head>
+Empty file which uses link-rel to disable favicon fetches. The corresponding
+.mock-http-headers sets client hints.  The Origin Trial token is a corrupted
+version of the original.
+</html>
diff --git a/chrome/test/data/client_hints/critical_ch_ua_reduced_with_invalid_origin_trial.html.mock-http-headers b/chrome/test/data/client_hints/critical_ch_ua_reduced_with_invalid_origin_trial.html.mock-http-headers
new file mode 100644
index 0000000..8452ef6
--- /dev/null
+++ b/chrome/test/data/client_hints/critical_ch_ua_reduced_with_invalid_origin_trial.html.mock-http-headers
@@ -0,0 +1,5 @@
+HTTP/1.1 200 OK
+Content-Type: text/html
+Accept-CH: sec-ch-ua-full-version,sec-ch-ua-reduced
+Critical-CH: sec-ch-ua-reduced
+Origin-Trial: A23QtcQ0CRKf5ioPasUwNbweXQWgbI4ZEshiz+YS7dkQEWVfW9Ua2pTnA866sZwRzsElkPwsUdGdIaW0fRUP8AwAAABceyJvcmlnaW4iOiAiaHR0cHM6Ly8xMjcuMC4wLjE6NDQ0NDQiLCAiZmVhdHVyZSI6ICJVc2VyQWdlbnRSZWR1Y3Rpb24iLCAiZXhwaXJ5IjogMjAwMDAwMDAwMH0=
diff --git a/chrome/test/data/client_hints/critical_ch_ua_reduced_with_valid_origin_trial.html b/chrome/test/data/client_hints/critical_ch_ua_reduced_with_valid_origin_trial.html
new file mode 100644
index 0000000..db1b984
--- /dev/null
+++ b/chrome/test/data/client_hints/critical_ch_ua_reduced_with_valid_origin_trial.html
@@ -0,0 +1,8 @@
+<html>
+<link rel="icon" href="data:;base64,=">
+<head></head>
+Empty file which uses link-rel to disable favicon fetches. The corresponding
+.mock-http-headers sets client hints.  The Origin Trial token was generated by
+running (in tools/origin_trials):
+generate_token.py https://127.0.0.1:44444 UserAgentReduction --expire-timestamp=2000000000
+</html>
diff --git a/chrome/test/data/client_hints/critical_ch_ua_reduced_with_valid_origin_trial.html.mock-http-headers b/chrome/test/data/client_hints/critical_ch_ua_reduced_with_valid_origin_trial.html.mock-http-headers
new file mode 100644
index 0000000..426c2fd
--- /dev/null
+++ b/chrome/test/data/client_hints/critical_ch_ua_reduced_with_valid_origin_trial.html.mock-http-headers
@@ -0,0 +1,5 @@
+HTTP/1.1 200 OK
+Content-Type: text/html
+Accept-CH: sec-ch-ua-full-version,sec-ch-ua-reduced
+Critical-CH: sec-ch-ua-reduced
+Origin-Trial: A93QtcQ0CRKf5ioPasUwNbweXQWgbI4ZEshiz+YS7dkQEWVfW9Ua2pTnA866sZwRzuElkPwsUdGdIaW0fRUP8AwAAABceyJvcmlnaW4iOiAiaHR0cHM6Ly8xMjcuMC4wLjE6NDQ0NDQiLCAiZmVhdHVyZSI6ICJVc2VyQWdlbnRSZWR1Y3Rpb24iLCAiZXhwaXJ5IjogMjAwMDAwMDAwMH0=
diff --git a/content/browser/client_hints/client_hints.cc b/content/browser/client_hints/client_hints.cc
index 4ed9891..876c026 100644
--- a/content/browser/client_hints/client_hints.cc
+++ b/content/browser/client_hints/client_hints.cc
@@ -18,6 +18,7 @@
 #include "content/browser/devtools/devtools_instrumentation.h"
 #include "content/browser/renderer_host/frame_tree.h"
 #include "content/browser/renderer_host/frame_tree_node.h"
+#include "content/browser/renderer_host/navigation_request.h"
 #include "content/browser/renderer_host/navigator.h"
 #include "content/browser/renderer_host/navigator_delegate.h"
 #include "content/browser/renderer_host/render_view_host_impl.h"
@@ -28,6 +29,8 @@
 #include "content/public/common/content_features.h"
 #include "content/public/common/content_switches.h"
 #include "net/base/url_util.h"
+#include "net/http/http_request_headers.h"
+#include "net/http/http_response_headers.h"
 #include "net/http/structured_headers.h"
 #include "net/nqe/effective_connection_type.h"
 #include "net/nqe/network_quality_estimator_params.h"
@@ -47,6 +50,8 @@
 namespace content {
 
 namespace {
+using ::network::mojom::WebClientHintsType;
+
 uint8_t randomization_salt = 0;
 
 constexpr size_t kMaxRandomNumbers = 21;
@@ -197,7 +202,7 @@
 }
 
 void SetHeaderToDouble(net::HttpRequestHeaders* headers,
-                       network::mojom::WebClientHintsType client_hint_type,
+                       WebClientHintsType client_hint_type,
                        double value) {
   headers->SetHeader(
       blink::kClientHintsHeaderMapping[static_cast<int>(client_hint_type)],
@@ -205,7 +210,7 @@
 }
 
 void SetHeaderToInt(net::HttpRequestHeaders* headers,
-                    network::mojom::WebClientHintsType client_hint_type,
+                    WebClientHintsType client_hint_type,
                     double value) {
   headers->SetHeader(
       blink::kClientHintsHeaderMapping[static_cast<int>(client_hint_type)],
@@ -213,14 +218,14 @@
 }
 
 void SetHeaderToString(net::HttpRequestHeaders* headers,
-                       network::mojom::WebClientHintsType client_hint_type,
+                       WebClientHintsType client_hint_type,
                        const std::string& value) {
   headers->SetHeader(
       blink::kClientHintsHeaderMapping[static_cast<int>(client_hint_type)],
       value);
 }
 
-void RemoveClientHintHeader(network::mojom::WebClientHintsType client_hint_type,
+void RemoveClientHintHeader(WebClientHintsType client_hint_type,
                             net::HttpRequestHeaders* headers) {
   headers->RemoveHeader(
       blink::kClientHintsHeaderMapping[static_cast<int>(client_hint_type)]);
@@ -232,8 +237,7 @@
   const float device_memory =
       blink::ApproximatedDeviceMemory::GetApproximatedDeviceMemory();
   DCHECK_LT(0.0, device_memory);
-  SetHeaderToDouble(headers, network::mojom::WebClientHintsType::kDeviceMemory,
-                    device_memory);
+  SetHeaderToDouble(headers, WebClientHintsType::kDeviceMemory, device_memory);
 }
 
 void AddDPRHeader(net::HttpRequestHeaders* headers,
@@ -243,7 +247,7 @@
   DCHECK(context);
   double device_scale_factor = GetDeviceScaleFactor();
   double zoom_factor = GetZoomFactor(context, url);
-  SetHeaderToDouble(headers, network::mojom::WebClientHintsType::kDpr,
+  SetHeaderToDouble(headers, WebClientHintsType::kDpr,
                     device_scale_factor * zoom_factor);
 }
 
@@ -267,8 +271,7 @@
   DCHECK_LT(0, viewport_width);
   // TODO(yoav): Find out why this 0 check is needed...
   if (viewport_width > 0) {
-    SetHeaderToInt(headers, network::mojom::WebClientHintsType::kViewportWidth,
-                   viewport_width);
+    SetHeaderToInt(headers, WebClientHintsType::kViewportWidth, viewport_width);
   }
 }
 
@@ -290,7 +293,7 @@
     http_rtt = net::NetworkQualityEstimatorParams::GetDefaultTypicalHttpRtt(
         net::EFFECTIVE_CONNECTION_TYPE_UNKNOWN);
   }
-  SetHeaderToInt(headers, network::mojom::WebClientHintsType::kRtt,
+  SetHeaderToInt(headers, WebClientHintsType::kRtt,
                  RoundRtt(url.host(), http_rtt));
 }
 
@@ -316,7 +319,7 @@
             net::EFFECTIVE_CONNECTION_TYPE_UNKNOWN);
   }
 
-  SetHeaderToDouble(headers, network::mojom::WebClientHintsType::kDownlink,
+  SetHeaderToDouble(headers, WebClientHintsType::kDownlink,
                     RoundKbpsToMbps(url.host(), downlink_throughput_kbps));
 }
 
@@ -344,13 +347,13 @@
   }
 
   SetHeaderToString(
-      headers, network::mojom::WebClientHintsType::kEct,
+      headers, WebClientHintsType::kEct,
       blink::kWebEffectiveConnectionTypeMapping[effective_connection_type]);
 }
 
 void AddLangHeader(net::HttpRequestHeaders* headers, BrowserContext* context) {
   SetHeaderToString(
-      headers, network::mojom::WebClientHintsType::kLang,
+      headers, WebClientHintsType::kLang,
       blink::SerializeLangClientHint(
           GetContentClient()->browser()->GetAcceptLangs(context)));
 }
@@ -363,8 +366,7 @@
       frame_tree_node->current_frame_host()->GetPreferredColorScheme();
   bool is_dark_mode =
       preferred_color_scheme == blink::mojom::PreferredColorScheme::kDark;
-  SetHeaderToString(headers,
-                    network::mojom::WebClientHintsType::kPrefersColorScheme,
+  SetHeaderToString(headers, WebClientHintsType::kPrefersColorScheme,
                     is_dark_mode ? "dark" : "light");
 }
 
@@ -383,15 +385,18 @@
 }
 
 void AddUAHeader(net::HttpRequestHeaders* headers,
-                 network::mojom::WebClientHintsType type,
+                 WebClientHintsType type,
                  const std::string& value) {
   SetHeaderToString(headers, type, value);
 }
 
-// Use structured headers to escape and quote headers
-std::string SerializeHeaderString(std::string str) {
+// Creates a serialized string header value out of the input type, using
+// structured headers as described in
+// https://www.rfc-editor.org/rfc/rfc8941.html.
+template <typename T>
+const std::string SerializeHeaderString(const T& value) {
   return net::structured_headers::SerializeItem(
-             net::structured_headers::Item(str))
+             net::structured_headers::Item(value))
       .value_or(std::string());
 }
 
@@ -436,7 +441,7 @@
 };
 
 bool IsClientHintAllowed(const ClientHintsExtendedData& data,
-                         network::mojom::WebClientHintsType type) {
+                         WebClientHintsType type) {
   if (!IsPermissionsPolicyForClientHintsEnabled() || data.is_main_frame)
     return data.is_1p_origin;
   return data.permissions_policy &&
@@ -447,7 +452,7 @@
 }
 
 bool ShouldAddClientHint(const ClientHintsExtendedData& data,
-                         network::mojom::WebClientHintsType type) {
+                         WebClientHintsType type) {
   if (!blink::IsClientHintSentByDefault(type) && !data.hints.IsEnabled(type))
     return false;
   return IsClientHintAllowed(data, type);
@@ -509,70 +514,60 @@
     // Permissions Policy.
     //
     // https://wicg.github.io/client-hints-infrastructure/#abstract-opdef-append-client-hints-to-request
-    if (ShouldAddClientHint(data, network::mojom::WebClientHintsType::kUA)) {
-      AddUAHeader(headers, network::mojom::WebClientHintsType::kUA,
+    if (ShouldAddClientHint(data, WebClientHintsType::kUA)) {
+      AddUAHeader(headers, WebClientHintsType::kUA,
                   ua_metadata->SerializeBrandVersionList());
     }
     // The `Sec-CH-UA-Mobile client hint was also deemed "low entropy" and can
     // safely be sent with every request. Similarly to UA, ShouldAddClientHints
     // makes sure it's controlled by Permissions Policy.
-    if (ShouldAddClientHint(data,
-                            network::mojom::WebClientHintsType::kUAMobile)) {
-      AddUAHeader(headers, network::mojom::WebClientHintsType::kUAMobile,
-                  ua_metadata->mobile ? "?1" : "?0");
+    if (ShouldAddClientHint(data, WebClientHintsType::kUAMobile)) {
+      AddUAHeader(headers, WebClientHintsType::kUAMobile,
+                  SerializeHeaderString(ua_metadata->mobile));
     }
 
-    if (ShouldAddClientHint(
-            data, network::mojom::WebClientHintsType::kUAFullVersion)) {
-      AddUAHeader(headers, network::mojom::WebClientHintsType::kUAFullVersion,
+    if (ShouldAddClientHint(data, WebClientHintsType::kUAFullVersion)) {
+      AddUAHeader(headers, WebClientHintsType::kUAFullVersion,
                   SerializeHeaderString(ua_metadata->full_version));
     }
 
-    if (ShouldAddClientHint(data,
-                            network::mojom::WebClientHintsType::kUAArch)) {
-      AddUAHeader(headers, network::mojom::WebClientHintsType::kUAArch,
+    if (ShouldAddClientHint(data, WebClientHintsType::kUAArch)) {
+      AddUAHeader(headers, WebClientHintsType::kUAArch,
                   SerializeHeaderString(ua_metadata->architecture));
     }
 
-    if (ShouldAddClientHint(data,
-                            network::mojom::WebClientHintsType::kUAPlatform)) {
-      AddUAHeader(headers, network::mojom::WebClientHintsType::kUAPlatform,
+    if (ShouldAddClientHint(data, WebClientHintsType::kUAPlatform)) {
+      AddUAHeader(headers, WebClientHintsType::kUAPlatform,
                   SerializeHeaderString(ua_metadata->platform));
     }
 
-    if (ShouldAddClientHint(
-            data, network::mojom::WebClientHintsType::kUAPlatformVersion)) {
-      AddUAHeader(headers,
-                  network::mojom::WebClientHintsType::kUAPlatformVersion,
+    if (ShouldAddClientHint(data, WebClientHintsType::kUAPlatformVersion)) {
+      AddUAHeader(headers, WebClientHintsType::kUAPlatformVersion,
                   SerializeHeaderString(ua_metadata->platform_version));
     }
 
-    if (ShouldAddClientHint(data,
-                            network::mojom::WebClientHintsType::kUAModel)) {
-      AddUAHeader(headers, network::mojom::WebClientHintsType::kUAModel,
+    if (ShouldAddClientHint(data, WebClientHintsType::kUAModel)) {
+      AddUAHeader(headers, WebClientHintsType::kUAModel,
                   SerializeHeaderString(ua_metadata->model));
     }
-    if (ShouldAddClientHint(data,
-                            network::mojom::WebClientHintsType::kUABitness)) {
-      AddUAHeader(headers, network::mojom::WebClientHintsType::kUABitness,
+    if (ShouldAddClientHint(data, WebClientHintsType::kUABitness)) {
+      AddUAHeader(headers, WebClientHintsType::kUABitness,
                   SerializeHeaderString(ua_metadata->bitness));
     }
+    if (ShouldAddClientHint(data, WebClientHintsType::kUAReduced)) {
+      AddUAHeader(headers, WebClientHintsType::kUAReduced,
+                  SerializeHeaderString(true));
+    }
   } else if (call_type == ClientUaHeaderCallType::kAfterCreated) {
-    RemoveClientHintHeader(network::mojom::WebClientHintsType::kUA, headers);
-    RemoveClientHintHeader(network::mojom::WebClientHintsType::kUAMobile,
-                           headers);
-    RemoveClientHintHeader(network::mojom::WebClientHintsType::kUAFullVersion,
-                           headers);
-    RemoveClientHintHeader(network::mojom::WebClientHintsType::kUAArch,
-                           headers);
-    RemoveClientHintHeader(network::mojom::WebClientHintsType::kUAPlatform,
-                           headers);
-    RemoveClientHintHeader(
-        network::mojom::WebClientHintsType::kUAPlatformVersion, headers);
-    RemoveClientHintHeader(network::mojom::WebClientHintsType::kUAModel,
-                           headers);
-    RemoveClientHintHeader(network::mojom::WebClientHintsType::kUABitness,
-                           headers);
+    RemoveClientHintHeader(WebClientHintsType::kUA, headers);
+    RemoveClientHintHeader(WebClientHintsType::kUAMobile, headers);
+    RemoveClientHintHeader(WebClientHintsType::kUAFullVersion, headers);
+    RemoveClientHintHeader(WebClientHintsType::kUAArch, headers);
+    RemoveClientHintHeader(WebClientHintsType::kUAPlatform, headers);
+    RemoveClientHintHeader(WebClientHintsType::kUAPlatformVersion, headers);
+    RemoveClientHintHeader(WebClientHintsType::kUAModel, headers);
+    RemoveClientHintHeader(WebClientHintsType::kUABitness, headers);
+    RemoveClientHintHeader(WebClientHintsType::kUAReduced, headers);
   }
 }
 
@@ -638,30 +633,27 @@
   }
 
   // Add Headers
-  if (ShouldAddClientHint(data,
-                          network::mojom::WebClientHintsType::kDeviceMemory)) {
+  if (ShouldAddClientHint(data, WebClientHintsType::kDeviceMemory)) {
     AddDeviceMemoryHeader(headers);
   }
-  if (ShouldAddClientHint(data, network::mojom::WebClientHintsType::kDpr)) {
+  if (ShouldAddClientHint(data, WebClientHintsType::kDpr)) {
     AddDPRHeader(headers, context, url);
   }
-  if (ShouldAddClientHint(data,
-                          network::mojom::WebClientHintsType::kViewportWidth)) {
+  if (ShouldAddClientHint(data, WebClientHintsType::kViewportWidth)) {
     AddViewportWidthHeader(headers, context, url);
   }
   network::NetworkQualityTracker* network_quality_tracker =
       delegate->GetNetworkQualityTracker();
-  if (ShouldAddClientHint(data, network::mojom::WebClientHintsType::kRtt)) {
+  if (ShouldAddClientHint(data, WebClientHintsType::kRtt)) {
     AddRttHeader(headers, network_quality_tracker, url);
   }
-  if (ShouldAddClientHint(data,
-                          network::mojom::WebClientHintsType::kDownlink)) {
+  if (ShouldAddClientHint(data, WebClientHintsType::kDownlink)) {
     AddDownlinkHeader(headers, network_quality_tracker, url);
   }
-  if (ShouldAddClientHint(data, network::mojom::WebClientHintsType::kEct)) {
+  if (ShouldAddClientHint(data, WebClientHintsType::kEct)) {
     AddEctHeader(headers, network_quality_tracker, url);
   }
-  if (ShouldAddClientHint(data, network::mojom::WebClientHintsType::kLang)) {
+  if (ShouldAddClientHint(data, WebClientHintsType::kLang)) {
     AddLangHeader(headers, context);
   }
 
@@ -671,8 +663,7 @@
         ClientUaHeaderCallType::kDuringCreation, headers);
   }
 
-  if (ShouldAddClientHint(
-          data, network::mojom::WebClientHintsType::kPrefersColorScheme)) {
+  if (ShouldAddClientHint(data, WebClientHintsType::kPrefersColorScheme)) {
     AddPrefersColorSchemeHeader(headers, frame_tree_node);
   }
 
@@ -681,8 +672,7 @@
   // If possible, logic should be added above so that the request headers for
   // the newly added client hint can be added to the request.
   static_assert(
-      network::mojom::WebClientHintsType::kUAReduced ==
-          network::mojom::WebClientHintsType::kMaxValue,
+      WebClientHintsType::kUAReduced == WebClientHintsType::kMaxValue,
       "Consider adding client hint request headers from the browser process");
 
   // TODO(crbug.com/735518): If the request is redirected, the client hint
@@ -743,18 +733,19 @@
                                container_policy);
 }
 
-absl::optional<std::vector<network::mojom::WebClientHintsType>>
+absl::optional<std::vector<WebClientHintsType>>
 ParseAndPersistAcceptCHForNavigation(
     const GURL& url,
-    const ::network::mojom::ParsedHeadersPtr& headers,
+    const network::mojom::ParsedHeadersPtr& parsed_headers,
+    const net::HttpResponseHeaders* response_headers,
     BrowserContext* context,
     ClientHintsControllerDelegate* delegate,
     FrameTreeNode* frame_tree_node) {
   DCHECK_CURRENTLY_ON(BrowserThread::UI);
   DCHECK(context);
-  DCHECK(headers);
+  DCHECK(parsed_headers);
 
-  if (!headers->accept_ch)
+  if (!parsed_headers->accept_ch)
     return absl::nullopt;
 
   if (!IsValidURLForClientHints(url))
@@ -774,6 +765,13 @@
   if (!frame_tree_node->IsMainFrame())
     return absl::nullopt;
 
+  blink::EnabledClientHints enabled_hints;
+  for (const WebClientHintsType type : parsed_headers->accept_ch.value()) {
+    enabled_hints.SetIsEnabled(url, response_headers, type, true);
+  }
+  const std::vector<WebClientHintsType> persisted_hints =
+      enabled_hints.GetEnabledHints();
+
   base::TimeDelta persist_duration;
   if (IsPermissionsPolicyForClientHintsEnabled()) {
     // JSON cannot store "non-finite" values (i.e. NaN or infinite) so
@@ -782,42 +780,35 @@
     // large was chosen instead
     persist_duration = base::TimeDelta::FromDays(1000000);
   } else {
-    persist_duration = headers->accept_ch_lifetime;
+    persist_duration = parsed_headers->accept_ch_lifetime;
     if (persist_duration.is_zero())
-      return headers->accept_ch;
+      return persisted_hints;
   }
 
-  delegate->PersistClientHints(url::Origin::Create(url),
-                               headers->accept_ch.value(), persist_duration);
+  delegate->PersistClientHints(url::Origin::Create(url), persisted_hints,
+                               persist_duration);
 
-  return headers->accept_ch;
+  return persisted_hints;
 }
 
-CONTENT_EXPORT std::vector<::network::mojom::WebClientHintsType>
-LookupAcceptCHForCommit(const GURL& url,
-                        ClientHintsControllerDelegate* delegate,
-                        FrameTreeNode* frame_tree_node) {
-  std::vector<::network::mojom::WebClientHintsType> result;
+CONTENT_EXPORT std::vector<WebClientHintsType> LookupAcceptCHForCommit(
+    const GURL& url,
+    ClientHintsControllerDelegate* delegate,
+    FrameTreeNode* frame_tree_node) {
+  std::vector<WebClientHintsType> result;
   if (!ShouldAddClientHints(url, frame_tree_node, delegate)) {
     return result;
   }
 
   const ClientHintsExtendedData data(url, frame_tree_node, delegate);
-  for (int v = 0;
-       v <= static_cast<int>(network::mojom::WebClientHintsType::kMaxValue);
-       ++v) {
-    auto hint = static_cast<network::mojom::WebClientHintsType>(v);
-    if (data.hints.IsEnabled(hint))
-      result.push_back(hint);
-  }
-  return result;
+  return data.hints.GetEnabledHints();
 }
 
 bool AreCriticalHintsMissing(
     const GURL& url,
     FrameTreeNode* frame_tree_node,
     ClientHintsControllerDelegate* delegate,
-    const std::vector<network::mojom::WebClientHintsType>& critical_hints) {
+    const std::vector<WebClientHintsType>& critical_hints) {
   ClientHintsExtendedData data(url, frame_tree_node, delegate);
 
   // Note: these only check for per-hint origin/permissions policy settings, not
diff --git a/content/browser/client_hints/client_hints.h b/content/browser/client_hints/client_hints.h
index 9c0bccd1..3c6d7389 100644
--- a/content/browser/client_hints/client_hints.h
+++ b/content/browser/client_hints/client_hints.h
@@ -13,6 +13,10 @@
 #include "services/network/public/mojom/parsed_headers.mojom-forward.h"
 #include "third_party/blink/public/common/permissions_policy/permissions_policy.h"
 
+namespace net {
+class HttpResponseHeaders;
+}  // namespace net
+
 namespace content {
 
 class BrowserContext;
@@ -82,10 +86,17 @@
 // persisted. The distinction is relevant in legacy case where permissions
 // policy is off and there is no valid Accept-CH-Lifetime, where the header
 // still applies locally within frame.
+//
+// The ParsedHeaders are used to retrieve the already parsed Accept-CH header
+// values. The HttpResponseHeaders are not meant to be used by non-sandboxed
+// processes, but here, we just pass the HttpRequestHeaders to the
+// TrialTokenValidator library.  There is precedent for calling the
+// TrialTokenValidator from the browser process, see crrev.com/c/2142580.
 CONTENT_EXPORT absl::optional<std::vector<network::mojom::WebClientHintsType>>
 ParseAndPersistAcceptCHForNavigation(
     const GURL& url,
-    const ::network::mojom::ParsedHeadersPtr& headers,
+    const network::mojom::ParsedHeadersPtr& parsed_headers,
+    const net::HttpResponseHeaders* response_headers,
     BrowserContext* context,
     ClientHintsControllerDelegate* delegate,
     FrameTreeNode*);
diff --git a/content/browser/client_hints/critical_client_hints_throttle.cc b/content/browser/client_hints/critical_client_hints_throttle.cc
index cbf620c..a173549 100644
--- a/content/browser/client_hints/critical_client_hints_throttle.cc
+++ b/content/browser/client_hints/critical_client_hints_throttle.cc
@@ -66,7 +66,7 @@
   blink::EnabledClientHints hints;
   for (const WebClientHintsType hint :
        response_head.parsed_headers->accept_ch.value())
-    hints.SetIsEnabled(hint, true);
+    hints.SetIsEnabled(response_url, response_head.headers.get(), hint, true);
 
   std::vector<WebClientHintsType> critical_hints;
   for (const WebClientHintsType hint :
@@ -85,8 +85,8 @@
   if (ShouldRestartWithHints(response_url, critical_hints, modified_headers)) {
     LogCriticalCHStatus(CriticalCHRestart::kNavigationRestarted);
     ParseAndPersistAcceptCHForNavigation(
-        response_url, response_head.parsed_headers, context_,
-        client_hint_delegate_,
+        response_url, response_head.parsed_headers, response_head.headers.get(),
+        context_, client_hint_delegate_,
         FrameTreeNode::GloballyFindByID(frame_tree_node_id_));
     delegate_->RestartWithURLResetAndFlags(/*additional_load_flags=*/0);
   }
diff --git a/content/browser/renderer_host/navigation_request.cc b/content/browser/renderer_host/navigation_request.cc
index 2e70ba9b..37e08af 100644
--- a/content/browser/renderer_host/navigation_request.cc
+++ b/content/browser/renderer_host/navigation_request.cc
@@ -3778,6 +3778,7 @@
     ParseAndPersistAcceptCHForNavigation(
         commit_params_->redirects.back(),
         commit_params_->redirect_response.back()->parsed_headers,
+        commit_params_->redirect_response.back()->headers.get(),
         browser_context, client_hints_delegate, frame_tree_node_);
     AddNavigationRequestClientHintsHeaders(
         common_params_->url, &client_hints_extra_headers, browser_context,
@@ -4169,8 +4170,9 @@
         opt_in_hints_from_response;
     if (response()) {
       opt_in_hints_from_response = ParseAndPersistAcceptCHForNavigation(
-          common_params_->url, response()->parsed_headers, browser_context,
-          client_hints_delegate, frame_tree_node_);
+          common_params_->url, response()->parsed_headers,
+          response()->headers.get(), browser_context, client_hints_delegate,
+          frame_tree_node_);
     }
     commit_params_->enabled_client_hints = LookupAcceptCHForCommit(
         common_params_->url, client_hints_delegate, frame_tree_node_);
diff --git a/content/public/test/url_loader_interceptor.cc b/content/public/test/url_loader_interceptor.cc
index 118f783..0855a5f 100644
--- a/content/public/test/url_loader_interceptor.cc
+++ b/content/public/test/url_loader_interceptor.cc
@@ -31,6 +31,7 @@
 #include "net/http/http_util.h"
 #include "net/test/embedded_test_server/request_handler_util.h"
 #include "services/network/public/cpp/features.h"
+#include "services/network/public/cpp/parsed_headers.h"
 #include "services/network/public/mojom/early_hints.mojom.h"
 #include "services/network/public/mojom/url_loader.mojom.h"
 
@@ -511,6 +512,17 @@
   }
 }
 
+const net::HttpRequestHeaders& URLLoaderInterceptor::GetLastRequestHeaders() {
+  base::AutoLock lock(last_request_lock_);
+  return last_request_headers_;
+}
+
+void URLLoaderInterceptor::SetLastRequestHeaders(
+    const net::HttpRequestHeaders& headers) {
+  base::AutoLock lock(last_request_lock_);
+  last_request_headers_ = headers;
+}
+
 // static
 std::unique_ptr<URLLoaderInterceptor>
 URLLoaderInterceptor::ServeFilesFromDirectoryAtOrigin(
@@ -538,8 +550,9 @@
           return false;
 
         callback.Run(params->url_request.url);
-        content::URLLoaderInterceptor::WriteResponse(full_path,
-                                                     params->client.get());
+        content::URLLoaderInterceptor::WriteResponse(
+            full_path, params->client.get(), /*headers=*/nullptr,
+            /*ssl_info=*/absl::nullopt, /*url=*/params->url_request.url);
         return true;
       }));
 }
@@ -548,13 +561,18 @@
     base::StringPiece headers,
     base::StringPiece body,
     network::mojom::URLLoaderClient* client,
-    absl::optional<net::SSLInfo> ssl_info) {
+    absl::optional<net::SSLInfo> ssl_info,
+    absl::optional<GURL> url) {
   net::HttpResponseInfo info;
   info.headers = base::MakeRefCounted<net::HttpResponseHeaders>(
       net::HttpUtil::AssembleRawHeaders(headers));
   auto response = network::mojom::URLResponseHead::New();
   response->headers = info.headers;
   response->headers->GetMimeType(&response->mime_type);
+  if (url.has_value()) {
+    response->parsed_headers =
+        network::PopulateParsedHeaders(response->headers.get(), *url);
+  }
   response->ssl_info = std::move(ssl_info);
   client->OnReceiveResponse(std::move(response));
 
@@ -565,16 +583,18 @@
     const std::string& relative_path,
     network::mojom::URLLoaderClient* client,
     const std::string* headers,
-    absl::optional<net::SSLInfo> ssl_info) {
+    absl::optional<net::SSLInfo> ssl_info,
+    absl::optional<GURL> url) {
   return WriteResponse(GetDataFilePath(relative_path), client, headers,
-                       std::move(ssl_info));
+                       std::move(ssl_info), std::move(url));
 }
 
 void URLLoaderInterceptor::WriteResponse(
     const base::FilePath& file_path,
     network::mojom::URLLoaderClient* client,
     const std::string* headers,
-    absl::optional<net::SSLInfo> ssl_info) {
+    absl::optional<net::SSLInfo> ssl_info,
+    absl::optional<GURL> url) {
   base::ScopedAllowBlockingForTesting allow_io;
   std::string headers_str;
   if (headers) {
@@ -589,7 +609,8 @@
                     net::test_server::GetContentType(file_path) + "\n\n";
     }
   }
-  WriteResponse(headers_str, ReadFile(file_path), client, std::move(ssl_info));
+  WriteResponse(headers_str, ReadFile(file_path), client, std::move(ssl_info),
+                std::move(url));
 }
 
 MojoResult URLLoaderInterceptor::WriteResponseBody(
@@ -670,8 +691,12 @@
 }
 
 bool URLLoaderInterceptor::Intercept(RequestParams* params) {
-  if (callback_.Run(params))
+  if (callback_.Run(params)) {
+    // Only set the last request headers if the request was actually processed
+    // by the interceptor.
+    SetLastRequestHeaders(params->url_request.headers);
     return true;
+  }
 
   // mock.failed.request is a special request whereby the query indicates what
   // error code to respond with.
diff --git a/content/public/test/url_loader_interceptor.h b/content/public/test/url_loader_interceptor.h
index 0d40094..4490aced 100644
--- a/content/public/test/url_loader_interceptor.h
+++ b/content/public/test/url_loader_interceptor.h
@@ -14,10 +14,12 @@
 #include "base/macros.h"
 #include "base/memory/scoped_refptr.h"
 #include "base/strings/string_piece.h"
+#include "base/synchronization/lock.h"
 #include "mojo/public/cpp/bindings/pending_receiver.h"
 #include "mojo/public/cpp/bindings/pending_remote.h"
 #include "mojo/public/cpp/bindings/remote.h"
 #include "net/base/net_errors.h"
+#include "net/http/http_request_headers.h"
 #include "net/traffic_annotation/network_traffic_annotation.h"
 #include "services/network/public/cpp/resource_request.h"
 #include "services/network/public/mojom/url_loader.mojom.h"
@@ -110,12 +112,14 @@
       base::RepeatingCallback<void(const GURL&)> callback = base::DoNothing());
 
   // Helper methods for use when intercepting.
-  // Writes the given response body, header, and SSL Info to |client|.
+  // Writes the given response body, header, and SSL Info to `client`.
+  // If `url` is present, also computes the ParsedHeaders for the response.
   static void WriteResponse(
       base::StringPiece headers,
       base::StringPiece body,
       network::mojom::URLLoaderClient* client,
-      absl::optional<net::SSLInfo> ssl_info = absl::nullopt);
+      absl::optional<net::SSLInfo> ssl_info = absl::nullopt,
+      absl::optional<GURL> url = absl::nullopt);
 
   // Reads the given path, relative to the root source directory, and writes it
   // to |client|. For headers:
@@ -125,18 +129,21 @@
   //   3) otherwise a simple 200 response will be used, with a Content-Type
   //      guessed from the file extension
   // For SSL info, if |ssl_info| is specified, then it is added to the response.
+  // If `url` is present, also computes the ParsedHeaders for the response.
   static void WriteResponse(
       const std::string& relative_path,
       network::mojom::URLLoaderClient* client,
       const std::string* headers = nullptr,
-      absl::optional<net::SSLInfo> ssl_info = absl::nullopt);
+      absl::optional<net::SSLInfo> ssl_info = absl::nullopt,
+      absl::optional<GURL> url = absl::nullopt);
 
   // Like above, but uses an absolute file path.
   static void WriteResponse(
       const base::FilePath& file_path,
       network::mojom::URLLoaderClient* client,
       const std::string* headers = nullptr,
-      absl::optional<net::SSLInfo> ssl_info = absl::nullopt);
+      absl::optional<net::SSLInfo> ssl_info = absl::nullopt,
+      absl::optional<GURL> url = absl::nullopt);
 
   // Attempts to write |body| to |client| and complete the load with status OK.
   // client->OnReceiveResponse() must have been called prior to this.
@@ -152,6 +159,15 @@
       net::Error error,
       base::OnceClosure ready_callback = {});
 
+  // Returns the request headers of the last request processed by this
+  // interceptor.
+  //
+  // Use this function instead of creating a WebContentsObserver to observe
+  // request headers, if you need the last request headers sent in the event of
+  // resends or redirects, as the NavigationHandle::GetRequestHeaders() function
+  // only returns the initial request's request headers.
+  const net::HttpRequestHeaders& GetLastRequestHeaders();
+
  private:
   class BrowserProcessWrapper;
   class Interceptor;
@@ -185,6 +201,9 @@
   // Called on IO thread at initialization and shutdown.
   void InitializeOnIOThread(base::OnceClosure closure);
 
+  // Sets the request headers of the last request processed by this interceptor.
+  void SetLastRequestHeaders(const net::HttpRequestHeaders& headers);
+
   bool use_runloop_;
   base::OnceClosure ready_callback_;
   InterceptCallback callback_;
@@ -197,6 +216,9 @@
   std::set<std::unique_ptr<URLLoaderFactoryNavigationWrapper>>
       navigation_wrappers_;
 
+  base::Lock last_request_lock_;
+  net::HttpRequestHeaders last_request_headers_ GUARDED_BY(last_request_lock_);
+
   DISALLOW_COPY_AND_ASSIGN(URLLoaderInterceptor);
 };
 
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index b557efb..a10085c 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -2468,6 +2468,7 @@
     "//third_party/blink/public:blink",
     "//third_party/blink/public:test_support",
     "//third_party/blink/public/common:font_enumeration_table_proto",
+    "//third_party/blink/public/common:headers",
     "//third_party/icu",
     "//third_party/inspector_protocol:crdtp_test",
     "//third_party/leveldatabase",
diff --git a/third_party/blink/common/client_hints/enabled_client_hints.cc b/third_party/blink/common/client_hints/enabled_client_hints.cc
index 59226fd..390470c 100644
--- a/third_party/blink/common/client_hints/enabled_client_hints.cc
+++ b/third_party/blink/common/client_hints/enabled_client_hints.cc
@@ -5,7 +5,11 @@
 #include "third_party/blink/public/common/client_hints/enabled_client_hints.h"
 
 #include "base/feature_list.h"
+#include "base/time/time.h"
+#include "net/http/http_response_headers.h"
 #include "third_party/blink/public/common/features.h"
+#include "third_party/blink/public/common/origin_trials/trial_token_validator.h"
+#include "url/gurl.h"
 
 namespace blink {
 
@@ -44,15 +48,37 @@
 }  // namespace
 
 bool EnabledClientHints::IsEnabled(const WebClientHintsType type) const {
-  if (IsDisabledByFeature(type)) {
-    return false;
-  }
   return enabled_types_[static_cast<int>(type)];
 }
 
 void EnabledClientHints::SetIsEnabled(const WebClientHintsType type,
                                       const bool should_send) {
-  enabled_types_[static_cast<int>(type)] = should_send;
+  enabled_types_[static_cast<int>(type)] =
+      IsDisabledByFeature(type) ? false : should_send;
+}
+
+void EnabledClientHints::SetIsEnabled(
+    const GURL& url,
+    const net::HttpResponseHeaders* response_headers,
+    const network::mojom::WebClientHintsType type,
+    const bool should_send) {
+  bool enabled = should_send;
+  if (type == WebClientHintsType::kUAReduced) {
+    enabled &= blink::TrialTokenValidator().RequestEnablesFeature(
+        url, response_headers, "UserAgentReduction", base::Time::Now());
+  }
+  SetIsEnabled(type, enabled);
+}
+
+std::vector<WebClientHintsType> EnabledClientHints::GetEnabledHints() const {
+  std::vector<WebClientHintsType> hints;
+  for (int v = 0; v <= static_cast<int>(WebClientHintsType::kMaxValue); ++v) {
+    const auto hint = static_cast<WebClientHintsType>(v);
+    if (IsEnabled(hint)) {
+      hints.push_back(hint);
+    }
+  }
+  return hints;
 }
 
 }  // namespace blink
diff --git a/third_party/blink/common/client_hints/enabled_client_hints_unittest.cc b/third_party/blink/common/client_hints/enabled_client_hints_unittest.cc
index 77be96a..b28a503 100644
--- a/third_party/blink/common/client_hints/enabled_client_hints_unittest.cc
+++ b/third_party/blink/common/client_hints/enabled_client_hints_unittest.cc
@@ -4,26 +4,93 @@
 
 #include "third_party/blink/public/common/client_hints/enabled_client_hints.h"
 
+#include "base/memory/scoped_refptr.h"
 #include "base/test/scoped_feature_list.h"
+#include "net/http/http_response_headers.h"
+#include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/blink/public/common/features.h"
+#include "third_party/blink/public/common/origin_trials/origin_trial_policy.h"
+#include "third_party/blink/public/common/origin_trials/origin_trial_public_key.h"
+#include "third_party/blink/public/common/origin_trials/trial_token_validator.h"
 
 namespace blink {
 
 using ::network::mojom::WebClientHintsType;
+using ::testing::ElementsAre;
+
+static constexpr char kOriginUrl[] = "https://127.0.0.1:44444";
+static const OriginTrialPublicKey kTestPublicKey = {
+    0x75, 0x10, 0xac, 0xf9, 0x3a, 0x1c, 0xb8, 0xa9, 0x28, 0x70, 0xd2,
+    0x9a, 0xd0, 0x0b, 0x59, 0xe1, 0xac, 0x2b, 0xb7, 0xd5, 0xca, 0x1f,
+    0x64, 0x90, 0x08, 0x8e, 0xa8, 0xe0, 0x56, 0x3a, 0x04, 0xd0,
+};
+// Generated by running (in tools/origin_trials):
+// generate_token.py https://127.0.0.1:44444 UserAgentReduction
+// --expire-timestamp=2000000000
+//
+// The Origin Trial token expires in 2033.  Generate a new token by then, or
+// find a better way to re-generate a test trial token.
+static constexpr char kValidOriginTrialToken[] =
+    "A93QtcQ0CRKf5ioPasUwNbweXQWgbI4ZEshiz+"
+    "YS7dkQEWVfW9Ua2pTnA866sZwRzuElkPwsUdGdIaW0fRUP8AwAAABceyJvcmlnaW4iOiAiaHR0"
+    "cHM6Ly8xMjcuMC4wLjE6NDQ0NDQiLCAiZmVhdHVyZSI6ICJVc2VyQWdlbnRSZWR1Y3Rpb24iLC"
+    "AiZXhwaXJ5IjogMjAwMDAwMDAwMH0=";
+// A slight corruption (changing a character) of kValidOriginTrialToken.
+static constexpr char kInvalidOriginTrialToken[] =
+    "A93QtcQ0CRKf5ioPasUwNbweXQWgbI4ZEshiz+"
+    "YS7dkQEWVfW9Ua2pTnA866sZwRzuElkPwsUdGdIaW0fRUP8AwAAABceyJvcmlnaW4iOiAiaHR0"
+    "cHM6Ly8xMjcuMC4wLjE6NDQ0NDQiLCAiZmVhdHVyzSI6ICJVc2VyQWdlbnRSZWR1Y3Rpb24iLC"
+    "AiZXhwaXJ5IjogMjAwMDAwMDAwMH0=";
 
 class EnabledClientHintsTest : public testing::Test {
  public:
-  EnabledClientHintsTest() {
+  EnabledClientHintsTest()
+      : response_headers_(base::MakeRefCounted<net::HttpResponseHeaders>("")) {
     // The UserAgentClientHint feature is enabled, and the LangClientHintHeader
     // feature is disabled.
     scoped_feature_list_.InitWithFeatures(
         /*enabled_features=*/{blink::features::kUserAgentClientHint},
         /*disabled_features=*/{blink::features::kLangClientHintHeader});
+    TrialTokenValidator::SetOriginTrialPolicyGetter(
+        base::BindRepeating([](OriginTrialPolicy* policy) { return policy; },
+                            base::Unretained(&policy_)));
+    policy_.SetPublicKeys({kTestPublicKey});
+  }
+
+  ~EnabledClientHintsTest() override {
+    TrialTokenValidator::ResetOriginTrialPolicyGetter();
+  }
+
+  const net::HttpResponseHeaders* response_headers() const {
+    return response_headers_.get();
+  }
+
+  void AddHeader(const std::string& header, const std::string& value) {
+    response_headers_->AddHeader(header, value);
   }
 
  private:
+  class TestOriginTrialPolicy : public OriginTrialPolicy {
+   public:
+    bool IsOriginTrialsSupported() const override { return true; }
+    bool IsOriginSecure(const GURL& url) const override {
+      return url.SchemeIs("https");
+    }
+    const std::vector<OriginTrialPublicKey>& GetPublicKeys() const override {
+      return keys_;
+    }
+    void SetPublicKeys(const std::vector<OriginTrialPublicKey>& keys) {
+      keys_ = keys;
+    }
+
+   private:
+    std::vector<OriginTrialPublicKey> keys_;
+  };
+
   base::test::ScopedFeatureList scoped_feature_list_;
+  TestOriginTrialPolicy policy_;
+  scoped_refptr<net::HttpResponseHeaders> response_headers_;
 };
 
 TEST_F(EnabledClientHintsTest, EnabledClientHint) {
@@ -50,4 +117,31 @@
   EXPECT_FALSE(hints.IsEnabled(WebClientHintsType::kLang));
 }
 
+TEST_F(EnabledClientHintsTest,
+       EnabledUaReducedClientHintWithValidOriginTrialToken) {
+  AddHeader("Origin-Trial", kValidOriginTrialToken);
+  EnabledClientHints hints;
+  hints.SetIsEnabled(GURL(kOriginUrl), response_headers(),
+                     WebClientHintsType::kUAReduced, true);
+  EXPECT_TRUE(hints.IsEnabled(WebClientHintsType::kUAReduced));
+}
+
+TEST_F(EnabledClientHintsTest,
+       EnabledUaReducedClientHintWithInvalidOriginTrialToken) {
+  AddHeader("Origin-Trial", kInvalidOriginTrialToken);
+  EnabledClientHints hints;
+  hints.SetIsEnabled(GURL(kOriginUrl), response_headers(),
+                     WebClientHintsType::kUAReduced, true);
+  EXPECT_FALSE(hints.IsEnabled(WebClientHintsType::kUAReduced));
+}
+
+TEST_F(EnabledClientHintsTest, GetEnabledHints) {
+  EnabledClientHints hints;
+  hints.SetIsEnabled(WebClientHintsType::kUAFullVersion, true);
+  hints.SetIsEnabled(WebClientHintsType::kRtt, true);
+  EXPECT_THAT(hints.GetEnabledHints(),
+              ElementsAre(WebClientHintsType::kRtt,
+                          WebClientHintsType::kUAFullVersion));
+}
+
 }  // namespace blink
diff --git a/third_party/blink/public/common/client_hints/enabled_client_hints.h b/third_party/blink/public/common/client_hints/enabled_client_hints.h
index 70fac17..f3b515f 100644
--- a/third_party/blink/public/common/client_hints/enabled_client_hints.h
+++ b/third_party/blink/public/common/client_hints/enabled_client_hints.h
@@ -8,6 +8,13 @@
 #include "services/network/public/mojom/web_client_hints_types.mojom-shared.h"
 #include "third_party/blink/public/common/common_export.h"
 
+// Forward declarations.
+class GURL;
+
+namespace net {
+class HttpResponseHeaders;
+}  // namespace net
+
 namespace blink {
 
 // EnabledClientHints stores all the client hints along with whether the hint
@@ -27,13 +34,30 @@
 
   // Sets the client hint as enabled for sending in an HTTP request header. Even
   // if the client hint header is set to enabled, it is still possible that
-  // other factors (such as feature toggles) could cause the client hint to not
+  // other factors (such as feature toggles) should cause the client hint to not
   // be sent, and in that case, IsEnabled() would return false.
   //
   // If `type` is not a valid WebClientHintsType value, nothing is changed (no
   // client hints get enabled).
   void SetIsEnabled(network::mojom::WebClientHintsType type, bool should_send);
 
+  // Sets the client hint as enabled for sending in an HTTP request header.
+  //
+  // In addition to the client hint checks outlined in the SetIsEnabled()
+  // function above, this function also checks if the origin and/or the
+  // response headers indicate that the client hint should not be enabled (e.g.
+  // if the client hint should only be enabled in an Origin Trial).
+  //
+  // If `type` is not a valid WebClientHintsType value, nothing is changed (no
+  // client hints get enabled).
+  void SetIsEnabled(const GURL& url,
+                    const net::HttpResponseHeaders* response_headers,
+                    network::mojom::WebClientHintsType type,
+                    bool should_send);
+
+  // Returns a list of the enabled client hints.
+  std::vector<network::mojom::WebClientHintsType> GetEnabledHints() const;
+
  private:
   std::array<bool,
              static_cast<int>(network::mojom::WebClientHintsType::kMaxValue) +
diff --git a/third_party/blink/renderer/core/loader/base_fetch_context.cc b/third_party/blink/renderer/core/loader/base_fetch_context.cc
index 07ce40cf..0cda0e6 100644
--- a/third_party/blink/renderer/core/loader/base_fetch_context.cc
+++ b/third_party/blink/renderer/core/loader/base_fetch_context.cc
@@ -35,8 +35,10 @@
 
 namespace {
 
-// Simple function to add quotes to make headers strings.
-const AtomicString SerializeHeaderString(std::string str) {
+// Creates a serialized AtomicString header value out of the input string, using
+// structured headers as described in
+// https://www.rfc-editor.org/rfc/rfc8941.html.
+const AtomicString SerializeStringHeader(std::string str) {
   std::string output;
   if (!str.empty()) {
     output = net::structured_headers::SerializeItem(
@@ -47,6 +49,17 @@
   return AtomicString(output.c_str());
 }
 
+// Creates a serialized AtomicString header value out of the input boolean,
+// using structured headers as described in
+// https://www.rfc-editor.org/rfc/rfc8941.html.
+const AtomicString SerializeBoolHeader(const bool value) {
+  const std::string output = net::structured_headers::SerializeItem(
+                                 net::structured_headers::Item(value))
+                                 .value_or(std::string());
+
+  return AtomicString(output.c_str());
+}
+
 }  // namespace
 
 namespace blink {
@@ -151,9 +164,7 @@
     }
 
     // We also send Sec-CH-UA-Mobile to all hints. It is a one-bit header
-    // identifying if the browser has opted for a "mobile" experience
-    // Formatted using the "sh-boolean" format from:
-    // https://httpwg.org/http-extensions/draft-ietf-httpbis-header-structure.html#boolean
+    // identifying if the browser has opted for a "mobile" experience.
     // ShouldSendClientHint is called to make sure it's controlled by
     // PermissionsPolicy.
     if (ShouldSendClientHint(
@@ -163,7 +174,7 @@
       request.SetHttpHeaderField(
           blink::kClientHintsHeaderMapping[static_cast<size_t>(
               network::mojom::blink::WebClientHintsType::kUAMobile)],
-          ua->mobile ? "?1" : "?0");
+          SerializeBoolHeader(ua->mobile));
     }
   }
 
@@ -293,7 +304,7 @@
       request.SetHttpHeaderField(
           blink::kClientHintsHeaderMapping[static_cast<size_t>(
               network::mojom::blink::WebClientHintsType::kUAArch)],
-          SerializeHeaderString(ua->architecture));
+          SerializeStringHeader(ua->architecture));
     }
 
     if (ShouldSendClientHint(
@@ -303,7 +314,7 @@
       request.SetHttpHeaderField(
           blink::kClientHintsHeaderMapping[static_cast<size_t>(
               network::mojom::blink::WebClientHintsType::kUAPlatform)],
-          SerializeHeaderString(ua->platform));
+          SerializeStringHeader(ua->platform));
     }
 
     if (ShouldSendClientHint(
@@ -313,7 +324,7 @@
       request.SetHttpHeaderField(
           blink::kClientHintsHeaderMapping[static_cast<size_t>(
               network::mojom::blink::WebClientHintsType::kUAPlatformVersion)],
-          SerializeHeaderString(ua->platform_version));
+          SerializeStringHeader(ua->platform_version));
     }
 
     if (ShouldSendClientHint(
@@ -323,7 +334,7 @@
       request.SetHttpHeaderField(
           blink::kClientHintsHeaderMapping[static_cast<size_t>(
               network::mojom::blink::WebClientHintsType::kUAModel)],
-          SerializeHeaderString(ua->model));
+          SerializeStringHeader(ua->model));
     }
 
     if (ShouldSendClientHint(
@@ -333,7 +344,7 @@
       request.SetHttpHeaderField(
           blink::kClientHintsHeaderMapping[static_cast<size_t>(
               network::mojom::blink::WebClientHintsType::kUAFullVersion)],
-          SerializeHeaderString(ua->full_version));
+          SerializeStringHeader(ua->full_version));
     }
 
     if (ShouldSendClientHint(
@@ -343,7 +354,20 @@
       request.SetHttpHeaderField(
           blink::kClientHintsHeaderMapping[static_cast<size_t>(
               network::mojom::blink::WebClientHintsType::kUABitness)],
-          SerializeHeaderString(ua->bitness));
+          SerializeStringHeader(ua->bitness));
+    }
+
+    if (ShouldSendClientHint(
+            ClientHintsMode::kStandard, policy, resource_origin, is_1p_origin,
+            network::mojom::blink::WebClientHintsType::kUAReduced,
+            hints_preferences)) {
+      // If the UA-Reduced client hint should be sent according to the hints
+      // preferences, it means the Origin Trial token for User-Agent Reduction
+      // has already been validated.
+      request.SetHttpHeaderField(
+          blink::kClientHintsHeaderMapping[static_cast<size_t>(
+              network::mojom::blink::WebClientHintsType::kUAReduced)],
+          SerializeBoolHeader(true));
     }
   }
 
diff --git a/third_party/blink/renderer/platform/loader/fetch/client_hints_preferences.cc b/third_party/blink/renderer/platform/loader/fetch/client_hints_preferences.cc
index 8ffd2aa..f1f127a 100644
--- a/third_party/blink/renderer/platform/loader/fetch/client_hints_preferences.cc
+++ b/third_party/blink/renderer/platform/loader/fetch/client_hints_preferences.cc
@@ -64,6 +64,7 @@
   // Note: .Ascii() would convert tab to ?, which is undesirable.
   absl::optional<std::vector<network::mojom::WebClientHintsType>> parsed_ch =
       network::ParseClientHintsHeader(header_value.Latin1());
+
   if (!parsed_ch.has_value())
     return;