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