From 852e9aa41ab01cf7247b2768eccf8cec94d9c56f Mon Sep 17 00:00:00 2001 From: BoSuY0 Date: Tue, 12 May 2026 09:59:50 +0300 Subject: [PATCH] Keep injected language headers native Filter accept-language out of fingerprint-suite extra headers while preserving the generated language through Playwright's native locale option. This fixes the headless signal without leaving navigator locale injection inconsistent. Constraint: keep the change narrow to fingerprint-injector header and locale behavior. Rejected: adding a new ignoreHeaders option, because the maintainer explicitly preferred avoiding more granular options. Confidence: high; unit tests and live areyouheadless checks pass for Playwright and Puppeteer. Scope-risk: low-to-medium; affects default injected request header behavior for accept-language. Directive: claim apify/fingerprint-suite#178 bounty only after PR is opened and reviewed by maintainers/Algora. Tested: pnpm build; pnpm exec vitest run test/fingerprint-injector/fingerprint-injector.test.ts -t 'accept-language|native locale'; pnpm exec prettier packages/fingerprint-injector/src/fingerprint-injector.ts test/fingerprint-injector/fingerprint-injector.test.ts --check; pnpm lint; live Playwright/Puppeteer areyouheadless checks. Not-tested: full browser suite with channel: chrome, because system Google Chrome is not installed and installer requires sudo. --- .../src/fingerprint-injector.ts | 5 +- .../fingerprint-injector.test.ts | 59 ++++++++++++++++--- 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/packages/fingerprint-injector/src/fingerprint-injector.ts b/packages/fingerprint-injector/src/fingerprint-injector.ts index 41a0353d..8ed4f82c 100644 --- a/packages/fingerprint-injector/src/fingerprint-injector.ts +++ b/packages/fingerprint-injector/src/fingerprint-injector.ts @@ -62,6 +62,7 @@ export class FingerprintInjector { ): Record { const requestHeaders = [ 'accept-encoding', + 'accept-language', 'accept', 'cache-control', 'pragma', @@ -341,10 +342,11 @@ export async function newInjectedContext( options?.fingerprint ?? generator.getFingerprint(options?.fingerprintOptions); - const { fingerprint, headers } = fingerprintWithHeaders; + const { fingerprint } = fingerprintWithHeaders; const context = await browser.newContext({ userAgent: fingerprint.navigator.userAgent, colorScheme: 'dark', + locale: fingerprint.navigator.language, ...options?.newContextOptions, viewport: { width: fingerprint.screen.width, @@ -352,7 +354,6 @@ export async function newInjectedContext( ...options?.newContextOptions?.viewport, }, extraHTTPHeaders: { - 'accept-language': headers['accept-language'], ...options?.newContextOptions?.extraHTTPHeaders, }, }); diff --git a/test/fingerprint-injector/fingerprint-injector.test.ts b/test/fingerprint-injector/fingerprint-injector.test.ts index 59b0c4b7..f1bec796 100644 --- a/test/fingerprint-injector/fingerprint-injector.test.ts +++ b/test/fingerprint-injector/fingerprint-injector.test.ts @@ -17,6 +17,7 @@ import { describe, expect, test, + vi, beforeEach, afterAll, beforeAll, @@ -91,6 +92,57 @@ describe('FingerprintInjector', () => { // eslint-disable-next-line dot-notation expect(fpInjector['utilsJs']).toBeTruthy(); }); + + test('does not inject accept-language as an extra HTTP header', () => { + const filteredHeaders = fpInjector['onlyInjectableHeaders']({ + 'accept-language': 'cs-CZ,cs;q=0.9', + 'user-agent': 'Mozilla/5.0 Test', + }); + + expect(filteredHeaders).not.toHaveProperty('accept-language'); + expect(filteredHeaders['user-agent']).toBe('Mozilla/5.0 Test'); + }); + + test('creates Playwright contexts with native locale instead of injected accept-language', async () => { + const fingerprintWithHeaders = new FingerprintGenerator({ + devices: ['desktop'], + operatingSystems: ['linux'], + browsers: [{ name: 'chrome', minVersion: 90 }], + locales: ['cs-CZ'], + }).getFingerprint(); + const setExtraHTTPHeaders = vi.fn(); + const context = { + browser: () => ({ + browserType: () => ({ + name: () => 'chromium', + }), + }), + setExtraHTTPHeaders, + on: vi.fn(), + addInitScript: vi.fn(), + } as unknown as import('playwright').BrowserContext; + const newContext = vi.fn().mockResolvedValue(context); + const browser = { + newContext, + } as unknown as PWBrowser; + + await newInjectedContext(browser, { + fingerprint: fingerprintWithHeaders, + }); + + const newContextOptions = newContext.mock.calls[0][0]; + + expect(newContextOptions.locale).toBe( + fingerprintWithHeaders.fingerprint.navigator.language, + ); + expect(newContextOptions.extraHTTPHeaders).not.toHaveProperty( + 'accept-language', + ); + expect(setExtraHTTPHeaders.mock.calls[0][0]).not.toHaveProperty( + 'accept-language', + ); + }); + // @ts-expect-error test only describe.each(cases)('%s', (frameworkName, testCases) => { // @ts-expect-error test only @@ -368,13 +420,6 @@ describe('FingerprintInjector', () => { }); test('should override locales', async () => { - response = await page.goto(`https://crawlee.dev`); - const requestHeaders = response.request().headers(); - - expect( - requestHeaders['accept-language']?.includes('cs'), - ).toBe(true); - const { navigator: { language }, } = fingerprint;