1"""Network-related pip exceptions."""
2
3from __future__ import annotations
4
5from typing import TYPE_CHECKING, Literal
6
7from pip._vendor.rich.markup import escape
8from pip._vendor.rich.text import Text
9
10from pip._internal.exceptions._base import DiagnosticPipError, PipError
11
12if TYPE_CHECKING:
13 from pip._vendor import urllib3
14 from pip._vendor.requests.models import PreparedRequest, Request, Response
15
16 from pip._internal.network.download import _FileDownload
17
18
19class NetworkConnectionError(PipError):
20 """HTTP connection error"""
21
22 def __init__(
23 self,
24 error_msg: str,
25 response: Response | None = None,
26 request: Request | PreparedRequest | None = None,
27 ) -> None:
28 """
29 Initialize NetworkConnectionError with `request` and `response`
30 objects.
31 """
32 self.response = response
33 self.request = request
34 self.error_msg = error_msg
35 if (
36 self.response is not None
37 and not self.request
38 and hasattr(response, "request")
39 ):
40 self.request = self.response.request
41 super().__init__(error_msg, response, request)
42
43 def __str__(self) -> str:
44 return str(self.error_msg)
45
46
47class ConnectionFailedError(DiagnosticPipError):
48 reference = "connection-failed"
49
50 def __init__(self, url: str, host: str, error: Exception) -> None:
51 from http.client import RemoteDisconnected
52
53 from pip._vendor.urllib3.exceptions import (
54 NameResolutionError,
55 NewConnectionError,
56 ProtocolError,
57 )
58
59 details = str(error)
60 if isinstance(error, NameResolutionError):
61 parts = details.split("Failed to resolve ", maxsplit=1)
62 if len(parts) == 2:
63 details = "Failed to resolve IP address for " + parts[1]
64 elif isinstance(error, NewConnectionError):
65 parts = details.split("Failed to establish a new connection: ", maxsplit=1)
66 if len(parts) == 2:
67 _, details = parts
68 elif isinstance(error, ProtocolError):
69 try:
70 reason = error.args[1]
71 except IndexError:
72 pass
73 else:
74 if isinstance(reason, (RemoteDisconnected, ConnectionResetError)):
75 details = (
76 "the connection was closed without a reply from the server."
77 )
78
79 super().__init__(
80 message=(
81 f"Failed to connect to [magenta]{escape(host)}[/] while fetching "
82 f"{escape(url)}"
83 ),
84 context=Text(details),
85 hint_stmt=(
86 "Are you connected to the Internet? If so, check whether your system "
87 f"can connect to [magenta]{escape(host)}[/] before trying again. "
88 "There may be a firewall or proxy that's preventing the connection."
89 ),
90 )
91
92
93class ConnectionTimeoutError(DiagnosticPipError):
94 reference = "connection-timeout"
95
96 def __init__(
97 self,
98 url: str,
99 host: str,
100 *,
101 kind: Literal["connect", "read"],
102 timeout: float,
103 ) -> None:
104 context = Text.assemble(
105 (host, "magenta"), f" didn't respond within {timeout} seconds"
106 )
107 if kind == "connect":
108 context.append(" (while establishing a connection)")
109 super().__init__(
110 message=f"Unable to fetch {escape(url)}",
111 context=context,
112 hint_stmt=(
113 "This is probably a temporary issue with the remote server or the "
114 "network connection. If this error persists, check the network "
115 "configuration. There may be a firewall or proxy that's preventing "
116 "the connection."
117 ),
118 )
119
120
121class SSLMissingError(DiagnosticPipError):
122 reference = "ssl-missing"
123
124 def __init__(self, url: str) -> None:
125 super().__init__(
126 message=f"Failed to establish a secure connection for {escape(url)}",
127 context="The 'ssl' module is unavailable but required for HTTPS URLs",
128 hint_stmt=None,
129 )
130
131
132class SSLVerificationError(DiagnosticPipError):
133 reference = "ssl-verification-failed"
134
135 def __init__(self, url: str, host: str, error: urllib3.exceptions.SSLError) -> None:
136 message = (
137 "Failed to establish a secure connection to "
138 f"[magenta]{escape(host)}[/] while fetching {escape(url)}"
139 )
140 hint = "You may need to use --cert or check your proxy/firewall configuration"
141 super().__init__(message=message, context=Text(str(error)), hint_stmt=hint)
142
143
144class ProxyConnectionError(DiagnosticPipError):
145 reference = "proxy-connection-failed"
146
147 def __init__(
148 self, url: str, proxy: str, error: urllib3.exceptions.ProxyError
149 ) -> None:
150 super().__init__(
151 message=(
152 "Failed to connect to proxy "
153 f"[magenta]{escape(proxy)}[/] while fetching {escape(url)}"
154 ),
155 context=Text(str(error)),
156 hint_stmt="This is likely a proxy configuration issue.",
157 )
158
159
160class IncompleteDownloadError(DiagnosticPipError):
161 """Raised when the downloader receives fewer bytes than advertised
162 in the Content-Length header."""
163
164 reference = "incomplete-download"
165
166 def __init__(self, download: _FileDownload) -> None:
167 # Dodge circular import.
168 from pip._internal.utils.misc import format_size
169
170 assert download.size is not None
171 download_status = (
172 f"{format_size(download.bytes_received)}/{format_size(download.size)}"
173 )
174 if download.reattempts:
175 retry_status = f"after {download.reattempts + 1} attempts "
176 hint = "Use --resume-retries to configure resume attempt limit."
177 else:
178 # Download retrying is not enabled.
179 retry_status = ""
180 hint = "Consider using --resume-retries to enable download resumption."
181 message = Text(
182 f"Download failed {retry_status}because not enough bytes "
183 f"were received ({download_status})"
184 )
185
186 super().__init__(
187 message=message,
188 context=f"URL: {download.link.redacted_url}",
189 hint_stmt=hint,
190 note_stmt="This is an issue with network connectivity, not pip.",
191 )