# Accept-Language header vs proxy country: a gap we hid

> A profile matched to a Russian exit kept sending es-MX in Accept-Language. Leaving the language alone was right. Saying nothing about it was the bug.

Source: https://mosaicbrowser.com/blog/accept-language-and-proxy-country

## A Russian exit and a profile speaking Mexican Spanish

A profile matched to a Russian exit kept sending es-MX in Accept-Language. Leaving the language alone was right. Saying nothing about it was the bug.

![Exit in Russia, Accept-Language: es-MX, beside a screenshot: a profile fingerprint in Mosaic: GPU, screen, fonts, timezone and language matched to the exit](https://mosaicbrowser.com/covers/accept-language-and-proxy-country.webp)

The `Accept-Language` header goes out with every request a browser makes, and a site that knows your proxy's country can compare the two. One of our profiles left through a Russian exit and kept sending `es-MX`, Spanish as spoken in Mexico. Our exit matcher had moved the timezone and the geolocation to Russia, left the language where it was, and then reported the match as complete. Leaving the language alone was correct. Reporting a clean match was the bug.

## How a Mexican Spanish profile ended up in Russia

We were measuring detectors with a provider, the kind of proxy account where Mosaic mints a fresh session for each profile from one gateway line. One of those sessions was `region-random`: the seller picks the country. It picked Russia, and the exit geolocated to Yoshkar-Ola.

Mosaic's exit matcher, the same code that runs before each launch, looked the exit up and brought the profile into line. Its response for this one read:

```
changed: true
fields:  ['timezone', 'geolocation']
skipped: []

```

Two fields moved and nothing was skipped, which reads as a profile that now fully agrees with its exit. Meanwhile `es-MX` went on riding every request out of Yoshkar-Ola. The response also carried a `reason` saying the profile already agreed with the proxy exit, in the same breath as the field that contradicted it.

## Why the language was left alone

Mosaic picks a language for an exit from its locale table. That table is what the generator draws new profiles from, country by country, so a profile matched to an exit afterwards looks like one that was created there. The table covers about fifty countries, and Russia is not one of them.

So the lookup returned nothing, and the matcher kept the old language rather than guessing. We still think that is right. A language invented on the spot, from a matcher, for one country, would be a value no profile created through the normal path could ever have.

Adding a Russian row is the real fix for the mismatch, and it is not a small one. It changes what every new profile can be drawn as, so it is a decision about the whole population of profiles. We did not want it made quietly from inside the code that matches one profile to one exit.

Mosaic is a desktop app

Email yourself the link and open it on your computer.

[How it works: Machines that exist](https://mosaicbrowser.com/features/real-fingerprints)

## What we changed

The matcher now says when there is no language to move to. On that profile the `skipped` list carries:

```
language (es-MX does not match RU; the locale table has no row
for RU, so there is no language to move it to)

```

And the reason no longer claims agreement while listing a field that disagrees.

The check is careful in the other direction too. A profile already speaking `ru-RU` on a Russian exit agrees with it, row or no row, so it is not flagged. A warning about a match that is fine is noise, and noise teaches people to stop reading warnings.

## Why the Accept-Language header is checked against the proxy country

Chrome builds the header from the profile's language list: `es-MX,es;q=0.9` says Mexican Spanish first, any Spanish second. Page script reads the same list from `navigator.languages`. Neither needs a fingerprinting script. The header arrives with the very first request, before any of your code has run.

That is why Mosaic treats the language differently from the timezone. A logged-in session reads the timezone fresh each time, so moving it to match a new exit costs nothing. The language is different, because the site has been receiving it all along. A profile that has spoken `de-AT` to a site for a month and suddenly speaks `en-US` has told that site something about itself.

So the rule at launch is:

- timezone and geolocation follow the exit, on every launch;
- the language follows the exit only on a profile that has never run;
- on a profile with history, a language that disagrees with the exit is reported, and stays.

The operator can then decide whether the mismatch matters for that account. Many real people browse in a language that is not their country's. A scanner still counts it. Whoer, in our runs, deducts for a language that does not match the exit's country.

## What a region-random session costs you

The bigger lesson was about the session, not the header. A seller that picks the country for you can hand you a country your profile was never drawn for, and some fields cannot follow it. We now pin the country on sessions we measure with, and let the address move inside it. The reasoning, with the numbers from the runs that taught us, is in [pin the country, not the address](https://mosaicbrowser.com/blog/pin-the-country-not-the-address).

A profile created against a checked proxy is drawn for that exit from the start: timezone, language and geolocation together, which is what [real fingerprints](https://mosaicbrowser.com/features/real-fingerprints) means in Mosaic. A profile with no proxy is drawn for the computer's own address, and how that went wrong once is in [a profile in Austria claimed New York](https://mosaicbrowser.com/blog/austria-claimed-new-york). For profiles on a seller's pool, set the country when you mint the session in [proxy providers](https://mosaicbrowser.com/features/proxy-providers), and when you attach a proxy to profiles that already exist, `POST /profiles/:id/match-exit` does the same matching without opening a browser. Its `skipped` list names anything the match could not move.

## Questions

### Does the Accept-Language header have to match my proxy country?

No rule says it must, and plenty of real people browse in a language that is not their country's. But a consistency scanner compares the header's language and region with the country of your exit, and Whoer deducts for a mismatch.

### Is the Accept-Language header sent on every request?

Yes. The browser attaches it to every page, image, script and API call, and navigator.languages gives page script the same list. A site that has seen one value for a month sees a change the moment it happens.

### Why didn't Mosaic switch the language to Russian?

Its locale table has no Russian row, so there was no Russian language entry to draw from, and we chose not to invent one inside the code that matches a profile to its exit. The match now reports the gap by name.

### Does Mosaic change the language of a profile that has already been used?

No. Timezone and geolocation follow the exit on every launch, but the language moves only for a profile that has never run. On a used profile the mismatch is reported and the language stays.

## Related

[Pin the country, not the address](https://mosaicbrowser.com/blog/pin-the-country-not-the-address): A residential pool moves your exit whatever you do. The checks that matter compare countries, so pin the country and let the address move inside it.

[A profile in Austria claimed New York](https://mosaicbrowser.com/blog/austria-claimed-new-york): Browser location does not match IP: a no-proxy profile made in Austria claimed New York. One failed lookup was never retried, and a table filled the gap.

[Machines that exist: fingerprints drawn from real hardware](https://mosaicbrowser.com/features/real-fingerprints): Mosaic draws each profile's GPU, screen, CPU, memory and fonts from one real device, so the parts of a fingerprint agree with each other and with the host.

[Provider accounts: one gateway line, one exit per profile](https://mosaicbrowser.com/features/proxy-providers): Paste your proxy seller's gateway line once and Mosaic mints a separate session for every profile, so each one browses from its own exit address.

[IPHey's score moved with the address, not the browser](https://mosaicbrowser.com/blog/iphey-score-moves-with-address): IPHey score changes even on an untouched Chrome: 100, 100, 70 and 0 in one afternoon. The score follows the exit and the moment; the browser card does not.

[We broke the timezone on purpose and Pixelscan liked it better](https://mosaicbrowser.com/blog/pixelscan-timezone-sweep): We changed only the timezone on one profile behind a Slovak exit. Pixelscan flagged masking on the Vienna row, the correct one, and the controls disagreed.

[The RTT client hint, read off the wire](https://mosaicbrowser.com/blog/rtt-on-the-wire): ECT, RTT and Downlink headers behind a proxy, measured: a claimed round trip moved JavaScript to 300 and left the RTT header at 50. So we claim neither.

[Why hCaptcha gives some browsers a puzzle every time](https://mosaicbrowser.com/blog/hcaptcha-image-challenge-every-time): hCaptcha's challenge is its score. A puzzle every time usually means blocked cookies, a crowded VPN exit or a browser whose values disagree with each other.

## Mosaic, on your own computer

The browser this blog is about: its own Chromium, fingerprints from real machines and a proxy exit per profile. Free holds 3 profiles. Pro is $49 a month for any number.

Mosaic is a desktop app

Email yourself the link and open it on your computer.
