From db12600cffada9971a9df5107f437129e0b511b8 Mon Sep 17 00:00:00 2001 From: Hassaan Naushahi Date: Fri, 2 Jun 2023 15:24:58 +0500 Subject: [PATCH 1/7] testing first commit --- scrapy/downloadermiddlewares/httpcompression.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scrapy/downloadermiddlewares/httpcompression.py b/scrapy/downloadermiddlewares/httpcompression.py index ead426951..344efd0ea 100644 --- a/scrapy/downloadermiddlewares/httpcompression.py +++ b/scrapy/downloadermiddlewares/httpcompression.py @@ -29,7 +29,7 @@ class HttpCompressionMiddleware: """This middleware allows compressed (gzip, deflate) traffic to be sent/received from web sites""" - def __init__(self, stats=None): + def __init__(self, stats=None, settings=None): self.stats = stats @classmethod From 4d2e3c354cd2ac48a9d37908a3ded055cfd4ce0c Mon Sep 17 00:00:00 2001 From: Hassaan Naushahi Date: Fri, 2 Jun 2023 15:53:13 +0500 Subject: [PATCH 2/7] Add Content-Encoding header in response flag --- .../downloadermiddlewares/httpcompression.py | 81 ++++++++++++------- 1 file changed, 54 insertions(+), 27 deletions(-) diff --git a/scrapy/downloadermiddlewares/httpcompression.py b/scrapy/downloadermiddlewares/httpcompression.py index 344efd0ea..30a6952df 100644 --- a/scrapy/downloadermiddlewares/httpcompression.py +++ b/scrapy/downloadermiddlewares/httpcompression.py @@ -31,22 +31,51 @@ class HttpCompressionMiddleware: def __init__(self, stats=None, settings=None): self.stats = stats + if not stats: + warnings.warn( + "The default value of COMPRESSION_KEEP_ENCODING_HEADER, " + "False, is deprecated, and will stop working and stop " + "being its default value in a future version of Scrapy. " + "Set COMPRESSION_KEEP_ENCODING_HEADER=True in your " + "settings to remove this warning.", + ScrapyDeprecationWarning, + stacklevel=2, + ) + if settings: + self.keep_encoding_header = settings.getbool('COMPRESSION_KEEP_ENCODING_HEADER') + if not self.keep_encoding_header: + warnings.warn( + "Setting COMPRESSION_KEEP_ENCODING_HEADER=False is deprecated", + ScrapyDeprecationWarning, + ) + else: + self.keep_encoding_header = False + warnings.warn( + "The default value of COMPRESSION_KEEP_ENCODING_HEADER, " + "False, is deprecated, and will stop working and stop " + "being its default value in a future version of Scrapy. " + "Set COMPRESSION_KEEP_ENCODING_HEADER=True in your " + "settings to remove this warning.", + ScrapyDeprecationWarning, + stacklevel=2, + ) @classmethod def from_crawler(cls, crawler): if not crawler.settings.getbool("COMPRESSION_ENABLED"): raise NotConfigured try: - return cls(stats=crawler.stats) + return cls(stats=crawler.stats, settings=crawler.settings) except TypeError: warnings.warn( "HttpCompressionMiddleware subclasses must either modify " - "their '__init__' method to support a 'stats' parameter or " + "their '__init__' method to support 'stats' and 'settings' parameters or " "reimplement the 'from_crawler' method.", ScrapyDeprecationWarning, ) result = cls() result.stats = crawler.stats + result.keep_encoding_header = False return result def process_request(self, request, spider): @@ -55,32 +84,30 @@ class HttpCompressionMiddleware: def process_response(self, request, response, spider): if request.method == "HEAD": return response - if isinstance(response, Response): - content_encoding = response.headers.getlist("Content-Encoding") - if content_encoding: - encoding = content_encoding.pop() - decoded_body = self._decode(response.body, encoding.lower()) - if self.stats: - self.stats.inc_value( - "httpcompression/response_bytes", - len(decoded_body), - spider=spider, - ) - self.stats.inc_value( - "httpcompression/response_count", spider=spider - ) - respcls = responsetypes.from_args( - headers=response.headers, url=response.url, body=decoded_body - ) - kwargs = dict(cls=respcls, body=decoded_body) - if issubclass(respcls, TextResponse): - # force recalculating the encoding until we make sure the - # responsetypes guessing is reliable - kwargs["encoding"] = None - response = response.replace(**kwargs) - if not content_encoding: - del response.headers["Content-Encoding"] + if b'decoded' in response.flags: + return response + content_encoding = response.headers.getlist('Content-Encoding') + if not content_encoding: + return response + encoding = content_encoding[0] + decoded_body = self._decode(response.body, encoding.lower()) + if self.stats: + self.stats.inc_value('httpcompression/response_bytes', len(decoded_body), spider=spider) + self.stats.inc_value('httpcompression/response_count', spider=spider) + respcls = responsetypes.from_args( + headers=response.headers, url=response.url, body=decoded_body + ) + kwargs = dict(cls=respcls, body=decoded_body) + if issubclass(respcls, TextResponse): + # force recalculating the encoding until we make sure the + # responsetypes guessing is reliable + kwargs['encoding'] = None + + kwargs['flags'] = response.flags + [b'decoded'] + response = response.replace(**kwargs) + if not self.keep_encoding_header: + del response.headers['Content-Encoding'] return response def _decode(self, body, encoding): From 918c8ffd867599dc57bff25ba78d313a48ead1cb Mon Sep 17 00:00:00 2001 From: Hassaan Naushahi Date: Fri, 8 Sep 2023 14:39:30 +0500 Subject: [PATCH 3/7] Added default setting values for successful tests --- scrapy/settings/default_settings.py | 2 +- scrapy/templates/project/module/settings.py.tmpl | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/scrapy/settings/default_settings.py b/scrapy/settings/default_settings.py index 260ec1701..8400fc057 100644 --- a/scrapy/settings/default_settings.py +++ b/scrapy/settings/default_settings.py @@ -37,7 +37,7 @@ CLOSESPIDER_ERRORCOUNT = 0 COMMANDS_MODULE = "" COMPRESSION_ENABLED = True - +COMPRESSION_KEEP_ENCODING_HEADER = False CONCURRENT_ITEMS = 100 CONCURRENT_REQUESTS = 16 diff --git a/scrapy/templates/project/module/settings.py.tmpl b/scrapy/templates/project/module/settings.py.tmpl index ecb1e5e5c..9d337c530 100644 --- a/scrapy/templates/project/module/settings.py.tmpl +++ b/scrapy/templates/project/module/settings.py.tmpl @@ -86,6 +86,7 @@ ROBOTSTXT_OBEY = True #HTTPCACHE_DIR = "httpcache" #HTTPCACHE_IGNORE_HTTP_CODES = [] #HTTPCACHE_STORAGE = "scrapy.extensions.httpcache.FilesystemCacheStorage" +COMPRESSION_KEEP_ENCODING_HEADER = True # Set settings whose default value is deprecated to a future-proof value REQUEST_FINGERPRINTER_IMPLEMENTATION = "2.7" From 72962d6f5bb972e9a49cbe0960ed0903ade98c33 Mon Sep 17 00:00:00 2001 From: Hassaan Naushahi Date: Fri, 8 Sep 2023 17:30:03 +0500 Subject: [PATCH 4/7] Added test case and code review suggestions --- scrapy/settings/default_settings.py | 1 + .../templates/project/module/settings.py.tmpl | 2 + ...st_downloadermiddleware_httpcompression.py | 173 ++++++++++++++---- 3 files changed, 139 insertions(+), 37 deletions(-) diff --git a/scrapy/settings/default_settings.py b/scrapy/settings/default_settings.py index 8400fc057..04bb5a696 100644 --- a/scrapy/settings/default_settings.py +++ b/scrapy/settings/default_settings.py @@ -38,6 +38,7 @@ COMMANDS_MODULE = "" COMPRESSION_ENABLED = True COMPRESSION_KEEP_ENCODING_HEADER = False + CONCURRENT_ITEMS = 100 CONCURRENT_REQUESTS = 16 diff --git a/scrapy/templates/project/module/settings.py.tmpl b/scrapy/templates/project/module/settings.py.tmpl index 9d337c530..856b9240b 100644 --- a/scrapy/templates/project/module/settings.py.tmpl +++ b/scrapy/templates/project/module/settings.py.tmpl @@ -86,6 +86,8 @@ ROBOTSTXT_OBEY = True #HTTPCACHE_DIR = "httpcache" #HTTPCACHE_IGNORE_HTTP_CODES = [] #HTTPCACHE_STORAGE = "scrapy.extensions.httpcache.FilesystemCacheStorage" + +# Keep original Content-Encoding header COMPRESSION_KEEP_ENCODING_HEADER = True # Set settings whose default value is deprecated to a future-proof value diff --git a/tests/test_downloadermiddleware_httpcompression.py b/tests/test_downloadermiddleware_httpcompression.py index fac5588ff..f9e9b4661 100644 --- a/tests/test_downloadermiddleware_httpcompression.py +++ b/tests/test_downloadermiddleware_httpcompression.py @@ -13,6 +13,7 @@ from scrapy.downloadermiddlewares.httpcompression import ( from scrapy.exceptions import NotConfigured, ScrapyDeprecationWarning from scrapy.http import HtmlResponse, Request, Response from scrapy.responsetypes import responsetypes +from scrapy.settings import Settings from scrapy.spiders import Spider from scrapy.utils.gz import gunzip from scrapy.utils.test import get_crawler @@ -109,23 +110,25 @@ class HttpCompressionTest(TestCase): self.assertEqual(response.headers["Content-Encoding"], b"gzip") newresponse = self.mw.process_response(request, response, self.spider) - assert newresponse is not response - assert newresponse.body.startswith(b" Date: Mon, 30 Dec 2024 19:30:31 +0500 Subject: [PATCH 5/7] Fix merge errors and other simple mistakes. --- .../downloadermiddlewares/httpcompression.py | 35 ++-- ...st_downloadermiddleware_httpcompression.py | 161 ++++++++++-------- 2 files changed, 101 insertions(+), 95 deletions(-) diff --git a/scrapy/downloadermiddlewares/httpcompression.py b/scrapy/downloadermiddlewares/httpcompression.py index dfdad9864..95d5dba66 100644 --- a/scrapy/downloadermiddlewares/httpcompression.py +++ b/scrapy/downloadermiddlewares/httpcompression.py @@ -1,12 +1,13 @@ from __future__ import annotations import io +import warnings import zlib from typing import TYPE_CHECKING, List, Optional, Union from scrapy import Request, Spider from scrapy.crawler import Crawler -from scrapy.exceptions import NotConfigured +from scrapy.exceptions import NotConfigured, ScrapyDeprecationWarning from scrapy.http import Response, TextResponse from scrapy.responsetypes import responsetypes from scrapy.statscollectors import StatsCollector @@ -37,20 +38,12 @@ class HttpCompressionMiddleware: """This middleware allows compressed (gzip, deflate) traffic to be sent/received from web sites""" - def __init__(self, stats: Optional[StatsCollector] = None): + def __init__(self, stats: Optional[StatsCollector] = None, settings=None): self.stats = stats - if not stats: - warnings.warn( - "The default value of COMPRESSION_KEEP_ENCODING_HEADER, " - "False, is deprecated, and will stop working and stop " - "being its default value in a future version of Scrapy. " - "Set COMPRESSION_KEEP_ENCODING_HEADER=True in your " - "settings to remove this warning.", - ScrapyDeprecationWarning, - stacklevel=2, - ) if settings: - self.keep_encoding_header = settings.getbool('COMPRESSION_KEEP_ENCODING_HEADER') + self.keep_encoding_header = settings.getbool( + "COMPRESSION_KEEP_ENCODING_HEADER" + ) if not self.keep_encoding_header: warnings.warn( "Setting COMPRESSION_KEEP_ENCODING_HEADER=False is deprecated", @@ -97,17 +90,19 @@ class HttpCompressionMiddleware: ) -> Union[Request, Response]: if request.method == "HEAD": return response - if b'decoded' in response.flags: + if b"decoded" in response.flags: return response - content_encoding = response.headers.getlist('Content-Encoding') + content_encoding = response.headers.getlist("Content-Encoding") if not content_encoding: return response encoding = content_encoding[0] decoded_body = self._decode(response.body, encoding.lower()) if self.stats: - self.stats.inc_value('httpcompression/response_bytes', len(decoded_body), spider=spider) - self.stats.inc_value('httpcompression/response_count', spider=spider) + self.stats.inc_value( + "httpcompression/response_bytes", len(decoded_body), spider=spider + ) + self.stats.inc_value("httpcompression/response_count", spider=spider) respcls = responsetypes.from_args( headers=response.headers, url=response.url, body=decoded_body ) @@ -115,12 +110,12 @@ class HttpCompressionMiddleware: if issubclass(respcls, TextResponse): # force recalculating the encoding until we make sure the # responsetypes guessing is reliable - kwargs['encoding'] = None + kwargs["encoding"] = None - kwargs['flags'] = response.flags + [b'decoded'] + kwargs["flags"] = response.flags + [b"decoded"] response = response.replace(**kwargs) if not self.keep_encoding_header: - del response.headers['Content-Encoding'] + del response.headers["Content-Encoding"] return response def _decode(self, body: bytes, encoding: bytes) -> bytes: diff --git a/tests/test_downloadermiddleware_httpcompression.py b/tests/test_downloadermiddleware_httpcompression.py index 0c4fff629..b52882795 100644 --- a/tests/test_downloadermiddleware_httpcompression.py +++ b/tests/test_downloadermiddleware_httpcompression.py @@ -2,6 +2,7 @@ from gzip import GzipFile from io import BytesIO from pathlib import Path from unittest import SkipTest, TestCase +from warnings import catch_warnings from w3lib.encoding import resolve_encoding @@ -9,7 +10,7 @@ from scrapy.downloadermiddlewares.httpcompression import ( ACCEPTED_ENCODINGS, HttpCompressionMiddleware, ) -from scrapy.exceptions import NotConfigured +from scrapy.exceptions import NotConfigured, ScrapyDeprecationWarning from scrapy.http import HtmlResponse, Request, Response from scrapy.responsetypes import responsetypes from scrapy.settings import Settings @@ -110,14 +111,16 @@ class HttpCompressionTest(TestCase): self.assertEqual(response.headers["Content-Encoding"], b"gzip") newresponse = self.mw.process_response(request, response, self.spider) self.assertNotEqual(newresponse, response) - self.assertTrue(newresponse.body.startswith(b' Date: Fri, 26 Jun 2026 22:30:51 +0200 Subject: [PATCH 6/7] Update --- docs/topics/downloader-middleware.rst | 24 ++++++++++++ .../downloadermiddlewares/httpcompression.py | 27 +++++++++---- .../templates/project/module/settings.py.tmpl | 4 +- ...st_downloadermiddleware_httpcompression.py | 39 +++++++++++++++++-- 4 files changed, 79 insertions(+), 15 deletions(-) diff --git a/docs/topics/downloader-middleware.rst b/docs/topics/downloader-middleware.rst index 5649453b1..e8cab6a20 100644 --- a/docs/topics/downloader-middleware.rst +++ b/docs/topics/downloader-middleware.rst @@ -754,6 +754,30 @@ Default: ``True`` Whether the Compression middleware will be enabled. +.. setting:: COMPRESSION_KEEP_ENCODING_HEADER + +COMPRESSION_KEEP_ENCODING_HEADER +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. versionadded:: VERSION + +Default: ``False`` + +Whether to keep the original ``Content-Encoding`` header of a response after +:class:`HttpCompressionMiddleware` has decompressed its body. + +When ``True``, the original ``Content-Encoding`` header is kept, so that you +can tell how the response body was encoded before Scrapy decompressed it. When +``False``, that header is removed from decompressed responses. + +In both cases, a ``decoded`` flag (see :attr:`Response.flags`) is added to +decompressed responses, and responses that already have that flag are not +decompressed again. + +.. note:: ``False`` is the current default for backward compatibility, but it + is deprecated. Set this setting to ``True`` to keep the header; ``True`` + will be the only supported behavior in a future version of Scrapy. New + projects created with :command:`startproject` set it to ``True``. HttpProxyMiddleware ------------------- diff --git a/scrapy/downloadermiddlewares/httpcompression.py b/scrapy/downloadermiddlewares/httpcompression.py index b2c944482..26f1f01f8 100644 --- a/scrapy/downloadermiddlewares/httpcompression.py +++ b/scrapy/downloadermiddlewares/httpcompression.py @@ -89,7 +89,12 @@ class HttpCompressionMiddleware: ) if not self.keep_encoding_header: warnings.warn( - "Setting COMPRESSION_KEEP_ENCODING_HEADER=False is deprecated", + "COMPRESSION_KEEP_ENCODING_HEADER is False (its current default " + "value), so HttpCompressionMiddleware removes the " + "Content-Encoding header from decoded responses. This is " + "deprecated; set COMPRESSION_KEEP_ENCODING_HEADER to True to " + "keep that header, which will be the only supported behavior " + "in a future Scrapy version, and to silence this warning.", ScrapyDeprecationWarning, stacklevel=2, ) @@ -124,7 +129,7 @@ class HttpCompressionMiddleware: ) -> Request | Response: if request.method == "HEAD": return response - if b"decoded" in response.flags: + if "decoded" in response.flags: return response content_encoding = response.headers.getlist("Content-Encoding") if not content_encoding: @@ -132,7 +137,7 @@ class HttpCompressionMiddleware: max_size = request.meta.get("download_maxsize", self._max_size) warn_size = request.meta.get("download_warnsize", self._warn_size) try: - decoded_body, content_encoding = self._handle_encoding( + decoded_body, to_keep = self._handle_encoding( response.body, content_encoding, max_size ) except _DecompressionMaxSizeExceeded as e: @@ -148,9 +153,11 @@ class HttpCompressionMiddleware: f"({len(decoded_body)} B) is larger than the " f"download warning size ({warn_size} B)." ) - if content_encoding: - self._warn_unknown_encoding(response, content_encoding) - response.headers["Content-Encoding"] = content_encoding + if to_keep: + self._warn_unknown_encoding(response, to_keep) + # Drop the encodings that have been decoded so that the response class + # is guessed from the decoded body rather than from the compressed one. + response.headers["Content-Encoding"] = to_keep if self.stats: self.stats.inc_value( "httpcompression/response_bytes", @@ -165,9 +172,13 @@ class HttpCompressionMiddleware: # force recalculating the encoding until we make sure the # responsetypes guessing is reliable kwargs["encoding"] = None - kwargs["flags"] = [*response.flags, b"decoded"] + kwargs["flags"] = [*response.flags, "decoded"] response = response.replace(cls=respcls, **kwargs) - if not self.keep_encoding_header and not content_encoding: + if self.keep_encoding_header: + # Restore the original Content-Encoding header so that the spider + # can tell how the response body was encoded before decoding. + response.headers["Content-Encoding"] = content_encoding + elif not to_keep: del response.headers["Content-Encoding"] return response diff --git a/scrapy/templates/project/module/settings.py.tmpl b/scrapy/templates/project/module/settings.py.tmpl index 34ce6cb0e..d8c60f405 100644 --- a/scrapy/templates/project/module/settings.py.tmpl +++ b/scrapy/templates/project/module/settings.py.tmpl @@ -83,8 +83,6 @@ DOWNLOAD_DELAY = 1 #HTTPCACHE_IGNORE_HTTP_CODES = [] #HTTPCACHE_STORAGE = "scrapy.extensions.httpcache.FilesystemCacheStorage" -# Keep original Content-Encoding header -COMPRESSION_KEEP_ENCODING_HEADER = True - # Set settings whose default value is deprecated to a future-proof value +COMPRESSION_KEEP_ENCODING_HEADER = True FEED_EXPORT_ENCODING = "utf-8" diff --git a/tests/test_downloadermiddleware_httpcompression.py b/tests/test_downloadermiddleware_httpcompression.py index 039bd2dae..493146b84 100644 --- a/tests/test_downloadermiddleware_httpcompression.py +++ b/tests/test_downloadermiddleware_httpcompression.py @@ -796,8 +796,29 @@ class TestHttpCompression: newresponse = mw.process_response(request, response) assert newresponse is not response assert newresponse.body.startswith(b" Date: Fri, 26 Jun 2026 22:56:58 +0200 Subject: [PATCH 7/7] Address test issues --- tests/AsyncCrawlerProcess/reactorless_datauri.py | 7 ++++++- tests/AsyncCrawlerProcess/reactorless_simple.py | 7 ++++++- tests/AsyncCrawlerRunner/reactorless_datauri.py | 7 ++++++- tests/AsyncCrawlerRunner/reactorless_simple.py | 7 ++++++- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/tests/AsyncCrawlerProcess/reactorless_datauri.py b/tests/AsyncCrawlerProcess/reactorless_datauri.py index 5270b7b83..c31554cd4 100644 --- a/tests/AsyncCrawlerProcess/reactorless_datauri.py +++ b/tests/AsyncCrawlerProcess/reactorless_datauri.py @@ -12,7 +12,12 @@ class DataSpider(Spider): return {"data": response.text} -process = AsyncCrawlerProcess(settings={"TWISTED_REACTOR_ENABLED": False}) +process = AsyncCrawlerProcess( + settings={ + "TWISTED_REACTOR_ENABLED": False, + "COMPRESSION_KEEP_ENCODING_HEADER": True, + } +) process.crawl(DataSpider) process.start() diff --git a/tests/AsyncCrawlerProcess/reactorless_simple.py b/tests/AsyncCrawlerProcess/reactorless_simple.py index 33b4e8cb2..99f397365 100644 --- a/tests/AsyncCrawlerProcess/reactorless_simple.py +++ b/tests/AsyncCrawlerProcess/reactorless_simple.py @@ -12,7 +12,12 @@ class NoRequestsSpider(scrapy.Spider): yield -process = AsyncCrawlerProcess(settings={"TWISTED_REACTOR_ENABLED": False}) +process = AsyncCrawlerProcess( + settings={ + "TWISTED_REACTOR_ENABLED": False, + "COMPRESSION_KEEP_ENCODING_HEADER": True, + } +) process.crawl(NoRequestsSpider) process.start() diff --git a/tests/AsyncCrawlerRunner/reactorless_datauri.py b/tests/AsyncCrawlerRunner/reactorless_datauri.py index d964f9c1c..19f8a05ad 100644 --- a/tests/AsyncCrawlerRunner/reactorless_datauri.py +++ b/tests/AsyncCrawlerRunner/reactorless_datauri.py @@ -17,7 +17,12 @@ class DataSpider(Spider): async def main() -> None: configure_logging() - runner = AsyncCrawlerRunner(settings={"TWISTED_REACTOR_ENABLED": False}) + runner = AsyncCrawlerRunner( + settings={ + "TWISTED_REACTOR_ENABLED": False, + "COMPRESSION_KEEP_ENCODING_HEADER": True, + } + ) await runner.crawl(DataSpider) diff --git a/tests/AsyncCrawlerRunner/reactorless_simple.py b/tests/AsyncCrawlerRunner/reactorless_simple.py index e91c7c89b..99f364d23 100644 --- a/tests/AsyncCrawlerRunner/reactorless_simple.py +++ b/tests/AsyncCrawlerRunner/reactorless_simple.py @@ -17,7 +17,12 @@ class NoRequestsSpider(Spider): async def main() -> None: configure_logging() - runner = AsyncCrawlerRunner(settings={"TWISTED_REACTOR_ENABLED": False}) + runner = AsyncCrawlerRunner( + settings={ + "TWISTED_REACTOR_ENABLED": False, + "COMPRESSION_KEEP_ENCODING_HEADER": True, + } + ) await runner.crawl(NoRequestsSpider)