Accept-Language header: 1 value, block page or login

What the Accept-Language header is, how Chrome builds it from a language list, and the malformed value that got a DataDome captcha instead of a login page.

Davide Valli · Founder, Scalebrowser

Published Nov 1, 2026 · 5 min read

Short answer

The Accept-Language header can trigger bot detection on its own, because a server reads it before any script runs and its form is strict: RFC 9110 allows one quality weight per language. Against PayPal's DataDome on 12 August 2026, a malformed value that listed de twice, once with two weights, got the captcha page with 3 cookies, and the value Chrome derives for de-AT got the login page with 19, in 4 of 4 runs. An automated browser should send what Chrome derives from the same list navigator.languages returns, the list real Chrome sends in its exit country.
1, value, block or login, beside a gate with a dark doorway

Nobody tunes the Accept-Language header on purpose. It sits in every request, it says which languages the visitor reads, and most automation code either leaves it alone or sets it once to match a proxy's country. We set it wrong once, by handing the browser a finished header where it expected a plain list, and a bot defence answered with its captcha page every time. Real Chrome on the same connection got the login page.

What is the Accept-Language header?

The Accept-Language header is the request header in which a browser lists the natural languages it prefers, each with an optional weight. RFC 9110 defines it as #( language-range [ weight ] ), which allows at most one weight per language, and sets the weight between 0 and 1, with 1 as the default when none is given. Its own example, da, en-gb;q=0.8, en;q=0.7, "would mean: 'I prefer Danish, but will accept British English and other types of English'".

Chrome does not store the header; it builds it from the language list in its settings. Chromium's source adds a language-only tag after each regional one when the next entry is from another language family, then appends weights that fall by 0.1, leaving the first entry without one.

Language list in ChromeAccept-Language Chrome sends
en-USen-US,en;q=0.9
de-AT,dede-AT,de;q=0.9
en-GB,fren-GB,en;q=0.9,fr;q=0.8
en-US,zh-CNen-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7

We computed the rows from that source, and the last one is also the example MDN gives for Chrome. The rule matters because the builder trusts its input. In a release build it never checks whether a tag already carries a weight, so a list that contains one comes out with two.

Chrome derives the header from the language list with one quality weight per language. For de-AT,de it sends de-AT,de;q=0.9, the value that got PayPal's login page instead of DataDome's captcha page.

Can the Accept-Language header trigger bot detection?

Yes. The Accept-Language header decided a bot check in our runs, against PayPal's login page, which DataDome protects. On 12 August 2026 we loaded that public page four times, each time from a fresh profile on the same machine and the same connection, in the order A, B, B, A, with no account and no form involved. Only the language list the browser started with changed, and with it the header.

Accept-Language sentAllowed by RFC 9110What loadedCookies after load
de-AT,de;q=0.9,de;q=0.9;q=0.8no: de twice, two weights on one tagDataDome's captcha page3
de-AT,de;q=0.9yesPayPal's login page19
Source: Scalebrowser engine, PayPal login page protected by DataDome, 4 runs in A-B-B-A order, fresh profile per run, 12 August 2026

Each variant gave the same result in both of its runs. The first value came from starting the browser with the list de-AT,de;q=0.9, a finished header, which Chromium's builder expanded into a value no Chrome produces from its own settings. The second is what Chrome derives from de-AT,de, and real Chrome on the same connection got the login page too. We fixed the input on 12 August 2026 and have checked every locale we ship against the derived header since.

DataDome's server reads the header directly: its Protection API has a field AcceptLanguage that carries the raw value, and the block page gave the IP address as its reason, which it was not. That makes the header an example of a header that decides the verdict before a single line of JavaScript runs. No fingerprint test page would have shown it, because the header never reaches the page's script. The Referer header and Sec-Fetch-Site, which show how a tab was opened, are covered in Referer header and Sec-Fetch-Site.

What Accept-Language should an automated browser send?

An automated browser should send the value Chrome derives itself from its own language list, the same list navigator.languages returns. Four properties of that value are easy to break by hand.

  • The first entry has no weight. Chrome writes weights from the second entry on, starting at 0.9.
  • Each tag has at most one weight. A second weight on the same tag is outside RFC 9110's grammar, and Chrome never produces it from its settings.
  • A language-only tag follows a regional one. en-US alone becomes en-US,en;q=0.9; a header of just en-US is not what Chrome sends.
  • The whole list is sent. Chrome's proposal to reduce the header to one language "has not been approved to ship in Chrome" as of September 2026, and according to MDN a single language is what Safari always sends and Chrome only in incognito mode.

The free path offers two ways to set the language, and only one keeps the values together. Playwright's locale option "will affect navigator.language value, Accept-Language request header value as well as number and date formatting rules", according to its API documentation. extraHTTPHeaders changes the header alone and leaves everything a script reads where it was.

Does Accept-Language have to match navigator.languages?

Yes. MDN's reference for navigator.languages states that the header "generally lists the same locales as the navigator.languages property, with decreasing q values", and that Chrome and Safari add the language-only fallback tags on top. A server sees the header, a script sees the list, and a checker that has both compares them. A header set by hand to fr-FR next to a script that answers ["en-US"] describes two different users in one visit.

How does the header relate to the exit country?

The header has to fit the country the request comes from, together with the timezone and the date formatting, because a checker reads the exit address and the language side by side. Fitting does not mean carrying the country's tag: Chrome sends the default list of its interface language, which follows the Windows display language, so a user in Vienna sends de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7 exactly like one in Berlin. Scalebrowser treats the four values as one unit: it writes the timezone, navigator.language, the language list and the Accept-Language header together from the exit country, for the 62 countries its locale table covers, and leaves a profile untouched when the exit is outside that table rather than rewriting half of it. The coherence page explains the three modes that decide what happens when the country does not match.

Run it on your own machine

Seven days to try it with your own agents on your own sites. Starting the trial needs a card.

Written by

Davide Valli

Founder, Scalebrowser

Builds Scalebrowser, the browser layer for AI agents that runs on your own machine. Measures every change a web page could observe against a real browser before it ships, and writes up the ones that turned out wrong.

All articles by Davide Valli