|
1 |
| -from typing import Dict, Generator |
| 1 | +import logging |
| 2 | +import urllib.parse |
| 3 | +from http.client import RemoteDisconnected |
| 4 | +from typing import Dict, Generator, Literal, Optional |
2 | 5 |
|
| 6 | +from pip._vendor import requests, urllib3 |
3 | 7 | from pip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response
|
4 | 8 |
|
5 |
| -from pip._internal.exceptions import NetworkConnectionError |
| 9 | +from pip._internal.exceptions import ( |
| 10 | + ConnectionFailedError, |
| 11 | + ConnectionTimeoutError, |
| 12 | + NetworkConnectionError, |
| 13 | + ProxyConnectionError, |
| 14 | + SSLVerificationError, |
| 15 | +) |
| 16 | +from pip._internal.utils.compat import has_tls |
| 17 | +from pip._internal.utils.logging import VERBOSE |
6 | 18 |
|
7 | 19 | # The following comments and HTTP headers were originally added by
|
8 | 20 | # Donald Stufft in git commit 22c562429a61bb77172039e480873fb239dd8c03.
|
@@ -94,3 +106,128 @@ def response_chunks(
|
94 | 106 | if not chunk:
|
95 | 107 | break
|
96 | 108 | yield chunk
|
| 109 | + |
| 110 | + |
| 111 | +def raise_connection_error(error: requests.ConnectionError, *, timeout: float) -> None: |
| 112 | + """Raise a specific error for a given ConnectionError, if possible. |
| 113 | +
|
| 114 | + Note: requests.ConnectionError is the parent class of |
| 115 | + requests.ProxyError, requests.SSLError, and requests.ConnectTimeout |
| 116 | + so these errors are also handled here. In addition, a ReadTimeout |
| 117 | + wrapped in a requests.MayRetryError is converted into a |
| 118 | + ConnectionError by requests internally. |
| 119 | + """ |
| 120 | + # NOTE: this function was written defensively to degrade gracefully when |
| 121 | + # a specific error cannot be raised due to issues inspecting the exception |
| 122 | + # state. This logic isn't pretty. |
| 123 | + |
| 124 | + url = error.request.url |
| 125 | + # requests.ConnectionError.args[0] should be an instance of |
| 126 | + # urllib3.exceptions.MaxRetryError. |
| 127 | + reason = error.args[0] |
| 128 | + # Attempt to query the host from the urllib3 error, otherwise fall back to |
| 129 | + # parsing the request URL. |
| 130 | + if isinstance(reason, urllib3.exceptions.MaxRetryError): |
| 131 | + host = reason.pool.host |
| 132 | + # Narrow the reason further to the specific error from the last retry. |
| 133 | + reason = reason.reason |
| 134 | + else: |
| 135 | + host = urllib.parse.urlsplit(error.request.url).netloc |
| 136 | + |
| 137 | + if isinstance(reason, urllib3.exceptions.SSLError): |
| 138 | + raise SSLVerificationError(url, host, reason, is_tls_available=has_tls()) |
| 139 | + # NewConnectionError is a subclass of TimeoutError for some reason ... |
| 140 | + if isinstance(reason, urllib3.exceptions.TimeoutError) and not isinstance( |
| 141 | + reason, urllib3.exceptions.NewConnectionError |
| 142 | + ): |
| 143 | + kind: Literal["connect", "read"] = ( |
| 144 | + "connect" |
| 145 | + if isinstance(reason, urllib3.exceptions.ConnectTimeoutError) |
| 146 | + else "read" |
| 147 | + ) |
| 148 | + raise ConnectionTimeoutError(url, host, kind=kind, timeout=timeout) |
| 149 | + if isinstance(reason, urllib3.exceptions.ProxyError): |
| 150 | + raise ProxyConnectionError(url, host, reason) |
| 151 | + |
| 152 | + # Unknown error, give up and raise a generic error. |
| 153 | + raise ConnectionFailedError(url, host, reason) |
| 154 | + |
| 155 | + |
| 156 | +class Urllib3RetryFilter: |
| 157 | + """A logging filter which attempts to rewrite urllib3's retrying |
| 158 | + warnings to be more readable and less technical. |
| 159 | + """ |
| 160 | + |
| 161 | + def __init__(self, *, timeout: Optional[float]) -> None: |
| 162 | + self.timeout = timeout |
| 163 | + self.verbose = logging.getLogger().isEnabledFor(VERBOSE) # HACK |
| 164 | + |
| 165 | + def filter(self, record: logging.LogRecord) -> bool: |
| 166 | + # Attempt to "sniff out" the retrying warning. |
| 167 | + if not isinstance(record.args, tuple): |
| 168 | + return True |
| 169 | + |
| 170 | + retry = next( |
| 171 | + (a for a in record.args if isinstance(a, urllib3.util.Retry)), None |
| 172 | + ) |
| 173 | + if record.levelno != logging.WARNING or retry is None: |
| 174 | + # Not the right warning, leave it alone. |
| 175 | + return True |
| 176 | + |
| 177 | + error = next((a for a in record.args if isinstance(a, Exception)), None) |
| 178 | + if error is None: |
| 179 | + # No error information available, leave it alone. |
| 180 | + return True |
| 181 | + |
| 182 | + rewritten = False |
| 183 | + if isinstance(error, urllib3.exceptions.NewConnectionError): |
| 184 | + rewritten = True |
| 185 | + connection = error.pool |
| 186 | + record.msg = f"failed to connect to {connection.host}" |
| 187 | + if isinstance(connection, urllib3.connection.HTTPSConnection): |
| 188 | + record.msg += " via HTTPS" |
| 189 | + elif isinstance(connection, urllib3.connection.HTTPConnection): |
| 190 | + record.msg += " via HTTP" |
| 191 | + elif isinstance(error, urllib3.exceptions.SSLError): |
| 192 | + rewritten = True |
| 193 | + # Yes, this is the most information we can provide as urllib3 |
| 194 | + # doesn't give us much. |
| 195 | + record.msg = "SSL verification failed" |
| 196 | + elif isinstance(error, urllib3.exceptions.TimeoutError): |
| 197 | + rewritten = True |
| 198 | + record.msg = f"server didn't respond within {self.timeout} seconds" |
| 199 | + elif isinstance(error, urllib3.exceptions.ProtocolError): |
| 200 | + try: |
| 201 | + if isinstance(error.args[1], RemoteDisconnected): |
| 202 | + rewritten = True |
| 203 | + record.msg = "Server closed connection unexpectedly" |
| 204 | + except IndexError: |
| 205 | + pass |
| 206 | + elif isinstance(error, urllib3.exceptions.ProxyError): |
| 207 | + rewritten = True |
| 208 | + record.msg = "failed to connect to proxy" |
| 209 | + try: |
| 210 | + reason = error.args[1] |
| 211 | + proxy = reason.pool.host |
| 212 | + except Exception: |
| 213 | + pass |
| 214 | + else: |
| 215 | + record.msg += f" {proxy}" |
| 216 | + |
| 217 | + if rewritten: |
| 218 | + # The total remaining retries is already decremented when this |
| 219 | + # warning is raised. |
| 220 | + retries = retry.total + 1 |
| 221 | + assert isinstance(retry, urllib3.util.Retry) |
| 222 | + if retries > 1: |
| 223 | + record.msg += f", retrying {retries} more times" |
| 224 | + elif retries == 1: |
| 225 | + record.msg += ", retrying 1 last time" |
| 226 | + |
| 227 | + if self.verbose: |
| 228 | + # As it's hard to provide enough detail, show the original |
| 229 | + # error under verbose mode. |
| 230 | + record.msg += f": {error!s}" |
| 231 | + record.args = () |
| 232 | + |
| 233 | + return True |
0 commit comments