Allow to Adjust HTTP Version and Use HTTP/2 by Default (#3506)

This commit is contained in:
Harshil 2023-01-21 01:45:02 +05:30 committed by GitHub
parent 21ebdbc973
commit deb6cb19b3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 126 additions and 4 deletions

View file

@ -135,7 +135,9 @@ As these features are *optional*, the corresponding 3rd party dependencies are n
Instead, they are listed as optional dependencies.
This allows to avoid unnecessary dependency conflicts for users who don't need the optional features.
The only required dependency is `httpx ~= 0.23.3 <https://www.python-httpx.org>`_ for ``telegram.request.HTTPXRequest``, the default networking backend.
The only required dependency is `httpx[http2] ~= 0.23.3 <https://www.python-httpx.org>`_ for
``telegram.request.HTTPXRequest``, the default networking backend. By default, HTTP/2 is used, as it
provides greater performance and stability, specially for concurrent requests.
``python-telegram-bot`` is most useful when used along with additional libraries.
To minimize dependency conflicts, we try to be liberal in terms of version requirements on the (optional) dependencies.

View file

@ -136,7 +136,9 @@ As these features are *optional*, the corresponding 3rd party dependencies are n
Instead, they are listed as optional dependencies.
This allows to avoid unnecessary dependency conflicts for users who don't need the optional features.
The only required dependency is `httpx ~= 0.23.3 <https://www.python-httpx.org>`_ for ``telegram.request.HTTPXRequest``, the default networking backend.
The only required dependency is `httpx[http2] ~= 0.23.3 <https://www.python-httpx.org>`_ for
``telegram.request.HTTPXRequest``, the default networking backend. By default, HTTP/2 is used, as
it provides greater performance and stability, specially for concurrent requests.
``python-telegram-bot`` is most useful when used along with additional libraries.
To minimize dependency conflicts, we try to be liberal in terms of version requirements on the (optional) dependencies.

View file

@ -6,4 +6,5 @@
# versions and only increase the lower bound if necessary
# httpx has no stable release yet, so let's be cautious for now
httpx ~= 0.23.3
# HTTP/2 is more performant and stable than HTTP/1.1, specially for concurrent requests
httpx[http2] ~= 0.23.3

View file

@ -70,12 +70,14 @@ _BOT_CHECKS = [
("connect_timeout", "connect_timeout"),
("read_timeout", "read_timeout"),
("write_timeout", "write_timeout"),
("http_version", "http_version"),
("get_updates_connection_pool_size", "get_updates_connection_pool_size"),
("get_updates_proxy_url", "get_updates_proxy_url"),
("get_updates_pool_timeout", "get_updates_pool_timeout"),
("get_updates_connect_timeout", "get_updates_connect_timeout"),
("get_updates_read_timeout", "get_updates_read_timeout"),
("get_updates_write_timeout", "get_updates_write_timeout"),
("get_updates_http_version", "get_updates_http_version"),
("base_file_url", "base_file_url"),
("base_url", "base_url"),
("token", "token"),
@ -137,6 +139,7 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
"_get_updates_read_timeout",
"_get_updates_request",
"_get_updates_write_timeout",
"_get_updates_http_version",
"_job_queue",
"_persistence",
"_pool_timeout",
@ -154,6 +157,7 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
"_updater",
"_write_timeout",
"_local_mode",
"_http_version",
)
def __init__(self: "InitApplicationBuilder"):
@ -174,6 +178,7 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
self._get_updates_write_timeout: ODVInput[float] = DEFAULT_NONE
self._get_updates_pool_timeout: ODVInput[float] = DEFAULT_NONE
self._get_updates_request: DVInput["BaseRequest"] = DEFAULT_NONE
self._get_updates_http_version: DVInput[str] = DefaultValue("2")
self._private_key: ODVInput[bytes] = DEFAULT_NONE
self._private_key_password: ODVInput[bytes] = DEFAULT_NONE
self._defaults: ODVInput["Defaults"] = DEFAULT_NONE
@ -199,6 +204,7 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
self._post_shutdown: Optional[Callable[[Application], Coroutine[Any, Any, None]]] = None
self._post_stop: Optional[Callable[[Application], Coroutine[Any, Any, None]]] = None
self._rate_limiter: ODVInput["BaseRateLimiter"] = DEFAULT_NONE
self._http_version: DVInput[str] = DefaultValue("2")
def _build_request(self, get_updates: bool) -> BaseRequest:
prefix = "_get_updates_" if get_updates else "_"
@ -226,9 +232,12 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
key: value for key, value in timeouts.items() if not isinstance(value, DefaultValue)
}
http_version = DefaultValue.get_value(getattr(self, f"{prefix}http_version")) or "2"
return HTTPXRequest(
connection_pool_size=connection_pool_size,
proxy_url=proxy_url,
http_version=http_version,
**effective_timeouts,
)
@ -401,11 +410,18 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
for attr in ("connect_timeout", "read_timeout", "write_timeout", "pool_timeout"):
if not isinstance(getattr(self, f"_{prefix}{attr}"), DefaultValue):
raise RuntimeError(_TWO_ARGS_REQ.format(name, attr))
if not isinstance(getattr(self, f"_{prefix}connection_pool_size"), DefaultValue):
raise RuntimeError(_TWO_ARGS_REQ.format(name, "connection_pool_size"))
if not isinstance(getattr(self, f"_{prefix}proxy_url"), DefaultValue):
raise RuntimeError(_TWO_ARGS_REQ.format(name, "proxy_url"))
if not isinstance(getattr(self, f"_{prefix}http_version"), DefaultValue):
raise RuntimeError(_TWO_ARGS_REQ.format(name, "http_version"))
self._bot_check(name)
if self._updater not in (DEFAULT_NONE, None):
raise RuntimeError(_TWO_ARGS_REQ.format(name, "updater instance"))
@ -543,6 +559,26 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
self._pool_timeout = pool_timeout
return self
def http_version(self: BuilderType, http_version: str) -> BuilderType:
"""Sets the HTTP protocol version which is used for the
:paramref:`~telegram.request.HTTPXRequest.http_version` parameter of
:attr:`telegram.Bot.request`. By default, HTTP/2 is used.
.. seealso:: :meth:`get_updates_http_version`
.. versionadded:: 20.1
Args:
http_version (:obj:`str`): Pass ``"1.1"`` if you'd like to use HTTP/1.1 for making
requests to Telegram. Defaults to ``"2"``, in which case HTTP/2 is used.
Returns:
:class:`ApplicationBuilder`: The same builder with the updated argument.
"""
self._request_param_check(name="http_version", get_updates=False)
self._http_version = http_version
return self
def get_updates_request(self: BuilderType, get_updates_request: BaseRequest) -> BuilderType:
"""Sets a :class:`telegram.request.BaseRequest` instance for the
:paramref:`~telegram.Bot.get_updates_request` parameter of
@ -664,6 +700,26 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
self._get_updates_pool_timeout = get_updates_pool_timeout
return self
def get_updates_http_version(self: BuilderType, get_updates_http_version: str) -> BuilderType:
"""Sets the HTTP protocol version which is used for the
:paramref:`~telegram.request.HTTPXRequest.http_version` parameter which is used in the
:meth:`telegram.Bot.get_updates` request. By default, HTTP/2 is used.
.. seealso:: :meth:`http_version`
.. versionadded:: 20.1
Args:
get_updates_http_version (:obj:`str`): Pass ``"1.1"`` if you'd like to use HTTP/1.1 for
making requests to Telegram. Defaults to ``"2"``, in which case HTTP/2 is used.
Returns:
:class:`ApplicationBuilder`: The same builder with the updated argument.
"""
self._request_param_check(name="http_version", get_updates=True)
self._get_updates_http_version = get_updates_http_version
return self
def private_key(
self: BuilderType,
private_key: Union[bytes, FilePathInput],

View file

@ -82,6 +82,11 @@ class HTTPXRequest(BaseRequest):
With a finite pool timeout, you must expect :exc:`telegram.error.TimedOut`
exceptions to be thrown when more requests are made simultaneously than there are
connections in the connection pool!
http_version (:obj:`str`, optional): If ``"1.1"``, HTTP/1.1 will be used instead of HTTP/2.
Defaults to ``"2"``.
.. versionadded:: 20.1
"""
__slots__ = ("_client", "_client_kwargs")
@ -94,6 +99,7 @@ class HTTPXRequest(BaseRequest):
write_timeout: Optional[float] = 5.0,
connect_timeout: Optional[float] = 5.0,
pool_timeout: Optional[float] = 1.0,
http_version: str = "2",
):
timeout = httpx.Timeout(
connect=connect_timeout,
@ -105,10 +111,18 @@ class HTTPXRequest(BaseRequest):
max_connections=connection_pool_size,
max_keepalive_connections=connection_pool_size,
)
if http_version not in ("1.1", "2"):
raise ValueError("`http_version` must be either '1.1' or '2'.")
http1 = http_version == "1.1"
self._client_kwargs = dict(
timeout=timeout,
proxies=proxy_url,
limits=limits,
http1=http1,
http2=not http1,
)
try:

View file

@ -90,6 +90,8 @@ class TestApplicationBuilder:
timeout: object
proxies: object
limits: object
http1: object
http2: object
monkeypatch.setattr(httpx, "AsyncClient", Client)
@ -118,11 +120,15 @@ class TestApplicationBuilder:
assert get_updates_client.timeout == httpx.Timeout(
connect=5.0, read=5.0, write=5.0, pool=1.0
)
assert not get_updates_client.http1
assert get_updates_client.http2 is True
client = app.bot.request._client
assert client.limits == httpx.Limits(max_connections=256, max_keepalive_connections=256)
assert client.proxies is None
assert client.timeout == httpx.Timeout(connect=5.0, read=5.0, write=5.0, pool=1.0)
assert not client.http1
assert client.http2 is True
assert isinstance(app.update_queue, asyncio.Queue)
assert isinstance(app.updater, Updater)
@ -165,6 +171,7 @@ class TestApplicationBuilder:
"proxy_url",
"bot",
"updater",
"http_version",
),
)
def test_mutually_exclusive_for_request(self, builder, method):
@ -189,6 +196,7 @@ class TestApplicationBuilder:
"get_updates_read_timeout",
"get_updates_write_timeout",
"get_updates_proxy_url",
"get_updates_http_version",
"bot",
"updater",
),
@ -216,12 +224,14 @@ class TestApplicationBuilder:
"get_updates_read_timeout",
"get_updates_write_timeout",
"get_updates_proxy_url",
"get_updates_http_version",
"connection_pool_size",
"connect_timeout",
"pool_timeout",
"read_timeout",
"write_timeout",
"proxy_url",
"http_version",
"bot",
"update_queue",
"rate_limiter",
@ -251,6 +261,7 @@ class TestApplicationBuilder:
"get_updates_read_timeout",
"get_updates_write_timeout",
"get_updates_proxy_url",
"get_updates_http_version",
"connection_pool_size",
"connect_timeout",
"pool_timeout",
@ -258,6 +269,7 @@ class TestApplicationBuilder:
"write_timeout",
"proxy_url",
"bot",
"http_version",
]
+ [entry[0] for entry in _BOT_CHECKS],
)
@ -308,19 +320,23 @@ class TestApplicationBuilder:
timeout: object
proxies: object
limits: object
http1: object
http2: object
monkeypatch.setattr(httpx, "AsyncClient", Client)
builder = ApplicationBuilder().token(bot.token)
builder.connection_pool_size(1).connect_timeout(2).pool_timeout(3).read_timeout(
4
).write_timeout(5).proxy_url("proxy_url")
).write_timeout(5).proxy_url("proxy_url").http_version("1.1")
app = builder.build()
client = app.bot.request._client
assert client.timeout == httpx.Timeout(pool=3, connect=2, read=4, write=5)
assert client.limits == httpx.Limits(max_connections=1, max_keepalive_connections=1)
assert client.proxies == "proxy_url"
assert client.http1 is True
assert client.http2 is False
builder = ApplicationBuilder().token(bot.token)
builder.get_updates_connection_pool_size(1).get_updates_connect_timeout(
@ -329,6 +345,8 @@ class TestApplicationBuilder:
5
).get_updates_proxy_url(
"proxy_url"
).get_updates_http_version(
"1.1"
)
app = builder.build()
client = app.bot._request[0]._client
@ -336,6 +354,8 @@ class TestApplicationBuilder:
assert client.timeout == httpx.Timeout(pool=3, connect=2, read=4, write=5)
assert client.limits == httpx.Limits(max_connections=1, max_keepalive_connections=1)
assert client.proxies == "proxy_url"
assert client.http1 is True
assert client.http2 is False
def test_custom_application_class(self, bot, builder):
class CustomApplication(Application):

View file

@ -338,6 +338,8 @@ class TestHTTPXRequest:
timeout: object
proxies: object
limits: object
http1: object
http2: object
monkeypatch.setattr(httpx, "AsyncClient", Client)
@ -347,6 +349,7 @@ class TestHTTPXRequest:
assert request._client.limits == httpx.Limits(
max_connections=1, max_keepalive_connections=1
)
assert request._client.http2 is True
request = HTTPXRequest(
connection_pool_size=42,
@ -400,6 +403,30 @@ class TestHTTPXRequest:
async with httpx_request:
await httpx_request.do_request(url="https://python-telegram-bot.org", method="GET")
async def test_http_version_error(self):
with pytest.raises(ValueError, match="`http_version` must be either"):
HTTPXRequest(http_version="1.0")
async def test_http_1_response(self):
httpx_request = HTTPXRequest(http_version="1.1")
async with httpx_request:
resp = await httpx_request._client.request(
url="https://python-telegram-bot.org",
method="GET",
headers={"User-Agent": httpx_request.USER_AGENT},
)
assert resp.http_version == "HTTP/1.1"
async def test_http_2_response(self):
httpx_request = HTTPXRequest()
async with httpx_request:
resp = await httpx_request._client.request(
url="https://python-telegram-bot.org",
method="GET",
headers={"User-Agent": httpx_request.USER_AGENT},
)
assert resp.http_version == "HTTP/2"
async def test_do_request_after_shutdown(self, httpx_request):
await httpx_request.shutdown()
with pytest.raises(RuntimeError, match="not initialized"):