1"""HTTP related errors."""
2
3import asyncio
4import warnings
5from typing import TYPE_CHECKING, Union
6
7from multidict import MultiMapping
8
9from .typedefs import StrOrURL
10
11if TYPE_CHECKING:
12 import ssl
13
14 SSLContext = ssl.SSLContext
15else:
16 try:
17 import ssl
18
19 SSLContext = ssl.SSLContext
20 except ImportError: # pragma: no cover
21 ssl = SSLContext = None # type: ignore[assignment]
22
23if TYPE_CHECKING:
24 from .client_reqrep import ClientResponse, ConnectionKey, Fingerprint, RequestInfo
25 from .http_parser import RawResponseMessage
26else:
27 RequestInfo = ClientResponse = ConnectionKey = RawResponseMessage = None
28
29__all__ = (
30 "ClientError",
31 "ClientConnectionError",
32 "ClientConnectionResetError",
33 "ClientOSError",
34 "ClientConnectorError",
35 "ClientProxyConnectionError",
36 "ClientSSLError",
37 "ClientConnectorDNSError",
38 "ClientConnectorSSLError",
39 "ClientConnectorCertificateError",
40 "ConnectionTimeoutError",
41 "SocketTimeoutError",
42 "ServerConnectionError",
43 "ServerTimeoutError",
44 "ServerDisconnectedError",
45 "ServerFingerprintMismatch",
46 "ClientResponseError",
47 "ClientHttpProxyError",
48 "WSServerHandshakeError",
49 "ContentTypeError",
50 "ClientPayloadError",
51 "UploadAbortedError",
52 "InvalidURL",
53 "InvalidUrlClientError",
54 "RedirectClientError",
55 "NonHttpUrlClientError",
56 "InvalidUrlRedirectClientError",
57 "NonHttpUrlRedirectClientError",
58 "WSMessageTypeError",
59)
60
61
62class ClientError(Exception):
63 """Base class for client connection errors."""
64
65
66class ClientResponseError(ClientError):
67 """Base class for exceptions that occur after getting a response.
68
69 request_info: An instance of RequestInfo.
70 history: A sequence of responses, if redirects occurred.
71 status: HTTP status code.
72 message: Error message.
73 headers: Response headers.
74 """
75
76 def __init__(
77 self,
78 request_info: RequestInfo,
79 history: tuple[ClientResponse, ...],
80 *,
81 code: int | None = None,
82 status: int | None = None,
83 message: str = "",
84 headers: MultiMapping[str] | None = None,
85 ) -> None:
86 self.request_info = request_info
87 if code is not None:
88 if status is not None:
89 raise ValueError(
90 "Both code and status arguments are provided; "
91 "code is deprecated, use status instead"
92 )
93 warnings.warn(
94 "code argument is deprecated, use status instead",
95 DeprecationWarning,
96 stacklevel=2,
97 )
98 if status is not None:
99 self.status = status
100 elif code is not None:
101 self.status = code
102 else:
103 self.status = 0
104 self.message = message
105 self.headers = headers
106 self.history = history
107 self.args = (request_info, history)
108
109 def __str__(self) -> str:
110 return f"{self.status}, message={self.message!r}, url={str(self.request_info.real_url)!r}"
111
112 def __repr__(self) -> str:
113 args = f"{self.request_info!r}, {self.history!r}"
114 if self.status != 0:
115 args += f", status={self.status!r}"
116 if self.message != "":
117 args += f", message={self.message!r}"
118 if self.headers is not None:
119 args += f", headers={self.headers!r}"
120 return f"{type(self).__name__}({args})"
121
122 @property
123 def code(self) -> int:
124 warnings.warn(
125 "code property is deprecated, use status instead",
126 DeprecationWarning,
127 stacklevel=2,
128 )
129 return self.status
130
131 @code.setter
132 def code(self, value: int) -> None:
133 warnings.warn(
134 "code property is deprecated, use status instead",
135 DeprecationWarning,
136 stacklevel=2,
137 )
138 self.status = value
139
140
141class ContentTypeError(ClientResponseError):
142 """ContentType found is not valid."""
143
144
145class WSServerHandshakeError(ClientResponseError):
146 """websocket server handshake error."""
147
148
149class ClientHttpProxyError(ClientResponseError):
150 """HTTP proxy error.
151
152 Raised in :class:`aiohttp.connector.TCPConnector` if
153 proxy responds with status other than ``200 OK``
154 on ``CONNECT`` request.
155 """
156
157
158class TooManyRedirects(ClientResponseError):
159 """Client was redirected too many times."""
160
161
162class ClientConnectionError(ClientError):
163 """Base class for client socket errors."""
164
165
166class ClientConnectionResetError(ClientConnectionError, ConnectionResetError):
167 """ConnectionResetError"""
168
169
170class ClientOSError(ClientConnectionError, OSError):
171 """OSError error."""
172
173
174class ClientConnectorError(ClientOSError):
175 """Client connector error.
176
177 Raised in :class:`aiohttp.connector.TCPConnector` if
178 a connection can not be established.
179 """
180
181 def __init__(self, connection_key: ConnectionKey, os_error: OSError) -> None:
182 self._conn_key = connection_key
183 self._os_error = os_error
184 super().__init__(os_error.errno, os_error.strerror)
185 self.args = (connection_key, os_error)
186
187 @property
188 def os_error(self) -> OSError:
189 return self._os_error
190
191 @property
192 def host(self) -> str:
193 return self._conn_key.host
194
195 @property
196 def port(self) -> int | None:
197 return self._conn_key.port
198
199 @property
200 def ssl(self) -> Union[SSLContext, bool, "Fingerprint"]:
201 return self._conn_key.ssl
202
203 def __str__(self) -> str:
204 return "Cannot connect to host {0.host}:{0.port} ssl:{1} [{2}]".format(
205 self, "default" if self.ssl is True else self.ssl, self.strerror
206 )
207
208 # OSError.__reduce__ does too much black magick
209 __reduce__ = BaseException.__reduce__
210
211
212class ClientConnectorDNSError(ClientConnectorError):
213 """DNS resolution failed during client connection.
214
215 Raised in :class:`aiohttp.connector.TCPConnector` if
216 DNS resolution fails.
217 """
218
219
220class ClientProxyConnectionError(ClientConnectorError):
221 """Proxy connection error.
222
223 Raised in :class:`aiohttp.connector.TCPConnector` if
224 connection to proxy can not be established.
225 """
226
227
228class UnixClientConnectorError(ClientConnectorError):
229 """Unix connector error.
230
231 Raised in :py:class:`aiohttp.connector.UnixConnector`
232 if connection to unix socket can not be established.
233 """
234
235 def __init__(
236 self, path: str, connection_key: ConnectionKey, os_error: OSError
237 ) -> None:
238 self._path = path
239 super().__init__(connection_key, os_error)
240
241 @property
242 def path(self) -> str:
243 return self._path
244
245 def __str__(self) -> str:
246 return "Cannot connect to unix socket {0.path} ssl:{1} [{2}]".format(
247 self, "default" if self.ssl is True else self.ssl, self.strerror
248 )
249
250
251class ServerConnectionError(ClientConnectionError):
252 """Server connection errors."""
253
254
255class ServerDisconnectedError(ServerConnectionError):
256 """Server disconnected."""
257
258 def __init__(self, message: RawResponseMessage | str | None = None) -> None:
259 if message is None:
260 message = "Server disconnected"
261
262 self.args = (message,)
263 self.message = message
264
265
266class ServerTimeoutError(ServerConnectionError, asyncio.TimeoutError):
267 """Server timeout error."""
268
269
270class ConnectionTimeoutError(ServerTimeoutError):
271 """Connection timeout error."""
272
273
274class SocketTimeoutError(ServerTimeoutError):
275 """Socket timeout error."""
276
277
278class ServerFingerprintMismatch(ServerConnectionError):
279 """SSL certificate does not match expected fingerprint."""
280
281 def __init__(self, expected: bytes, got: bytes, host: str, port: int) -> None:
282 self.expected = expected
283 self.got = got
284 self.host = host
285 self.port = port
286 self.args = (expected, got, host, port)
287
288 def __repr__(self) -> str:
289 return f"<{self.__class__.__name__} expected={self.expected!r} got={self.got!r} host={self.host!r} port={self.port!r}>"
290
291
292class ClientPayloadError(ClientError):
293 """Response payload error."""
294
295
296class UploadAbortedError(ClientError):
297 """The request body was never fully sent."""
298
299
300class InvalidURL(ClientError, ValueError):
301 """Invalid URL.
302
303 URL used for fetching is malformed, e.g. it doesn't contains host
304 part.
305 """
306
307 # Derive from ValueError for backward compatibility
308
309 def __init__(self, url: StrOrURL, description: str | None = None) -> None:
310 # The type of url is not yarl.URL because the exception can be raised
311 # on URL(url) call
312 self._url = url
313 self._description = description
314
315 if description:
316 super().__init__(url, description)
317 else:
318 super().__init__(url)
319
320 @property
321 def url(self) -> StrOrURL:
322 return self._url
323
324 @property
325 def description(self) -> "str | None":
326 return self._description
327
328 def __repr__(self) -> str:
329 return f"<{self.__class__.__name__} {self}>"
330
331 def __str__(self) -> str:
332 if self._description:
333 return f"{self._url} - {self._description}"
334 return str(self._url)
335
336
337class InvalidUrlClientError(InvalidURL):
338 """Invalid URL client error."""
339
340
341class RedirectClientError(ClientError):
342 """Client redirect error."""
343
344
345class NonHttpUrlClientError(ClientError):
346 """Non http URL client error."""
347
348
349class InvalidUrlRedirectClientError(InvalidUrlClientError, RedirectClientError):
350 """Invalid URL redirect client error."""
351
352
353class NonHttpUrlRedirectClientError(NonHttpUrlClientError, RedirectClientError):
354 """Non http URL redirect client error."""
355
356
357class ClientSSLError(ClientConnectorError):
358 """Base error for ssl.*Errors."""
359
360
361if ssl is not None:
362 cert_errors = (ssl.CertificateError,)
363 cert_errors_bases = (
364 ClientSSLError,
365 ssl.CertificateError,
366 )
367
368 ssl_errors = (ssl.SSLError,)
369 ssl_error_bases = (ClientSSLError, ssl.SSLError)
370else: # pragma: no cover
371 cert_errors = tuple()
372 cert_errors_bases = (
373 ClientSSLError,
374 ValueError,
375 )
376
377 ssl_errors = tuple()
378 ssl_error_bases = (ClientSSLError,)
379
380
381class ClientConnectorSSLError(*ssl_error_bases): # type: ignore[misc]
382 """Response ssl error."""
383
384
385class ClientConnectorCertificateError(*cert_errors_bases): # type: ignore[misc]
386 """Response certificate error."""
387
388 _conn_key: ConnectionKey
389
390 def __init__(
391 # TODO: If we require ssl in future, this can become ssl.CertificateError
392 self,
393 connection_key: ConnectionKey,
394 certificate_error: Exception,
395 ) -> None:
396 if isinstance(certificate_error, cert_errors + (OSError,)):
397 # ssl.CertificateError has errno and strerror, so we should be fine
398 os_error = certificate_error
399 else:
400 os_error = OSError()
401
402 super().__init__(connection_key, os_error)
403 self._certificate_error = certificate_error
404 self.args = (connection_key, certificate_error)
405
406 @property
407 def certificate_error(self) -> Exception:
408 return self._certificate_error
409
410 @property
411 def host(self) -> str:
412 return self._conn_key.host
413
414 @property
415 def port(self) -> int | None:
416 return self._conn_key.port
417
418 @property
419 def ssl(self) -> bool:
420 return self._conn_key.is_ssl
421
422 def __str__(self) -> str:
423 return (
424 f"Cannot connect to host {self.host}:{self.port} ssl:{self.ssl} "
425 f"[{self.certificate_error.__class__.__name__}: "
426 f"{self.certificate_error.args}]"
427 )
428
429
430class WSMessageTypeError(TypeError):
431 """WebSocket message type is not valid."""