Skip to content

Commit 557c15c

Browse files
authored
fix: allow removing impersonated headers by passing empty string (#382)
Users can now remove impersonated headers (like `Sec-Fetch-User`) from requests by passing an empty string as the header value. When an empty string is provided, the header is filtered out before the request is sent. This enables users, e.g., to manually control which `Sec-Fetch-*` headers should be included in their requests, addressing use cases where the default impersonated headers don't match the actual request context. Closes #228
1 parent 82dd244 commit 557c15c

8 files changed

Lines changed: 77 additions & 28 deletions

File tree

impit-node/index.d.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,8 @@ cookieJar?: { setCookie: (cookie: string, url: string, cb?: any) => Promise<void
313313
* Header matching is **case-insensitive** — for example, setting `user-agent` here will override
314314
* the impersonation `User-Agent` header.
315315
*
316+
* To remove an impersonated header, pass an empty string as the value.
317+
*
316318
* @default `undefined` (no additional headers)
317319
*/
318320
headers?: Headers | Record<string, string> | [string, string][]
@@ -347,10 +349,12 @@ export interface RequestInit {
347349
*
348350
* Can be an object, a Map, or an array of tuples or an instance of the {@link https://developer.mozilla.org/en-US/docs/Web/API/Headers | Headers} class.
349351
*
350-
* These headers have the highest priority — they override both client-level headers
351-
* (set via {@link ImpitOptions.headers}) and browser impersonation headers (set via {@link ImpitOptions.browser}).
352+
* Note that headers set here will override any default headers set in {@link ImpitOptions.headers}.
353+
*
352354
* Header matching is **case-insensitive** — for example, setting `user-agent` here will override
353355
* the impersonation `User-Agent` header.
356+
*
357+
* To remove an impersonated header, pass an empty string as the value.
354358
*/
355359
headers?: Headers | Record<string, string> | [string, string][]
356360
/** Request body. Can be a string, Buffer, ArrayBuffer, TypedArray, DataView, Blob, File, URLSearchParams, FormData or ReadableStream. */

impit-node/src/impit_builder.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,13 @@ pub struct ImpitOptions<'a> {
9696
///
9797
/// Can be an object, a Map, or an array of tuples or an instance of the {@link https://developer.mozilla.org/en-US/docs/Web/API/Headers | Headers} class.
9898
///
99+
/// These headers override any browser impersonation headers (set via the {@link ImpitOptions.browser} option)
100+
/// and are in turn overridden by request-specific headers (set via {@link RequestInit.headers}).
101+
/// Header matching is **case-insensitive** — for example, setting `user-agent` here will override
102+
/// the impersonation `User-Agent` header.
103+
///
104+
/// To remove an impersonated header, pass an empty string as the value.
105+
///
99106
/// @default `undefined` (no additional headers)
100107
#[napi(ts_type = "Headers | Record<string, string> | [string, string][]")]
101108
pub headers: Option<Vec<(String, String)>>,

impit-node/src/request.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ pub struct RequestInit {
3535
/// Can be an object, a Map, or an array of tuples or an instance of the {@link https://developer.mozilla.org/en-US/docs/Web/API/Headers | Headers} class.
3636
///
3737
/// Note that headers set here will override any default headers set in {@link ImpitOptions.headers}.
38+
///
39+
/// Header matching is **case-insensitive** — for example, setting `user-agent` here will override
40+
/// the impersonation `User-Agent` header.
41+
///
42+
/// To remove an impersonated header, pass an empty string as the value.
3843
#[napi(ts_type = "Headers | Record<string, string> | [string, string][]")]
3944
pub headers: Option<Vec<(String, String)>>,
4045
#[napi(

impit-node/test/basics.test.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,15 +188,15 @@ describe.each([
188188

189189
test('impit accepts custom cookie jars', async (t) => {
190190
const cookieJar = new CookieJar();
191-
cookieJar.setCookieSync('preset-cookie=123; Path=/', getHttpBinUrl('/cookies/'));
191+
cookieJar.setCookieSync('preset-cookie=123; Path=/', getHttpBinUrl('/cookies'));
192192

193193
const impit = new Impit({
194194
cookieJar,
195195
browser,
196196
})
197197

198198
const response1 = await impit.fetch(
199-
getHttpBinUrl('/cookies/'),
199+
getHttpBinUrl('/cookies'),
200200
).then(x => x.json());
201201

202202
t.expect(response1.cookies).toEqual({
@@ -208,7 +208,7 @@ describe.each([
208208
);
209209

210210
const response2 = await impit.fetch(
211-
getHttpBinUrl('/cookies/'),
211+
getHttpBinUrl('/cookies'),
212212
).then(x => x.json());
213213

214214
t.expect(response2.cookies).toEqual({
@@ -278,6 +278,20 @@ describe.each([
278278
t.expect(json.headers?.['User-Agent']).toBe('this is impit!');
279279
})
280280

281+
test('removing impersonated headers with empty string works', async (t) => {
282+
const response = await impit.fetch(
283+
getHttpBinUrl('/headers'),
284+
{
285+
headers: {
286+
'Sec-Fetch-User': '',
287+
}
288+
}
289+
);
290+
const json = await response.json();
291+
292+
t.expect(json.headers?.['Sec-Fetch-User']).toBeUndefined();
293+
});
294+
281295
test('client-scoped headers work', async (t) => {
282296
const headers = new Headers();
283297
headers.set('User-Agent', 'client-scoped user agent');

impit-python/python/impit/impit.pyi

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,7 @@ class Client:
484484
cookie_jar: Cookie jar to store cookies in.
485485
cookies: httpx-compatible cookies object.
486486
headers: Default HTTP headers to include in requests. These override browser impersonation
487-
headers and are overridden by per-request headers. Matching is case-insensitive.
487+
headers and are overridden by per-request headers. Matching is case-insensitive. To remove an impersonated header, pass an empty string as the value.
488488
local_address: Local address to bind the client to. Useful for testing purposes or when you want to bind the client to a specific network interface.
489489
Can be an IP address in the format "xxx.xxx.xxx.xxx" (for IPv4) or "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff" (for IPv6).
490490
"""
@@ -504,7 +504,7 @@ class Client:
504504
url: URL to request
505505
content: Raw content to send
506506
data: Form data to send in request body
507-
headers: HTTP headers for this request. Override both client-level and impersonation headers (case-insensitive)
507+
headers: HTTP headers for this request. Override both client-level and impersonation headers (case-insensitive). To remove an impersonated header, pass an empty string as the value
508508
timeout: Request timeout in seconds (overrides default timeout)
509509
force_http3: Force HTTP/3 protocol
510510
"""
@@ -524,7 +524,7 @@ class Client:
524524
url: URL to request
525525
content: Raw content to send
526526
data: Form data to send in request body
527-
headers: HTTP headers for this request. Override both client-level and impersonation headers (case-insensitive)
527+
headers: HTTP headers for this request. Override both client-level and impersonation headers (case-insensitive). To remove an impersonated header, pass an empty string as the value
528528
timeout: Request timeout in seconds (overrides default timeout)
529529
force_http3: Force HTTP/3 protocol
530530
@@ -545,7 +545,7 @@ class Client:
545545
url: URL to request
546546
content: Raw content to send
547547
data: Form data to send in request body
548-
headers: HTTP headers for this request. Override both client-level and impersonation headers (case-insensitive)
548+
headers: HTTP headers for this request. Override both client-level and impersonation headers (case-insensitive). To remove an impersonated header, pass an empty string as the value
549549
timeout: Request timeout in seconds (overrides default timeout)
550550
force_http3: Force HTTP/3 protocol
551551
"""
@@ -565,7 +565,7 @@ class Client:
565565
url: URL to request
566566
content: Raw content to send
567567
data: Form data to send in request body
568-
headers: HTTP headers for this request. Override both client-level and impersonation headers (case-insensitive)
568+
headers: HTTP headers for this request. Override both client-level and impersonation headers (case-insensitive). To remove an impersonated header, pass an empty string as the value
569569
timeout: Request timeout in seconds (overrides default timeout)
570570
force_http3: Force HTTP/3 protocol
571571
"""
@@ -585,7 +585,7 @@ class Client:
585585
url: URL to request
586586
content: Raw content to send
587587
data: Form data to send in request body
588-
headers: HTTP headers for this request. Override both client-level and impersonation headers (case-insensitive)
588+
headers: HTTP headers for this request. Override both client-level and impersonation headers (case-insensitive). To remove an impersonated header, pass an empty string as the value
589589
timeout: Request timeout in seconds (overrides default timeout)
590590
force_http3: Force HTTP/3 protocol
591591
"""
@@ -605,7 +605,7 @@ class Client:
605605
url: URL to request
606606
content: Raw content to send
607607
data: Form data to send in request body
608-
headers: HTTP headers for this request. Override both client-level and impersonation headers (case-insensitive)
608+
headers: HTTP headers for this request. Override both client-level and impersonation headers (case-insensitive). To remove an impersonated header, pass an empty string as the value
609609
timeout: Request timeout in seconds (overrides default timeout)
610610
force_http3: Force HTTP/3 protocol
611611
"""
@@ -625,7 +625,7 @@ class Client:
625625
url: URL to request
626626
content: Raw content to send
627627
data: Form data to send in request body
628-
headers: HTTP headers for this request. Override both client-level and impersonation headers (case-insensitive)
628+
headers: HTTP headers for this request. Override both client-level and impersonation headers (case-insensitive). To remove an impersonated header, pass an empty string as the value
629629
timeout: Request timeout in seconds (overrides default timeout)
630630
force_http3: Force HTTP/3 protocol
631631
"""
@@ -645,7 +645,7 @@ class Client:
645645
url: URL to request
646646
content: Raw content to send
647647
data: Form data to send in request body
648-
headers: HTTP headers for this request. Override both client-level and impersonation headers (case-insensitive)
648+
headers: HTTP headers for this request. Override both client-level and impersonation headers (case-insensitive). To remove an impersonated header, pass an empty string as the value
649649
timeout: Request timeout in seconds (overrides default timeout)
650650
force_http3: Force HTTP/3 protocol
651651
"""
@@ -668,7 +668,7 @@ class Client:
668668
url: URL to request
669669
content: Raw content to send
670670
data: Form data to send in request body
671-
headers: HTTP headers for this request. Override both client-level and impersonation headers (case-insensitive)
671+
headers: HTTP headers for this request. Override both client-level and impersonation headers (case-insensitive). To remove an impersonated header, pass an empty string as the value
672672
timeout: Request timeout in seconds (overrides default timeout)
673673
force_http3: Force HTTP/3 protocol
674674
stream: Whether to return a streaming response (default: False)
@@ -702,7 +702,7 @@ class Client:
702702
url: URL to request
703703
content: Raw content to send
704704
data: Form data to send in request body
705-
headers: HTTP headers for this request. Override both client-level and impersonation headers (case-insensitive)
705+
headers: HTTP headers for this request. Override both client-level and impersonation headers (case-insensitive). To remove an impersonated header, pass an empty string as the value
706706
timeout: Request timeout in seconds (overrides default timeout)
707707
force_http3: Force HTTP/3 protocol
708708
"""
@@ -825,7 +825,7 @@ class AsyncClient:
825825
cookie_jar: Cookie jar to store cookies in.
826826
cookies: httpx-compatible cookies object.
827827
headers: Default HTTP headers to include in requests. These override browser impersonation
828-
headers and are overridden by per-request headers. Matching is case-insensitive.
828+
headers and are overridden by per-request headers. Matching is case-insensitive. To remove an impersonated header, pass an empty string as the value.
829829
local_address: Local address to bind the client to. Useful for testing purposes or when you want to bind the client to a specific network interface.
830830
Can be an IP address in the format "xxx.xxx.xxx.xxx" (for IPv4) or "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff" (for IPv6).
831831
"""
@@ -845,7 +845,7 @@ class AsyncClient:
845845
url: URL to request
846846
content: Raw content to send
847847
data: Form data to send in request body
848-
headers: HTTP headers for this request. Override both client-level and impersonation headers (case-insensitive)
848+
headers: HTTP headers for this request. Override both client-level and impersonation headers (case-insensitive). To remove an impersonated header, pass an empty string as the value
849849
timeout: Request timeout in seconds (overrides default timeout)
850850
force_http3: Force HTTP/3 protocol
851851
"""
@@ -865,7 +865,7 @@ class AsyncClient:
865865
url: URL to request
866866
content: Raw content to send
867867
data: Form data to send in request body
868-
headers: HTTP headers for this request. Override both client-level and impersonation headers (case-insensitive)
868+
headers: HTTP headers for this request. Override both client-level and impersonation headers (case-insensitive). To remove an impersonated header, pass an empty string as the value
869869
timeout: Request timeout in seconds (overrides default timeout)
870870
force_http3: Force HTTP/3 protocol
871871
@@ -886,7 +886,7 @@ class AsyncClient:
886886
url: URL to request
887887
content: Raw content to send
888888
data: Form data to send in request body
889-
headers: HTTP headers for this request. Override both client-level and impersonation headers (case-insensitive)
889+
headers: HTTP headers for this request. Override both client-level and impersonation headers (case-insensitive). To remove an impersonated header, pass an empty string as the value
890890
timeout: Request timeout in seconds (overrides default timeout)
891891
force_http3: Force HTTP/3 protocol
892892
"""
@@ -906,7 +906,7 @@ class AsyncClient:
906906
url: URL to request
907907
content: Raw content to send
908908
data: Form data to send in request body
909-
headers: HTTP headers for this request. Override both client-level and impersonation headers (case-insensitive)
909+
headers: HTTP headers for this request. Override both client-level and impersonation headers (case-insensitive). To remove an impersonated header, pass an empty string as the value
910910
timeout: Request timeout in seconds (overrides default timeout)
911911
force_http3: Force HTTP/3 protocol
912912
"""
@@ -926,7 +926,7 @@ class AsyncClient:
926926
url: URL to request
927927
content: Raw content to send
928928
data: Form data to send in request body
929-
headers: HTTP headers for this request. Override both client-level and impersonation headers (case-insensitive)
929+
headers: HTTP headers for this request. Override both client-level and impersonation headers (case-insensitive). To remove an impersonated header, pass an empty string as the value
930930
timeout: Request timeout in seconds (overrides default timeout)
931931
force_http3: Force HTTP/3 protocol
932932
"""
@@ -946,7 +946,7 @@ class AsyncClient:
946946
url: URL to request
947947
content: Raw content to send
948948
data: Form data to send in request body
949-
headers: HTTP headers for this request. Override both client-level and impersonation headers (case-insensitive)
949+
headers: HTTP headers for this request. Override both client-level and impersonation headers (case-insensitive). To remove an impersonated header, pass an empty string as the value
950950
timeout: Request timeout in seconds (overrides default timeout)
951951
force_http3: Force HTTP/3 protocol
952952
"""
@@ -966,7 +966,7 @@ class AsyncClient:
966966
url: URL to request
967967
content: Raw content to send
968968
data: Form data to send in request body
969-
headers: HTTP headers for this request. Override both client-level and impersonation headers (case-insensitive)
969+
headers: HTTP headers for this request. Override both client-level and impersonation headers (case-insensitive). To remove an impersonated header, pass an empty string as the value
970970
timeout: Request timeout in seconds (overrides default timeout)
971971
force_http3: Force HTTP/3 protocol
972972
"""
@@ -986,7 +986,7 @@ class AsyncClient:
986986
url: URL to request
987987
content: Raw content to send
988988
data: Form data to send in request body
989-
headers: HTTP headers for this request. Override both client-level and impersonation headers (case-insensitive)
989+
headers: HTTP headers for this request. Override both client-level and impersonation headers (case-insensitive). To remove an impersonated header, pass an empty string as the value
990990
timeout: Request timeout in seconds (overrides default timeout)
991991
force_http3: Force HTTP/3 protocol
992992
"""
@@ -1009,7 +1009,7 @@ class AsyncClient:
10091009
url: URL to request
10101010
content: Raw content to send
10111011
data: Form data to send in request body
1012-
headers: HTTP headers for this request. Override both client-level and impersonation headers (case-insensitive)
1012+
headers: HTTP headers for this request. Override both client-level and impersonation headers (case-insensitive). To remove an impersonated header, pass an empty string as the value
10131013
timeout: Request timeout in seconds (overrides default timeout)
10141014
force_http3: Force HTTP/3 protocol
10151015
stream: Whether to return a streaming response (default: False)
@@ -1043,7 +1043,7 @@ class AsyncClient:
10431043
url: URL to request
10441044
content: Raw content to send
10451045
data: Form data to send in request body
1046-
headers: HTTP headers for this request. Override both client-level and impersonation headers (case-insensitive)
1046+
headers: HTTP headers for this request. Override both client-level and impersonation headers (case-insensitive). To remove an impersonated header, pass an empty string as the value
10471047
timeout: Request timeout in seconds (overrides default timeout)
10481048
force_http3: Force HTTP/3 protocol
10491049
"""

impit-python/test/async_client_test.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,14 @@ async def test_overwriting_headers_work(self, browser: Browser) -> None:
299299
assert response.status_code == 200
300300
assert json.loads(response.text)['headers']['User-Agent'] == 'this is impit!'
301301

302+
@pytest.mark.asyncio
303+
async def test_removing_impersonated_headers_with_empty_string(self, browser: Browser) -> None:
304+
impit = AsyncClient(browser=browser)
305+
306+
response = await impit.get(get_httpbin_url('/headers'), headers={'Sec-Fetch-User': ''})
307+
assert response.status_code == 200
308+
assert 'Sec-Fetch-User' not in json.loads(response.text)['headers']
309+
302310
@pytest.mark.skip(reason='Flaky under the CI environment')
303311
@pytest.mark.asyncio
304312
async def test_http3_works(self, browser: Browser) -> None:

impit-python/test/basic_client_test.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,13 @@ def test_overwriting_headers_work(self, browser: Browser) -> None:
276276
assert response.status_code == 200
277277
assert response.json()['headers']['User-Agent'] == 'this is impit!'
278278

279+
def test_removing_impersonated_headers_with_empty_string(self, browser: Browser) -> None:
280+
impit = Client(browser=browser)
281+
282+
response = impit.get(get_httpbin_url('/headers'), headers={'Sec-Fetch-User': ''})
283+
assert response.status_code == 200
284+
assert 'Sec-Fetch-User' not in response.json()['headers']
285+
279286
@pytest.mark.skip(reason='Flaky under the CI environment')
280287
def test_http3_works(self, browser: Browser) -> None:
281288
impit = Client(browser=browser, http3=True)

0 commit comments

Comments
 (0)