Learn Measure Blog Live About
A variety of different footprints in the snow. A hint at who's been there.

Improving user privacy and developer experience with User-Agent Client Hints

User-Agent Client Hints are a new expansion to the Client Hints API, that enables developers to access information about a user's browser in a privacy-preserving and ergonomic way.

Updated

Client Hints enable developers to actively request information about the user's device or conditions, rather than needing to parse it out of the User-Agent (UA) string. Providing this alternative route is the first step to eventually reducing User-Agent string granularity.

Learn how to update your existing functionality that relies on parsing the User-Agent string to make use of User-Agent Client Hints instead.

Background

When web browsers make requests they include information about the browser and its environment so that servers can enable analytics and customize the response. This was defined all the way back in 1996 (RFC 1945 for HTTP/1.0), where you can find the original definition for the User-Agent string, which includes an example:

User-Agent: CERN-LineMode/2.15 libwww/2.17b3

This header was intended to specify, in order of significance, the product (e.g. browser or library) and a comment (e.g. version).

The state of the User-Agent string

Over the intervening decades, this string has accrued a variety of additional details about the client making the request (as well as cruft, due to backwards compatibility). We can see that when looking at Chrome's current User-Agent string:

Mozilla/5.0 (Linux; Android 10; Pixel 3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4076.0 Mobile Safari/537.36

The above string contains information about the user's operating system and version, the device model, the browser's brand and full version, enough clues to infer it's a mobile browser, and not to mention a number of references to other browsers for historical reasons.

The combination of these parameters with the sheer diversity of possible values means the User-Agent string could contain enough information to allow individual users to be uniquely identified. If you test your own browser at AmIUnique, you can see just how closely your User-Agent string identifies you. The lower your resulting "Similarity ratio" is, the more unique your requests are, the easier it is for servers to covertly track you.

The User-Agent string enables many legitimate use cases, and serves an important purpose for developers and site owners. However, it is also critical that users' privacy is protected against covert tracking methods, and sending UA information by default goes against that goal.

There's also a need to improve web compatibility when it comes to the User-Agent string. It is unstructured, so parsing it results in unnecessary complexity, which is often the cause for bugs and site compatibility issues that hurt users. These issues also disproportionately hurt users of less common browsers, as sites may have failed to test against their configuration.

Introducing the new User-Agent Client Hints

User-Agent Client Hints enable access to the same information but in a more privacy-preserving way, in turn enabling browsers to eventually reduce the User-Agent string's default of broadcasting everything. Client Hints enforce a model where the server must ask the browser for a set of data about the client (the hints) and the browser applies its own policies or user configuration to determine what data is returned. This means that rather than exposing all the User-Agent information by default, access is now managed in an explicit and auditable fashion. Developers also benefit from a simpler API - no more regular expressions!

The current set of Client Hints primarily describes the browser's display and connection capabilities. You can explore the details in Automating Resource Selection with Client Hints, but here's a quick refresher on the process.

The server asks for specific Client Hints via a header:

⬇️ Response from server

Accept-CH: Viewport-Width, Width

Or a meta tag:

<meta http-equiv="Accept-CH" content="Viewport-Width, Width" />

The browser can then choose to send the following headers back in subsequent requests:

⬆️ Subsequent request

Viewport-Width: 460
Width: 230

The server can choose to vary its responses, for example by serving images at an appropriate resolution.

There are ongoing discussions on enabling Client Hints on an initial request, but you should be considering responsive design or progressive enhancement before going down this route.

User-Agent Client Hints expand the range of properties that can be specified via the Accept-CH server response header. When the browser returns information it will use the Sec-CH-UA prefix. For all the details, start with the explainer and then dive into the full proposal.

Client Hints are only sent over secure connections, so make sure you have migrated your site to HTTPS.

The new set of hints is available from Chromium 84, so let's explore how it all works.

User-Agent Client Hints in Chromium 84

User-Agent Client Hints will only be enabled gradually on Chrome Stable as compatibility concerns are resolved. To force the functionality on for testing, ensure you:

  • are on Chrome 84 beta or equivalent
  • enable the chrome://flags/#enable-experimental-web-platform-features flag

By default, the browser returns the browser brand, significant / major version, and an indicator if the client is a mobile device:

⬆️ All requests

Sec-CH-UA: "Chromium";v="84", "Google Chrome";v="84"
Sec-CH-UA-Mobile: ?0

Caution: These properties are more complex than just a single value, so Structured Headers are used for representing lists and booleans.

User-Agent response and request headers

⬇️ Response
Accept-CH
⬆️ Request
header
⬆️ Request
Example value

Description
UA Sec-CH-UA "Chromium";v="84",
"Google Chrome";v="84"
List of browser brands and their significant version.
UA-Mobile Sec-CH-UA-Mobile ?1 Boolean indicating if the browser is on a mobile device (?1 for true) or not (?0 for false).
UA-Full-Version Sec-CH-UA-Full-Version "84.0.4143.2" The complete version for the browser.
UA-Platform Sec-CH-UA-Platform "Android" The platform for the device, usually the operating system (OS).
UA-Platform-Version Sec-CH-UA-Platform-Version "10" The version for the platform or OS.
UA-Arch Sec-CH-UA-Arch "ARM64" The underlying architecture for the device. While this may not be relevant to displaying the page, the site may want to offer a download which defaults to the right format.
UA-Model Sec-CH-UA-Model "Pixel 3" The device model.
Gotchas!

Privacy and compatibility considerations mean the value may be blank, not returned, or populated with a varying value. This is referred to as GREASE.

Example exchange

An example exchange would look like this:

⬆️ Initial request from browser
The browser is requesting the /downloads page from the site and sends its default basic User-Agent.

GET /downloads HTTP/1.1
Host: example.site

Sec-CH-UA: "Chromium";v="84", "Google Chrome";v="84"
Sec-CH-UA-Mobile: ?0

⬇️ Response from server
The server sends the page back and additionally asks for the full browser version and the platform.

HTTP/1.1 200 OK
Accept-CH: UA-Full-Version, UA-Platform

⬆️ Subsequent requests
The browser grants the server access to the additional information and sends the extra hints back in all subsequent responses.

GET /downloads/app1 HTTP/1.1
Host: example.site

Sec-CH-UA: "Chromium";v="84", "Google Chrome";v="84"
Sec-CH-UA-Mobile: ?0
Sec-CH-UA-Full-Version: "84.0.4143.2"
Sec-CH-UA-Platform: "Android"

JavaScript API

Alongside the headers, the User-Agent can also be accessed in JavaScript via navigator.userAgentData. The default Sec-CH-UA and Sec-CH-UA-Mobile header information can be accessed via the brands and mobile properties, respectively:

// Log the brand data
console.log(navigator.userAgentData.brands);

// output
[
{
brand: 'Chromium',
version: '84',
},
{
brand: 'Google Chrome',
version: '84',
},
];

// Log the mobile indicator
console.log(navigator.userAgentData.mobile);

// output
false;

The additional values are accessed via the getHighEntropyValues() call. The "high entropy" term is a reference to information entropy, in other words - the amount of information that these values reveal about the user's browser. As with requesting the additional headers, it's down to the browser what values, if any, are returned.

// Log the full user-agent data
navigator
.userAgentData.getHighEntropyValues(
["architecture", "model", "platform", "platformVersion",
"uaFullVersion"])
.then(ua => { console.log(ua) });

// output
{
"architecture": "x86",
"model": "",
"platform": "Linux",
"platformVersion": "",
"uaFullVersion": "84.0.4143.2"
}

Demo

You can try out both the headers and the JavaScript API on your own device at user-agent-client-hints.glitch.me.

Ensure you're using Chrome 84 Beta or equivalent with chrome://flags/#enable-experimental-web-platform-features enabled.

Hint life-time and resetting

Hints specified via the Accept-CH header will be sent for the duration of the browser session or until a different set of hints are specified.

That means if the server sends:

⬇️ Response

Accept-CH: UA-Full-Version

Then the browser will send the Sec-CH-UA-Full-Version header on all requests for that site until the browser is closed.

⬆️ Subsequent requests

Sec-CH-UA-Full-Version: "84.0.4143.2"

However, if another Accept-CH header is received then that will completely replace the current hints the browser is sending.

⬇️ Response

Accept-CH: UA-Platform

⬆️ Subsequent requests

Sec-CH-UA-Platform: "Android"

The previously asked-for Sec-CH-UA-Full-Version will not be sent.

It's best to think of the Accept-CH header as specifying the complete set of hints desired for that page, meaning the browser then sends the specified hints for all the subresources on that page. While hints will persist to the next navigation, the site should not rely or assume they will be delivered.

Success: Always ensure you can still deliver a meaningful experience without this information. This is to enhance the user experience, not define it. That's why they're called "hints" and not "answers" or "requirements"!

You can also use this to effectively clear all hints being sent by the browser by sending a blank Accept-CH in the response. Consider adding this anywhere that the user is resetting preferences or signing out of your site.

This pattern also matches how hints work via the <meta http-equiv="Accept-CH" …> tag. The requested hints will only be sent on requests initiated by the page and not on any subsequent navigation.

Hint scope and cross-origin requests

By default, Client Hints will only be sent on same-origin requests. That means if you ask for specific hints on https://example.com, but the resources you want to optimize are on https://downloads.example.com they will not receive any hints.

To allow hints on cross-origin requests each hint and origin must be specified by a Feature-Policy header. This specifies each hint (prefixed with ch-) and the allowlist of origins that should receive the hint. For example:

⬇️ Response from example.com

Accept-CH: UA-Platform, DPR
Feature-Policy: ch-UA-Platform downloads.example.com;
ch-DPR cdn.provider img.example.com

⬆️ Request to downloads.example.com

Sec-CH-UA-Platform: "Android"

⬆️ Requests to cdn.provider or img.example.com

DPR: 2

Where to use User-Agent Client Hints?

The quick answer is that you should refactor any instances where you are parsing either the User-Agent header or making use of any of the JavaScript calls that access the same information (i.e. navigator.userAgent, navigator.appVersion, or navigator.platform) to make use of User-Agent Client Hints instead.

Taking this a step further, you should re-examine your use of User-Agent information, and replace it with other methods whenever possible. Often, you can accomplish the same goal by making use of progressive enhancement, feature detection, or responsive design. The base problem with relying on the User-Agent data is that you are always maintaining a mapping between the property you're inspecting and the behavior it enables. It's a maintenance overhead to ensure that your detection is comprehensive and remains up-to-date.

With these caveats in mind, the User-Agent Client Hints repo lists some valid use cases for sites.

What happens to the User-Agent string?

The plan is to minimize the ability for covert tracking on the web by reducing the amount of identifying information exposed by the existing User-Agent string while not causing undue disruption on existing sites. Introducing User-Agent Client Hints now gives you a chance to understand and experiment with the new capability, before any changes are made to User-Agent strings.

Eventually, the information in the User-Agent string will be reduced so it maintains the legacy format while only providing the same high-level browser and significant version information as per the default hints. In Chromium, this change has been deferred until at least 2021 to provide additional time for the ecosystem to evaluate the new User Agent Client Hints capabilities.

You can test a version of this by enabling the chrome://flags/#freeze-user-agent flag from Chrome 84. This will return a string with the historical entries for compatibility reasons, but with sanitized specifics. For example, something like:

Mozilla/5.0 (Linux; Android 9; Unspecified Device) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.0.0 Mobile Safari/537.36

Photo by Sergey Zolkin on Unsplash

Last updated: Improve article