1313from apify_client .http_clients import HttpClient , HttpClientAsync , HttpResponse , ImpitHttpClient , ImpitHttpClientAsync
1414from apify_client .http_clients ._impit import _is_retryable_error
1515
16+ try :
17+ import brotli as _brotli_mod
18+
19+ _EXPECTED_ENCODING = 'br'
20+
21+ def _decompress (data : bytes ) -> bytes :
22+ return _brotli_mod .decompress (data )
23+
24+ except ImportError :
25+ try :
26+ import brotlicffi as _brotli_mod
27+
28+ _EXPECTED_ENCODING = 'br'
29+
30+ def _decompress (data : bytes ) -> bytes :
31+ return _brotli_mod .decompress (data )
32+
33+ except ImportError :
34+ _EXPECTED_ENCODING = 'gzip'
35+
36+ def _decompress (data : bytes ) -> bytes :
37+ return gzip .decompress (data )
38+
1639
1740class _ConcreteHttpClient (HttpClient ):
1841 """Minimal concrete HttpClient for testing base class helpers."""
@@ -278,7 +301,7 @@ def test_prepare_request_call_with_json() -> None:
278301 headers , _params , data = client ._prepare_request_call (json = json_data )
279302
280303 assert headers ['Content-Type' ] == 'application/json'
281- assert headers ['Content-Encoding' ] == 'gzip'
304+ assert headers ['Content-Encoding' ] == _EXPECTED_ENCODING
282305 assert data is not None
283306 assert isinstance (data , bytes )
284307
@@ -290,12 +313,10 @@ def test_prepare_request_call_with_empty_dict_json() -> None:
290313 headers , _params , data = client ._prepare_request_call (json = {})
291314
292315 assert headers ['Content-Type' ] == 'application/json'
293- assert headers ['Content-Encoding' ] == 'gzip'
316+ assert headers ['Content-Encoding' ] == _EXPECTED_ENCODING
294317 assert data is not None
295318 assert isinstance (data , bytes )
296- # Verify the gzipped data contains the JSON
297- decompressed = gzip .decompress (data )
298- assert decompressed == b'{}'
319+ assert _decompress (data ) == b'{}'
299320
300321
301322def test_prepare_request_call_with_empty_list_json () -> None :
@@ -305,12 +326,10 @@ def test_prepare_request_call_with_empty_list_json() -> None:
305326 headers , _params , data = client ._prepare_request_call (json = [])
306327
307328 assert headers ['Content-Type' ] == 'application/json'
308- assert headers ['Content-Encoding' ] == 'gzip'
329+ assert headers ['Content-Encoding' ] == _EXPECTED_ENCODING
309330 assert data is not None
310331 assert isinstance (data , bytes )
311- # Verify the gzipped data contains the JSON
312- decompressed = gzip .decompress (data )
313- assert decompressed == b'[]'
332+ assert _decompress (data ) == b'[]'
314333
315334
316335def test_prepare_request_call_with_zero_json () -> None :
@@ -320,12 +339,10 @@ def test_prepare_request_call_with_zero_json() -> None:
320339 headers , _params , data = client ._prepare_request_call (json = 0 )
321340
322341 assert headers ['Content-Type' ] == 'application/json'
323- assert headers ['Content-Encoding' ] == 'gzip'
342+ assert headers ['Content-Encoding' ] == _EXPECTED_ENCODING
324343 assert data is not None
325344 assert isinstance (data , bytes )
326- # Verify the gzipped data contains the JSON
327- decompressed = gzip .decompress (data )
328- assert decompressed == b'0'
345+ assert _decompress (data ) == b'0'
329346
330347
331348def test_prepare_request_call_with_false_json () -> None :
@@ -335,12 +352,10 @@ def test_prepare_request_call_with_false_json() -> None:
335352 headers , _params , data = client ._prepare_request_call (json = False )
336353
337354 assert headers ['Content-Type' ] == 'application/json'
338- assert headers ['Content-Encoding' ] == 'gzip'
355+ assert headers ['Content-Encoding' ] == _EXPECTED_ENCODING
339356 assert data is not None
340357 assert isinstance (data , bytes )
341- # Verify the gzipped data contains the JSON
342- decompressed = gzip .decompress (data )
343- assert decompressed == b'false'
358+ assert _decompress (data ) == b'false'
344359
345360
346361def test_prepare_request_call_with_empty_string_json () -> None :
@@ -350,12 +365,10 @@ def test_prepare_request_call_with_empty_string_json() -> None:
350365 headers , _params , data = client ._prepare_request_call (json = '' )
351366
352367 assert headers ['Content-Type' ] == 'application/json'
353- assert headers ['Content-Encoding' ] == 'gzip'
368+ assert headers ['Content-Encoding' ] == _EXPECTED_ENCODING
354369 assert data is not None
355370 assert isinstance (data , bytes )
356- # Verify the gzipped data contains the JSON
357- decompressed = gzip .decompress (data )
358- assert decompressed == b'""'
371+ assert _decompress (data ) == b'""'
359372
360373
361374def test_prepare_request_call_with_string_data () -> None :
@@ -364,7 +377,7 @@ def test_prepare_request_call_with_string_data() -> None:
364377
365378 headers , _params , data = client ._prepare_request_call (data = 'test string' )
366379
367- assert headers ['Content-Encoding' ] == 'gzip'
380+ assert headers ['Content-Encoding' ] == _EXPECTED_ENCODING
368381 assert isinstance (data , bytes )
369382
370383
@@ -374,7 +387,7 @@ def test_prepare_request_call_with_bytes_data() -> None:
374387
375388 headers , _params , data = client ._prepare_request_call (data = b'test bytes' )
376389
377- assert headers ['Content-Encoding' ] == 'gzip'
390+ assert headers ['Content-Encoding' ] == _EXPECTED_ENCODING
378391 assert isinstance (data , bytes )
379392
380393
@@ -452,3 +465,41 @@ def test_build_url_with_params_mixed() -> None:
452465 assert 'tags=a' in url
453466 assert 'tags=b' in url
454467 assert 'name=test' in url
468+
469+
470+ def test_prepare_request_call_brotli_compression (monkeypatch : pytest .MonkeyPatch ) -> None :
471+ """When a brotli compressor is available, request body uses brotli (Content-Encoding: br)."""
472+ brotlicffi = pytest .importorskip ('brotlicffi' )
473+ monkeypatch .setattr ('apify_client.http_clients._base._brotli_compress' , brotlicffi .compress )
474+
475+ client = _ConcreteHttpClient ()
476+ headers , _ , data = client ._prepare_request_call (json = {'k' : 'v' })
477+
478+ assert headers ['Content-Encoding' ] == 'br'
479+ assert data is not None
480+ assert brotlicffi .decompress (data ) == b'{"k": "v"}'
481+
482+
483+ def test_prepare_request_call_brotli_library_compression (monkeypatch : pytest .MonkeyPatch ) -> None :
484+ """When the brotli (C extension) library is available, request body uses brotli (Content-Encoding: br)."""
485+ brotli = pytest .importorskip ('brotli' )
486+ monkeypatch .setattr ('apify_client.http_clients._base._brotli_compress' , brotli .compress )
487+
488+ client = _ConcreteHttpClient ()
489+ headers , _ , data = client ._prepare_request_call (json = {'k' : 'v' })
490+
491+ assert headers ['Content-Encoding' ] == 'br'
492+ assert data is not None
493+ assert brotli .decompress (data ) == b'{"k": "v"}'
494+
495+
496+ def test_prepare_request_call_gzip_fallback_without_brotli (monkeypatch : pytest .MonkeyPatch ) -> None :
497+ """When no brotli library is available, request body falls back to gzip (Content-Encoding: gzip)."""
498+ monkeypatch .setattr ('apify_client.http_clients._base._brotli_compress' , None )
499+
500+ client = _ConcreteHttpClient ()
501+ headers , _ , data = client ._prepare_request_call (json = {'k' : 'v' })
502+
503+ assert headers ['Content-Encoding' ] == 'gzip'
504+ assert data is not None
505+ assert gzip .decompress (data ) == b'{"k": "v"}'
0 commit comments