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

Issue:
[FEATURE] SSL verification + custom proxies
### What kind of feature are you missing? Where do you notice a shortcoming of PTB?

There is no way to disable ssl verification for proxies and pass a custom httpx.Proxy, instead of just a string.

### Describe the solution you'd like

Some proxies work as a man-in-the-middle and may lead to errors, like: ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1006). 

Due to this reason, I think it's necessary to allow control over ssl verification in HTTPXRequest. Just add an argument like `ssl_verify: bool = True` and add `verify=ssl_verify` to `self._client_kwargs`.

Another thing I'd love to see, is to change the type of:
`proxy_url: Optional[str] = None,`
to
`proxy_url: Optional[Union[str, httpx.Proxy] = None,`

This is due to some proxies requiring authorization through headers, which could be passed as:
```
httpx.Proxy(
    url=proxy_url,
    headers={
        "x-secret-header": "secret value"
    },
)
```
(moreover it might be used for passing logging headers to the proxy and so on)


### Describe alternatives you've considered

_No response_

### Additional context

_No response_


Discussion:
1. I really dislike verify=False. It is better to add a custom CA cert, if and only if the user trusts the entity performing ssl mitm.
I would argue that we shouldn't easily allow disabling ssl verification as it compromises the security guarantees of Telegram. If someone really really want it then they should inherit from HTTPXRequest (or monkey patch it) and mangle whatever they want, but then the responsibility and the implications are on them. 

2. The suggested approach to setting a proxy seems like a solid solution. We should do it.
@tsnoam 

1 - In my specific case, there is simply no way to provide a custom CA cert, but I can agree that this might be a one-of-a-kind problem, so it shouldn't be implemented. However, I'd ask you to review the patch I'm currently using:
```
def _build_client(self) -> httpx.AsyncClient:
    self._client_kwargs['verify'] = False
    return httpx.AsyncClient(**self._client_kwargs)
```
2 - If I could be of any assistance, I'd be glad to help. I can make a pull request, but since this seems like a minor change, I'd probably spend more time reading guidelines.



1: Your snippet looks okay to me. Just be aware that we make no guarantees about the stability of protected methods :)

> 2: if we allow `httpx.Proxie` input, then the name "proxy_url" is no longer the best. I suggest deprecating that argument in favor of a new `proxy` argument that accepts both a string or such an object.
One more comment: httpx accepts multiple proxies, e.f.

```python
proxies = {
    # Route all traffic through a proxy by default...
    "all://": "http://localhost:8030",
    # But don't use proxies for HTTPS requests to "domain.io"...
    "https://domain.io": None,
    # And use another proxy for requests to "example.com" and its subdomains...
    "all://*example.com": "http://localhost:8031",
    # And yet another proxy if HTTP is used,
    # and the "internal" subdomain on port 5550 is requested...
    "http://internal.example.com:5550": "http://localhost:8032",
}
```

Since in our case we only have one target to query, I see no need to allow for such functionality. Cc @tsnoam 

@codereptile you are ofc very welcome to contribute if you are interested :) 
v0.25.0 of httpx adds support for https proxies is encode/httpx#2845. Would this feature resolve the verification problem mentioned in the original comment (possibly after exposing this feature appropriately via PTBs interface)? CC @tsnoam 
@Bibo-Joshi As far as I'm concerned, yes, you could set up the https proxy correctly, if PTB would expose the https proxy feature.
Seems like both tickets could be solved with a single PR.
@Bibo-Joshi I think that ptb already supports getting a `httpx.Proxy` object over `proxy_url` as there's no enforcement to it being `str`.

So if I read the code right, the problem is semantics & documentation. Nothing on the code itself.

So basically, in ideal world what could have been changed is:
1. rename `proxy_url` to `proxy`
2. change `proxy` type from `str` to `httpx.Proxy`.
and that's it.

again, I hope I got things right here...
@tsnoam Yes, you can actually just throw in the proxy and it'll work (and that's how I did), but the type/name change would be nice to avoid IDE/mypy errors

Resolving patch:
diff --git a/telegram/ext/_applicationbuilder.py b/telegram/ext/_applicationbuilder.py
--- a/telegram/ext/_applicationbuilder.py
+++ b/telegram/ext/_applicationbuilder.py
@@ -32,9 +32,12 @@
     Union,
 )
 
+import httpx
+
 from telegram._bot import Bot
 from telegram._utils.defaultvalue import DEFAULT_FALSE, DEFAULT_NONE, DefaultValue
 from telegram._utils.types import DVInput, DVType, FilePathInput, HTTPVersion, ODVInput
+from telegram._utils.warnings import warn
 from telegram.ext._application import Application
 from telegram.ext._baseupdateprocessor import BaseUpdateProcessor, SimpleUpdateProcessor
 from telegram.ext._contexttypes import ContextTypes
@@ -44,6 +47,7 @@
 from telegram.ext._utils.types import BD, BT, CCT, CD, JQ, UD
 from telegram.request import BaseRequest
 from telegram.request._httpxrequest import HTTPXRequest
+from telegram.warnings import PTBDeprecationWarning
 
 if TYPE_CHECKING:
     from telegram.ext import BasePersistence, BaseRateLimiter, CallbackContext, Defaults
@@ -66,14 +70,14 @@
     ("request", "request instance"),
     ("get_updates_request", "get_updates_request instance"),
     ("connection_pool_size", "connection_pool_size"),
-    ("proxy_url", "proxy_url"),
+    ("proxy", "proxy"),
     ("pool_timeout", "pool_timeout"),
     ("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_proxy", "get_updates_proxy"),
     ("get_updates_pool_timeout", "get_updates_pool_timeout"),
     ("get_updates_connect_timeout", "get_updates_connect_timeout"),
     ("get_updates_read_timeout", "get_updates_read_timeout"),
@@ -136,7 +140,7 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
         "_get_updates_connect_timeout",
         "_get_updates_connection_pool_size",
         "_get_updates_pool_timeout",
-        "_get_updates_proxy_url",
+        "_get_updates_proxy",
         "_get_updates_read_timeout",
         "_get_updates_request",
         "_get_updates_write_timeout",
@@ -149,7 +153,7 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
         "_post_stop",
         "_private_key",
         "_private_key_password",
-        "_proxy_url",
+        "_proxy",
         "_rate_limiter",
         "_read_timeout",
         "_request",
@@ -166,14 +170,14 @@ def __init__(self: "InitApplicationBuilder"):
         self._base_url: DVType[str] = DefaultValue("https://api.telegram.org/bot")
         self._base_file_url: DVType[str] = DefaultValue("https://api.telegram.org/file/bot")
         self._connection_pool_size: DVInput[int] = DEFAULT_NONE
-        self._proxy_url: DVInput[str] = DEFAULT_NONE
+        self._proxy: DVInput[Union[str, httpx.Proxy, httpx.URL]] = DEFAULT_NONE
         self._connect_timeout: ODVInput[float] = DEFAULT_NONE
         self._read_timeout: ODVInput[float] = DEFAULT_NONE
         self._write_timeout: ODVInput[float] = DEFAULT_NONE
         self._pool_timeout: ODVInput[float] = DEFAULT_NONE
         self._request: DVInput[BaseRequest] = DEFAULT_NONE
         self._get_updates_connection_pool_size: DVInput[int] = DEFAULT_NONE
-        self._get_updates_proxy_url: DVInput[str] = DEFAULT_NONE
+        self._get_updates_proxy: DVInput[Union[str, httpx.Proxy, httpx.URL]] = DEFAULT_NONE
         self._get_updates_connect_timeout: ODVInput[float] = DEFAULT_NONE
         self._get_updates_read_timeout: ODVInput[float] = DEFAULT_NONE
         self._get_updates_write_timeout: ODVInput[float] = DEFAULT_NONE
@@ -214,7 +218,7 @@ def _build_request(self, get_updates: bool) -> BaseRequest:
         if not isinstance(getattr(self, f"{prefix}request"), DefaultValue):
             return getattr(self, f"{prefix}request")
 
-        proxy_url = DefaultValue.get_value(getattr(self, f"{prefix}proxy_url"))
+        proxy = DefaultValue.get_value(getattr(self, f"{prefix}proxy"))
         if get_updates:
             connection_pool_size = (
                 DefaultValue.get_value(getattr(self, f"{prefix}connection_pool_size")) or 1
@@ -239,7 +243,7 @@ def _build_request(self, get_updates: bool) -> BaseRequest:
 
         return HTTPXRequest(
             connection_pool_size=connection_pool_size,
-            proxy_url=proxy_url,
+            proxy=proxy,
             http_version=http_version,  # type: ignore[arg-type]
             **effective_timeouts,
         )
@@ -419,8 +423,8 @@ def _request_check(self, get_updates: bool) -> None:
         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}proxy"), DefaultValue):
+            raise RuntimeError(_TWO_ARGS_REQ.format(name, "proxy"))
 
         if not isinstance(getattr(self, f"_{prefix}http_version"), DefaultValue):
             raise RuntimeError(_TWO_ARGS_REQ.format(name, "http_version"))
@@ -486,21 +490,45 @@ def connection_pool_size(self: BuilderType, connection_pool_size: int) -> Builde
         return self
 
     def proxy_url(self: BuilderType, proxy_url: str) -> BuilderType:
-        """Sets the proxy for the :paramref:`~telegram.request.HTTPXRequest.proxy_url`
+        """Legacy name for :meth:`proxy`, kept for backward compatibility.
+
+        .. seealso:: :meth:`get_updates_proxy`
+
+        .. deprecated:: NEXT.VERSION
+
+        Args:
+            proxy_url (:obj:`str` | ``httpx.Proxy`` | ``httpx.URL``): See
+                :paramref:`telegram.ext.ApplicationBuilder.proxy.proxy`.
+
+        Returns:
+            :class:`ApplicationBuilder`: The same builder with the updated argument.
+        """
+        warn(
+            "`ApplicationBuilder.proxy_url` is deprecated since version "
+            "NEXT.VERSION. Use `ApplicationBuilder.proxy` instead.",
+            PTBDeprecationWarning,
+            stacklevel=2,
+        )
+        return self.proxy(proxy_url)
+
+    def proxy(self: BuilderType, proxy: Union[str, httpx.Proxy, httpx.URL]) -> BuilderType:
+        """Sets the proxy for the :paramref:`~telegram.request.HTTPXRequest.proxy`
         parameter of :attr:`telegram.Bot.request`. Defaults to :obj:`None`.
 
+        .. seealso:: :meth:`get_updates_proxy`
 
-        .. seealso:: :meth:`get_updates_proxy_url`
+        .. versionadded:: NEXT.VERSION
 
         Args:
-            proxy_url (:obj:`str`): The URL to the proxy server. See
-                :paramref:`telegram.request.HTTPXRequest.proxy_url` for more information.
+            proxy (:obj:`str` | ``httpx.Proxy`` | ``httpx.URL``): The URL to a proxy
+                server, a ``httpx.Proxy`` object or a ``httpx.URL`` object. See
+                :paramref:`telegram.request.HTTPXRequest.proxy` for more information.
 
         Returns:
             :class:`ApplicationBuilder`: The same builder with the updated argument.
         """
-        self._request_param_check(name="proxy_url", get_updates=False)
-        self._proxy_url = proxy_url
+        self._request_param_check(name="proxy", get_updates=False)
+        self._proxy = proxy
         return self
 
     def connect_timeout(self: BuilderType, connect_timeout: Optional[float]) -> BuilderType:
@@ -655,21 +683,47 @@ def get_updates_connection_pool_size(
         return self
 
     def get_updates_proxy_url(self: BuilderType, get_updates_proxy_url: str) -> BuilderType:
-        """Sets the proxy for the :paramref:`telegram.request.HTTPXRequest.proxy_url`
+        """Legacy name for :meth:`get_updates_proxy`, kept for backward compatibility.
+
+        .. seealso:: :meth:`proxy`
+
+        .. deprecated:: NEXT.VERSION
+
+        Args:
+            get_updates_proxy_url (:obj:`str` | ``httpx.Proxy`` | ``httpx.URL``): See
+                :paramref:`telegram.ext.ApplicationBuilder.get_updates_proxy.get_updates_proxy`.
+
+        Returns:
+            :class:`ApplicationBuilder`: The same builder with the updated argument.
+        """
+        warn(
+            "`ApplicationBuilder.get_updates_proxy_url` is deprecated since version "
+            "NEXT.VERSION. Use `ApplicationBuilder.get_updates_proxy` instead.",
+            PTBDeprecationWarning,
+            stacklevel=2,
+        )
+        return self.get_updates_proxy(get_updates_proxy_url)
+
+    def get_updates_proxy(
+        self: BuilderType, get_updates_proxy: Union[str, httpx.Proxy, httpx.URL]
+    ) -> BuilderType:
+        """Sets the proxy for the :paramref:`telegram.request.HTTPXRequest.proxy`
         parameter which is used for :meth:`telegram.Bot.get_updates`. Defaults to :obj:`None`.
 
+        .. seealso:: :meth:`proxy`
 
-        .. seealso:: :meth:`proxy_url`
+        .. versionadded:: NEXT.VERSION
 
         Args:
-            get_updates_proxy_url (:obj:`str`): The URL to the proxy server. See
-                :paramref:`telegram.request.HTTPXRequest.proxy_url` for more information.
+            proxy (:obj:`str` | ``httpx.Proxy`` | ``httpx.URL``): The URL to a proxy server,
+                a ``httpx.Proxy`` object or a ``httpx.URL`` object. See
+                :paramref:`telegram.request.HTTPXRequest.proxy` for more information.
 
         Returns:
             :class:`ApplicationBuilder`: The same builder with the updated argument.
         """
-        self._request_param_check(name="proxy_url", get_updates=True)
-        self._get_updates_proxy_url = get_updates_proxy_url
+        self._request_param_check(name="proxy", get_updates=True)
+        self._get_updates_proxy = get_updates_proxy
         return self
 
     def get_updates_connect_timeout(
diff --git a/telegram/request/_httpxrequest.py b/telegram/request/_httpxrequest.py
--- a/telegram/request/_httpxrequest.py
+++ b/telegram/request/_httpxrequest.py
@@ -24,9 +24,11 @@
 from telegram._utils.defaultvalue import DefaultValue
 from telegram._utils.logging import get_logger
 from telegram._utils.types import HTTPVersion, ODVInput
+from telegram._utils.warnings import warn
 from telegram.error import NetworkError, TimedOut
 from telegram.request._baserequest import BaseRequest
 from telegram.request._requestdata import RequestData
+from telegram.warnings import PTBDeprecationWarning
 
 # Note to future devs:
 # Proxies are currently only tested manually. The httpx development docs have a nice guide on that:
@@ -55,17 +57,10 @@ class HTTPXRequest(BaseRequest):
             Note:
                 Independent of the value, one additional connection will be reserved for
                 :meth:`telegram.Bot.get_updates`.
-        proxy_url (:obj:`str`, optional): The URL to the proxy server. For example
-            ``'http://127.0.0.1:3128'`` or ``'socks5://127.0.0.1:3128'``. Defaults to :obj:`None`.
-
-            Note:
-                * The proxy URL can also be set via the environment variables ``HTTPS_PROXY`` or
-                  ``ALL_PROXY``. See `the docs of httpx`_ for more info.
-                * For Socks5 support, additional dependencies are required. Make sure to install
-                  PTB via :command:`pip install "python-telegram-bot[socks]"` in this case.
-                * Socks5 proxies can not be set via environment variables.
+        proxy_url (:obj:`str`, optional): Legacy name for :paramref:`proxy`, kept for backward
+            compatibility. Defaults to :obj:`None`.
 
-            .. _the docs of httpx: https://www.python-httpx.org/environment_variables/#proxies
+            .. deprecated:: NEXT.VERSION
         read_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum
             amount of time (in seconds) to wait for a response from Telegram's server.
             This value is used unless a different value is passed to :meth:`do_request`.
@@ -107,6 +102,22 @@ class HTTPXRequest(BaseRequest):
                 these concepts.
 
             .. versionadded:: NEXT.VERSION
+        proxy (:obj:`str` | ``httpx.Proxy`` | ``httpx.URL``, optional): The URL to a proxy server,
+            a ``httpx.Proxy`` object or a ``httpx.URL`` object. For example
+            ``'http://127.0.0.1:3128'`` or ``'socks5://127.0.0.1:3128'``. Defaults to :obj:`None`.
+
+            Note:
+                * The proxy URL can also be set via the environment variables ``HTTPS_PROXY`` or
+                  ``ALL_PROXY``. See `the docs of httpx`_ for more info.
+                * HTTPS proxies can be configured by passing a ``httpx.Proxy`` object with
+                  a corresponding ``ssl_context``.
+                * For Socks5 support, additional dependencies are required. Make sure to install
+                  PTB via :command:`pip install "python-telegram-bot[socks]"` in this case.
+                * Socks5 proxies can not be set via environment variables.
+
+            .. _the docs of httpx: https://www.python-httpx.org/environment_variables/#proxies
+
+            .. versionadded:: NEXT.VERSION
 
     """
 
@@ -115,14 +126,27 @@ class HTTPXRequest(BaseRequest):
     def __init__(
         self,
         connection_pool_size: int = 1,
-        proxy_url: Optional[str] = None,
+        proxy_url: Optional[Union[str, httpx.Proxy, httpx.URL]] = None,
         read_timeout: Optional[float] = 5.0,
         write_timeout: Optional[float] = 5.0,
         connect_timeout: Optional[float] = 5.0,
         pool_timeout: Optional[float] = 1.0,
         http_version: HTTPVersion = "1.1",
         socket_options: Optional[Collection[_SocketOpt]] = None,
+        proxy: Optional[Union[str, httpx.Proxy, httpx.URL]] = None,
     ):
+        if proxy_url is not None and proxy is not None:
+            raise ValueError("The parameters `proxy_url` and `proxy` are mutually exclusive.")
+
+        if proxy_url is not None:
+            proxy = proxy_url
+            warn(
+                "The parameter `proxy_url` is deprecated since version NEXT.VERSION. Use `proxy` "
+                "instead.",
+                PTBDeprecationWarning,
+                stacklevel=2,
+            )
+
         self._http_version = http_version
         timeout = httpx.Timeout(
             connect=connect_timeout,
@@ -149,7 +173,7 @@ def __init__(
         )
         self._client_kwargs = {
             "timeout": timeout,
-            "proxies": proxy_url,
+            "proxies": proxy,
             "limits": limits,
             "transport": transport,
             **http_kwargs,


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
@@ -37,6 +37,7 @@
 from telegram.ext._applicationbuilder import _BOT_CHECKS
 from telegram.ext._baseupdateprocessor import SimpleUpdateProcessor
 from telegram.request import HTTPXRequest
+from telegram.warnings import PTBDeprecationWarning
 from tests.auxil.constants import PRIVATE_KEY
 from tests.auxil.envvars import TEST_WITH_OPT_DEPS
 from tests.auxil.files import data_file
@@ -169,6 +170,7 @@ def test_mutually_exclusive_for_bot(self, builder, method, description):
             "pool_timeout",
             "read_timeout",
             "write_timeout",
+            "proxy",
             "proxy_url",
             "bot",
             "updater",
@@ -178,8 +180,9 @@ def test_mutually_exclusive_for_bot(self, builder, method, description):
     def test_mutually_exclusive_for_request(self, builder, method):
         builder.request(1)
 
+        method_name = method.replace("proxy_url", "proxy")
         with pytest.raises(
-            RuntimeError, match=f"`{method}` may only be set, if no request instance"
+            RuntimeError, match=f"`{method_name}` may only be set, if no request instance"
         ):
             getattr(builder, method)(data_file("private.key"))
 
@@ -196,6 +199,7 @@ def test_mutually_exclusive_for_request(self, builder, method):
             "get_updates_pool_timeout",
             "get_updates_read_timeout",
             "get_updates_write_timeout",
+            "get_updates_proxy",
             "get_updates_proxy_url",
             "get_updates_http_version",
             "bot",
@@ -205,9 +209,10 @@ def test_mutually_exclusive_for_request(self, builder, method):
     def test_mutually_exclusive_for_get_updates_request(self, builder, method):
         builder.get_updates_request(1)
 
+        method_name = method.replace("proxy_url", "proxy")
         with pytest.raises(
             RuntimeError,
-            match=f"`{method}` may only be set, if no get_updates_request instance",
+            match=f"`{method_name}` may only be set, if no get_updates_request instance",
         ):
             getattr(builder, method)(data_file("private.key"))
 
@@ -225,12 +230,14 @@ def test_mutually_exclusive_for_get_updates_request(self, builder, method):
             "get_updates_read_timeout",
             "get_updates_write_timeout",
             "get_updates_proxy_url",
+            "get_updates_proxy",
             "get_updates_http_version",
             "connection_pool_size",
             "connect_timeout",
             "pool_timeout",
             "read_timeout",
             "write_timeout",
+            "proxy",
             "proxy_url",
             "http_version",
             "bot",
@@ -242,14 +249,17 @@ def test_mutually_exclusive_for_get_updates_request(self, builder, method):
     def test_mutually_exclusive_for_updater(self, builder, method):
         builder.updater(1)
 
+        method_name = method.replace("proxy_url", "proxy")
         with pytest.raises(
             RuntimeError,
-            match=f"`{method}` may only be set, if no updater",
+            match=f"`{method_name}` may only be set, if no updater",
         ):
             getattr(builder, method)(data_file("private.key"))
 
         builder = ApplicationBuilder()
         getattr(builder, method)(data_file("private.key"))
+
+        method = method.replace("proxy_url", "proxy")
         with pytest.raises(RuntimeError, match=f"`updater` may only be set, if no {method}"):
             builder.updater(1)
 
@@ -261,6 +271,7 @@ def test_mutually_exclusive_for_updater(self, builder, method):
             "get_updates_pool_timeout",
             "get_updates_read_timeout",
             "get_updates_write_timeout",
+            "get_updates_proxy",
             "get_updates_proxy_url",
             "get_updates_http_version",
             "connection_pool_size",
@@ -268,6 +279,7 @@ def test_mutually_exclusive_for_updater(self, builder, method):
             "pool_timeout",
             "read_timeout",
             "write_timeout",
+            "proxy",
             "proxy_url",
             "bot",
             "http_version",
@@ -285,7 +297,15 @@ def test_mutually_non_exclusive_for_updater(self, builder, method):
         getattr(builder, method)(data_file("private.key"))
         builder.updater(None)
 
-    def test_all_bot_args_custom(self, builder, bot, monkeypatch):
+    # We test with bot the new & legacy version to ensure that the legacy version still works
+    @pytest.mark.parametrize(
+        ("proxy_method", "get_updates_proxy_method"),
+        [("proxy", "get_updates_proxy"), ("proxy_url", "get_updates_proxy_url")],
+        ids=["new", "legacy"],
+    )
+    def test_all_bot_args_custom(
+        self, builder, bot, monkeypatch, proxy_method, get_updates_proxy_method
+    ):
         defaults = Defaults()
         request = HTTPXRequest()
         get_updates_request = HTTPXRequest()
@@ -330,13 +350,14 @@ class 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").http_version("1.1")
+        ).write_timeout(5).http_version("1.1")
+        getattr(builder, proxy_method)("proxy")
         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.proxies == "proxy"
         assert client.http1 is True
         assert client.http2 is False
 
@@ -345,17 +366,16 @@ class Client:
             2
         ).get_updates_pool_timeout(3).get_updates_read_timeout(4).get_updates_write_timeout(
             5
-        ).get_updates_proxy_url(
-            "proxy_url"
         ).get_updates_http_version(
             "1.1"
         )
+        getattr(builder, get_updates_proxy_method)("get_updates_proxy")
         app = builder.build()
         client = app.bot._request[0]._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.proxies == "get_updates_proxy"
         assert client.http1 is True
         assert client.http2 is False
 
@@ -478,3 +498,19 @@ def test_no_job_queue(self, bot, builder):
         assert app.job_queue is None
         assert isinstance(app.update_queue, asyncio.Queue)
         assert isinstance(app.updater, Updater)
+
+    def test_proxy_url_deprecation_warning(self, bot, builder, recwarn):
+        builder.token(bot.token).proxy_url("proxy_url")
+        assert len(recwarn) == 1
+        assert "`ApplicationBuilder.proxy_url` is deprecated" in str(recwarn[0].message)
+        assert recwarn[0].category is PTBDeprecationWarning
+        assert recwarn[0].filename == __file__, "wrong stacklevel"
+
+    def test_get_updates_proxy_url_deprecation_warning(self, bot, builder, recwarn):
+        builder.token(bot.token).get_updates_proxy_url("get_updates_proxy_url")
+        assert len(recwarn) == 1
+        assert "`ApplicationBuilder.get_updates_proxy_url` is deprecated" in str(
+            recwarn[0].message
+        )
+        assert recwarn[0].category is PTBDeprecationWarning
+        assert recwarn[0].filename == __file__, "wrong stacklevel"
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
@@ -43,6 +43,7 @@
     TimedOut,
 )
 from telegram.request._httpxrequest import HTTPXRequest
+from telegram.warnings import PTBDeprecationWarning
 from tests.auxil.envvars import TEST_WITH_OPT_DEPS
 from tests.auxil.slots import mro_slots
 
@@ -79,7 +80,7 @@ async def httpx_request():
 class TestNoSocksHTTP2WithoutRequest:
     async def test_init(self, bot):
         with pytest.raises(RuntimeError, match=r"python-telegram-bot\[socks\]"):
-            HTTPXRequest(proxy_url="socks5://foo")
+            HTTPXRequest(proxy="socks5://foo")
         with pytest.raises(RuntimeError, match=r"python-telegram-bot\[http2\]"):
             HTTPXRequest(http_version="2")
 
@@ -118,7 +119,7 @@ def __init__(self, *args, **kwargs):
 
         # Make sure that other exceptions are forwarded
         with pytest.raises(ImportError, match=r"Other Error Message"):
-            HTTPXRequest(proxy_url="socks5://foo")
+            HTTPXRequest(proxy="socks5://foo")
 
     def test_slot_behaviour(self):
         inst = HTTPXRequest()
@@ -359,7 +360,9 @@ class TestHTTPXRequestWithoutRequest:
     def _reset(self):
         self.test_flag = None
 
-    def test_init(self, monkeypatch):
+    # We parametrize this to make sure that the legacy `proxy_url` argument is still supported
+    @pytest.mark.parametrize("proxy_argument", ["proxy", "proxy_url"])
+    def test_init(self, monkeypatch, proxy_argument):
         @dataclass
         class Client:
             timeout: object
@@ -380,20 +383,32 @@ class Client:
         assert request._client.http1 is True
         assert not request._client.http2
 
-        request = HTTPXRequest(
-            connection_pool_size=42,
-            proxy_url="proxy_url",
-            connect_timeout=43,
-            read_timeout=44,
-            write_timeout=45,
-            pool_timeout=46,
-        )
-        assert request._client.proxies == "proxy_url"
+        kwargs = {
+            "connection_pool_size": 42,
+            proxy_argument: "proxy",
+            "connect_timeout": 43,
+            "read_timeout": 44,
+            "write_timeout": 45,
+            "pool_timeout": 46,
+        }
+        request = HTTPXRequest(**kwargs)
+        assert request._client.proxies == "proxy"
         assert request._client.limits == httpx.Limits(
             max_connections=42, max_keepalive_connections=42
         )
         assert request._client.timeout == httpx.Timeout(connect=43, read=44, write=45, pool=46)
 
+    def test_proxy_mutually_exclusive(self):
+        with pytest.raises(ValueError, match="mutually exclusive"):
+            HTTPXRequest(proxy="proxy", proxy_url="proxy_url")
+
+    def test_proxy_url_deprecation_warning(self, recwarn):
+        HTTPXRequest(proxy_url="http://127.0.0.1:3128")
+        assert len(recwarn) == 1
+        assert recwarn[0].category is PTBDeprecationWarning
+        assert "`proxy_url` is deprecated" in str(recwarn[0].message)
+        assert recwarn[0].filename == __file__, "incorrect stacklevel"
+
     async def test_multiple_inits_and_shutdowns(self, monkeypatch):
         self.test_flag = defaultdict(int)