Add a HTTP2_MAX_FRAME_SIZE setting (#7988)

This commit is contained in:
Adrian 2026-08-14 11:19:06 +02:00 committed by GitHub
parent f975e92159
commit a22523bfd3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 50 additions and 6 deletions

View File

@ -209,12 +209,8 @@ Known limitations of the HTTP/2 support:
- No support for HTTP/2 Cleartext (h2c), since no major browser supports
HTTP/2 unencrypted (refer `http2 faq`_).
- No setting to specify a maximum `frame size`_ larger than the default
value, 16384. Connections to servers that send a larger frame will fail.
- No support for `server pushes`_, which are ignored.
.. _frame size: https://datatracker.ietf.org/doc/html/rfc7540#section-4.2
.. _http2 faq: https://http2.github.io/faq/#does-http2-require-encryption
.. _server pushes: https://datatracker.ietf.org/doc/html/rfc7540#section-8.2

View File

@ -1499,6 +1499,28 @@ Default: ``None``
The Project ID that will be used when storing data on `Google Cloud Storage`_.
.. setting:: HTTP2_MAX_FRAME_SIZE
HTTP2_MAX_FRAME_SIZE
--------------------
.. versionadded:: VERSION
Default: ``16384``
Maximum `frame size`_, in bytes, that servers may send, between ``16384`` and
``16777215``. Connections to servers that send a larger frame fail.
Raise it for servers that send larger frames regardless of this value. Note
that :setting:`DOWNLOAD_MAXSIZE` and :setting:`DOWNLOAD_WARNSIZE` are checked
once per received frame, so a higher value allows a response to exceed them by
more before being caught.
:class:`~scrapy.core.downloader.handlers._httpx.HttpxDownloadHandler` ignores
this setting, as ``httpx`` does not allow configuring the frame size.
.. _frame size: https://datatracker.ietf.org/doc/html/rfc7540#section-4.2
.. setting:: ITEM_PIPELINES
ITEM_PIPELINES

View File

@ -21,6 +21,7 @@ from h2.events import (
WindowUpdated,
)
from h2.exceptions import FrameTooLargeError, H2Error
from h2.settings import SettingCodes
from twisted.internet.interfaces import (
IAddress,
IHandshakeListener,
@ -261,6 +262,9 @@ class H2ClientProtocol(Protocol, TimeoutMixin):
# Initiate H2 Connection
self.conn.initiate_connection()
max_frame_size = self._crawler.settings.getint("HTTP2_MAX_FRAME_SIZE")
if max_frame_size != self.conn.local_settings.max_frame_size:
self.conn.update_settings({SettingCodes.MAX_FRAME_SIZE: max_frame_size})
self._write_to_transport()
def _lose_connection_with_error(self, errors: list[BaseException]) -> None:

View File

@ -108,6 +108,7 @@ __all__ = [
"FTP_PASSWORD",
"FTP_USER",
"GCS_PROJECT_ID",
"HTTP2_MAX_FRAME_SIZE",
"HTTPAUTH_DOMAIN",
"HTTPAUTH_PASS",
"HTTPAUTH_USER",
@ -402,6 +403,8 @@ FTP_PASSWORD = "guest" # noqa: S105
GCS_PROJECT_ID = None
HTTP2_MAX_FRAME_SIZE = 16384
HTTPAUTH_USER = ""
HTTPAUTH_PASS = ""
HTTPAUTH_DOMAIN = None

View File

@ -37,6 +37,7 @@ if TYPE_CHECKING:
from collections.abc import AsyncGenerator, Callable, Coroutine, Generator
from scrapy.core._http2.protocol import H2ClientProtocol
from scrapy.crawler import Crawler
pytestmark = [
@ -236,9 +237,16 @@ class TestHttps2ClientProtocol:
) + self.certificate_file.read_text(encoding="utf-8")
return PrivateCertificate.loadPEM(pem) # type: ignore[no-any-return]
@pytest.fixture
def crawler(self, request: pytest.FixtureRequest) -> Crawler:
return get_crawler(settings_dict=getattr(request, "param", None))
@async_yield_fixture # type: ignore[untyped-decorator]
async def client(
self, server_port: int, client_certificate: PrivateCertificate
self,
server_port: int,
client_certificate: PrivateCertificate,
crawler: Crawler,
) -> AsyncGenerator[H2ClientProtocol]:
from twisted.internet import reactor
@ -250,7 +258,7 @@ class TestHttps2ClientProtocol:
acceptableProtocols=[b"h2"],
)
uri = URI.fromBytes(bytes(self.get_url(server_port, "/"), "utf-8"))
h2_client_factory = H2ClientFactory(uri, get_crawler(), Deferred())
h2_client_factory = H2ClientFactory(uri, crawler, Deferred())
client_endpoint = SSL4ClientEndpoint(
reactor, self.host, server_port, client_options
)
@ -312,6 +320,17 @@ class TestHttps2ClientProtocol:
request = Request(self.get_url(server_port, "/get-data-html-large"))
await self._check_GET(client, request, Data.HTML_LARGE, 200)
@pytest.mark.parametrize(
"crawler", [{"HTTP2_MAX_FRAME_SIZE": 1024**2}], indirect=True
)
@deferred_f_from_coro_f
async def test_GET_large_frames(
self, server_port: int, client: H2ClientProtocol
) -> None:
request = Request(self.get_url(server_port, "/get-data-html-large"))
await self._check_GET(client, request, Data.HTML_LARGE, 200)
assert client.conn.local_settings.max_frame_size == 1024**2
async def _check_GET_x10(
self,
client: H2ClientProtocol,