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

Issue:
[FEATURE] More fine grained network exceptions
### What kind of feature are you missing? Where do you notice a shortcoming of PTB?

We currently only have `NetworkError` and `TimeoutError(NetworkError)` to indicate exceptions that are due to natworking problems (in contrast to exceptions issued by TG). This is not very fine grained and can make advanced exception handling difficult.

### Describe the solution you'd like

I'd like to add more subclasses. Looking it httpx, they have a [large number of them](https://github.com/encode/httpx/blob/2e2949c8ea88668f4cc5e509342b57e60fe4d86a/httpx/_exceptions.py#L1-L32). Not all of them add much value for us IMO. In the following I list all of the httpx excetpions and check the ones that I think would make sense to have for us:

- [ ] HTTPError
  - [ ] RequestError
    - [ ] TransportError
      - [x] TimeoutException ( (corresponds to `tg.TimeoutError`)
        - [x] ConnectTimeout
        - [x] ReadTimeout
        - [x] WriteTimeout
        - [x] PoolTimeout
      - [x] NetworkError (corresponds to `tg.NetworkError`)
        - [x] ConnectError
        - [x] ReadError
        - [x] WriteError
        - [x] CloseError
      - [ ] ProtocolError
        - [ ] LocalProtocolError
        - [ ] RemoteProtocolError
      - [x] ProxyError
      - [ ] UnsupportedProtocol
    - [ ] DecodingError
    - [ ] TooManyRedirects
  - [ ] HTTPStatusError
- [ ] InvalidURL
- [ ] CookieConflict
- [ ] StreamError
  - [ ] StreamConsumed
  - [ ] StreamClosed
  - [ ] ResponseNotRead
  - [ ] RequestNotRead

### Describe alternatives you've considered

_No response_

### Additional context

_No response_


Discussion:

Resolving patch:
diff --git a/telegram/error.py b/telegram/error.py
--- a/telegram/error.py
+++ b/telegram/error.py
@@ -118,6 +118,13 @@ def __init__(self, message: Optional[str] = None) -> None:
 class NetworkError(TelegramError):
     """Base class for exceptions due to networking errors.
 
+    Tip:
+        This exception (and its subclasses) usually originates from the networking backend
+        used by :class:`~telegram.request.HTTPXRequest`, or a custom implementation of
+        :class:`~telegram.request.BaseRequest`. In this case, the original exception can be
+        accessed via the ``__cause__``
+        `attribute <https://docs.python.org/3/library/exceptions.html#exception-context>`_.
+
     Examples:
         :any:`Raw API Bot <examples.rawapibot>`
     """


Test patch:
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
@@ -295,7 +295,7 @@ async def test_special_errors(
             (TelegramError("TelegramError"), TelegramError, "TelegramError"),
             (
                 RuntimeError("CustomError"),
-                Exception,
+                NetworkError,
                 r"HTTP implementation: RuntimeError\('CustomError'\)",
             ),
         ],
@@ -312,9 +312,12 @@ async def do_request(*args, **kwargs):
             do_request,
         )
 
-        with pytest.raises(catch_class, match=match):
+        with pytest.raises(catch_class, match=match) as exc_info:
             await httpx_request.post(None, None, None)
 
+        if catch_class is NetworkError:
+            assert exc_info.value.__cause__ is exception
+
     async def test_retrieve(self, monkeypatch, httpx_request):
         """Here we just test that retrieve gives us the raw bytes instead of trying to parse them
         as json
@@ -571,43 +574,49 @@ async def make_assertion(self, method, url, headers, timeout, files, data):
         assert content == b"content"
 
     @pytest.mark.parametrize(
-        ("raised_class", "expected_class", "expected_message"),
+        ("raised_exception", "expected_class", "expected_message"),
         [
-            (httpx.TimeoutException, TimedOut, "Timed out"),
-            (httpx.ReadError, NetworkError, "httpx.ReadError: message"),
+            (httpx.TimeoutException("timeout"), TimedOut, "Timed out"),
+            (httpx.ReadError("read_error"), NetworkError, "httpx.ReadError: read_error"),
         ],
     )
     async def test_do_request_exceptions(
-        self, monkeypatch, httpx_request, raised_class, expected_class, expected_message
+        self, monkeypatch, httpx_request, raised_exception, expected_class, expected_message
     ):
         async def make_assertion(self, method, url, headers, timeout, files, data):
-            raise raised_class("message")
+            raise raised_exception
 
         monkeypatch.setattr(httpx.AsyncClient, "request", make_assertion)
 
-        with pytest.raises(expected_class, match=expected_message):
+        with pytest.raises(expected_class, match=expected_message) as exc_info:
             await httpx_request.do_request(
                 "method",
                 "url",
             )
 
+        assert exc_info.value.__cause__ is raised_exception
+
     async def test_do_request_pool_timeout(self, monkeypatch):
+        pool_timeout = httpx.PoolTimeout("pool timeout")
+
         async def request(_, **kwargs):
             if self.test_flag is None:
                 self.test_flag = True
             else:
-                raise httpx.PoolTimeout("pool timeout")
+                raise pool_timeout
             return httpx.Response(HTTPStatus.OK)
 
         monkeypatch.setattr(httpx.AsyncClient, "request", request)
 
         async with HTTPXRequest(pool_timeout=0.02) as httpx_request:
-            with pytest.raises(TimedOut, match="Pool timeout"):
+            with pytest.raises(TimedOut, match="Pool timeout") as exc_info:
                 await asyncio.gather(
                     httpx_request.do_request(method="GET", url="URL"),
                     httpx_request.do_request(method="GET", url="URL"),
                 )
 
+            assert exc_info.value.__cause__ is pool_timeout
+
 
 @pytest.mark.skipif(not TEST_WITH_OPT_DEPS, reason="No need to run this twice")
 class TestHTTPXRequestWithRequest: