Repository: python-telegram-bot/python-telegram-bot

Issue:
[FEATURE] Set SocketOpts
To improve the networking backend, we would like to set socketopts if possible. Before the `asyncio` switch, we did something like

https://github.com/python-telegram-bot/python-telegram-bot/blob/ad5a1fb1f6a01f5f662e9e610ddb77fb57558e44/telegram/request/_httpxrequest.py#L164-L168

It would be nice if we could do that for httpx as well at some point.


Discussion:

Resolving patch:
diff --git a/telegram/request/_httpxrequest.py b/telegram/request/_httpxrequest.py
--- a/telegram/request/_httpxrequest.py
+++ b/telegram/request/_httpxrequest.py
@@ -17,7 +17,7 @@
 # You should have received a copy of the GNU Lesser Public License
 # along with this program.  If not, see [http://www.gnu.org/licenses/].
 """This module contains methods to make POST and GET requests using the httpx library."""
-from typing import Optional, Tuple
+from typing import Collection, Optional, Tuple, Union
 
 import httpx
 
@@ -35,6 +35,12 @@
 
 _LOGGER = get_logger(__name__, "HTTPXRequest")
 
+_SocketOpt = Union[
+    Tuple[int, int, int],
+    Tuple[int, int, Union[bytes, bytearray]],
+    Tuple[int, int, None, int],
+]
+
 
 class HTTPXRequest(BaseRequest):
     """Implementation of :class:`~telegram.request.BaseRequest` using the library
@@ -91,6 +97,16 @@ class HTTPXRequest(BaseRequest):
 
             .. versionchanged:: 20.5
                 Accept ``"2"`` as a valid value.
+        socket_options (Collection[:obj:`tuple`], optional): Socket options to be passed to the
+            underlying `library \
+            <https://www.encode.io/httpcore/async/#httpcore.AsyncConnectionPool.__init__>`_.
+
+            Note:
+                The values accepted by this parameter depend on the operating system.
+                This is a low-level parameter and should only be used if you are familiar with
+                these concepts.
+
+            .. versionadded:: NEXT.VERSION
 
     """
 
@@ -105,6 +121,7 @@ def __init__(
         connect_timeout: Optional[float] = 5.0,
         pool_timeout: Optional[float] = 1.0,
         http_version: HTTPVersion = "1.1",
+        socket_options: Optional[Collection[_SocketOpt]] = None,
     ):
         self._http_version = http_version
         timeout = httpx.Timeout(
@@ -122,16 +139,21 @@ def __init__(
             raise ValueError("`http_version` must be either '1.1', '2.0' or '2'.")
 
         http1 = http_version == "1.1"
-
-        # See https://github.com/python-telegram-bot/python-telegram-bot/pull/3542
-        # for why we need to use `dict()` here.
-        self._client_kwargs = dict(  # pylint: disable=use-dict-literal  # noqa: C408
-            timeout=timeout,
-            proxies=proxy_url,
-            limits=limits,
-            http1=http1,
-            http2=not http1,
+        http_kwargs = {"http1": http1, "http2": not http1}
+        transport = (
+            httpx.AsyncHTTPTransport(
+                socket_options=socket_options,
+            )
+            if socket_options
+            else None
         )
+        self._client_kwargs = {
+            "timeout": timeout,
+            "proxies": proxy_url,
+            "limits": limits,
+            "transport": transport,
+            **http_kwargs,
+        }
 
         try:
             self._client = self._build_client()
@@ -206,12 +228,6 @@ async def do_request(
             pool=pool_timeout,
         )
 
-        # TODO p0: On Linux, use setsockopt to properly set socket level keepalive.
-        #          (socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 120)
-        #          (socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 30)
-        #          (socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 8)
-        # TODO p4: Support setsockopt on lesser platforms than Linux.
-
         files = request_data.multipart_data if request_data else None
         data = request_data.json_parameters if request_data else None
 


Test patch:
diff --git a/tests/ext/test_applicationbuilder.py b/tests/ext/test_applicationbuilder.py
--- a/tests/ext/test_applicationbuilder.py
+++ b/tests/ext/test_applicationbuilder.py
@@ -91,6 +91,7 @@ class Client:
             limits: object
             http1: object
             http2: object
+            transport: object = None
 
         monkeypatch.setattr(httpx, "AsyncClient", Client)
 
@@ -322,6 +323,7 @@ class Client:
             limits: object
             http1: object
             http2: object
+            transport: object = None
 
         monkeypatch.setattr(httpx, "AsyncClient", Client)
 
diff --git a/tests/request/test_request.py b/tests/request/test_request.py
--- a/tests/request/test_request.py
+++ b/tests/request/test_request.py
@@ -28,6 +28,7 @@
 
 import httpx
 import pytest
+from httpx import AsyncHTTPTransport
 
 from telegram._utils.defaultvalue import DEFAULT_NONE
 from telegram.error import (
@@ -366,6 +367,7 @@ class Client:
             limits: object
             http1: object
             http2: object
+            transport: object = None
 
         monkeypatch.setattr(httpx, "AsyncClient", Client)
 
@@ -618,6 +620,24 @@ async def request(_, **kwargs):
 
             assert exc_info.value.__cause__ is pool_timeout
 
+    async def test_socket_opts(self, monkeypatch):
+        transport_kwargs = {}
+        transport_init = AsyncHTTPTransport.__init__
+
+        def init_transport(*args, **kwargs):
+            nonlocal transport_kwargs
+            transport_kwargs = kwargs.copy()
+            transport_init(*args, **kwargs)
+
+        monkeypatch.setattr(AsyncHTTPTransport, "__init__", init_transport)
+
+        HTTPXRequest()
+        assert "socket_options" not in transport_kwargs
+
+        transport_kwargs = {}
+        HTTPXRequest(socket_options=((1, 2, 3),))
+        assert transport_kwargs["socket_options"] == ((1, 2, 3),)
+
 
 @pytest.mark.skipif(not TEST_WITH_OPT_DEPS, reason="No need to run this twice")
 class TestHTTPXRequestWithRequest: