Coverage for /pythoncovmergedfiles/medio/medio/src/aiohttp/aiohttp/client_reqrep.py: 25%
617 statements
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-27 06:09 +0000
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-27 06:09 +0000
1import asyncio
2import codecs
3import contextlib
4import dataclasses
5import functools
6import io
7import re
8import sys
9import traceback
10import warnings
11from hashlib import md5, sha1, sha256
12from http.cookies import CookieError, Morsel, SimpleCookie
13from types import MappingProxyType, TracebackType
14from typing import (
15 TYPE_CHECKING,
16 Any,
17 Callable,
18 Dict,
19 Iterable,
20 List,
21 Literal,
22 Mapping,
23 Optional,
24 Tuple,
25 Type,
26 Union,
27 cast,
28)
30from multidict import CIMultiDict, CIMultiDictProxy, MultiDict, MultiDictProxy
31from yarl import URL
33from . import hdrs, helpers, http, multipart, payload
34from .abc import AbstractStreamWriter
35from .client_exceptions import (
36 ClientConnectionError,
37 ClientOSError,
38 ClientResponseError,
39 ContentTypeError,
40 InvalidURL,
41 ServerFingerprintMismatch,
42)
43from .compression_utils import HAS_BROTLI
44from .formdata import FormData
45from .hdrs import CONTENT_TYPE
46from .helpers import (
47 BaseTimerContext,
48 BasicAuth,
49 HeadersMixin,
50 TimerNoop,
51 basicauth_from_netrc,
52 is_expected_content_type,
53 netrc_from_env,
54 noop,
55 parse_mimetype,
56 reify,
57 set_result,
58)
59from .http import SERVER_SOFTWARE, HttpVersion10, HttpVersion11, StreamWriter
60from .log import client_logger
61from .streams import StreamReader
62from .typedefs import (
63 DEFAULT_JSON_DECODER,
64 JSONDecoder,
65 LooseCookies,
66 LooseHeaders,
67 RawHeaders,
68)
70try:
71 import ssl
72 from ssl import SSLContext
73except ImportError: # pragma: no cover
74 ssl = None # type: ignore[assignment]
75 SSLContext = object # type: ignore[misc,assignment]
78__all__ = ("ClientRequest", "ClientResponse", "RequestInfo", "Fingerprint")
81if TYPE_CHECKING: # pragma: no cover
82 from .client import ClientSession
83 from .connector import Connection
84 from .tracing import Trace
87_CONTAINS_CONTROL_CHAR_RE = re.compile(r"[^-!#$%&'*+.^_`|~0-9a-zA-Z]")
90def _gen_default_accept_encoding() -> str:
91 return "gzip, deflate, br" if HAS_BROTLI else "gzip, deflate"
94@dataclasses.dataclass(frozen=True)
95class ContentDisposition:
96 type: Optional[str]
97 parameters: "MappingProxyType[str, str]"
98 filename: Optional[str]
101@dataclasses.dataclass(frozen=True)
102class RequestInfo:
103 url: URL
104 method: str
105 headers: "CIMultiDictProxy[str]"
106 real_url: URL
109class Fingerprint:
110 HASHFUNC_BY_DIGESTLEN = {
111 16: md5,
112 20: sha1,
113 32: sha256,
114 }
116 def __init__(self, fingerprint: bytes) -> None:
117 digestlen = len(fingerprint)
118 hashfunc = self.HASHFUNC_BY_DIGESTLEN.get(digestlen)
119 if not hashfunc:
120 raise ValueError("fingerprint has invalid length")
121 elif hashfunc is md5 or hashfunc is sha1:
122 raise ValueError(
123 "md5 and sha1 are insecure and " "not supported. Use sha256."
124 )
125 self._hashfunc = hashfunc
126 self._fingerprint = fingerprint
128 @property
129 def fingerprint(self) -> bytes:
130 return self._fingerprint
132 def check(self, transport: asyncio.Transport) -> None:
133 if not transport.get_extra_info("sslcontext"):
134 return
135 sslobj = transport.get_extra_info("ssl_object")
136 cert = sslobj.getpeercert(binary_form=True)
137 got = self._hashfunc(cert).digest()
138 if got != self._fingerprint:
139 host, port, *_ = transport.get_extra_info("peername")
140 raise ServerFingerprintMismatch(self._fingerprint, got, host, port)
143if ssl is not None:
144 SSL_ALLOWED_TYPES = (ssl.SSLContext, bool, Fingerprint, type(None))
145else: # pragma: no cover
146 SSL_ALLOWED_TYPES = type(None)
149@dataclasses.dataclass(frozen=True)
150class ConnectionKey:
151 # the key should contain an information about used proxy / TLS
152 # to prevent reusing wrong connections from a pool
153 host: str
154 port: Optional[int]
155 is_ssl: bool
156 ssl: Union[SSLContext, None, Literal[False], Fingerprint]
157 proxy: Optional[URL]
158 proxy_auth: Optional[BasicAuth]
159 proxy_headers_hash: Optional[int] # hash(CIMultiDict)
162class ClientRequest:
163 GET_METHODS = {
164 hdrs.METH_GET,
165 hdrs.METH_HEAD,
166 hdrs.METH_OPTIONS,
167 hdrs.METH_TRACE,
168 }
169 POST_METHODS = {hdrs.METH_PATCH, hdrs.METH_POST, hdrs.METH_PUT}
170 ALL_METHODS = GET_METHODS.union(POST_METHODS).union({hdrs.METH_DELETE})
172 DEFAULT_HEADERS = {
173 hdrs.ACCEPT: "*/*",
174 hdrs.ACCEPT_ENCODING: _gen_default_accept_encoding(),
175 }
177 body = b""
178 auth = None
179 response = None
181 _writer = None # async task for streaming data
182 _continue = None # waiter future for '100 Continue' response
184 # N.B.
185 # Adding __del__ method with self._writer closing doesn't make sense
186 # because _writer is instance method, thus it keeps a reference to self.
187 # Until writer has finished finalizer will not be called.
189 def __init__(
190 self,
191 method: str,
192 url: URL,
193 *,
194 params: Optional[Mapping[str, str]] = None,
195 headers: Optional[LooseHeaders] = None,
196 skip_auto_headers: Iterable[str] = frozenset(),
197 data: Any = None,
198 cookies: Optional[LooseCookies] = None,
199 auth: Optional[BasicAuth] = None,
200 version: http.HttpVersion = http.HttpVersion11,
201 compress: Optional[str] = None,
202 chunked: Optional[bool] = None,
203 expect100: bool = False,
204 loop: asyncio.AbstractEventLoop,
205 response_class: Optional[Type["ClientResponse"]] = None,
206 proxy: Optional[URL] = None,
207 proxy_auth: Optional[BasicAuth] = None,
208 timer: Optional[BaseTimerContext] = None,
209 session: Optional["ClientSession"] = None,
210 ssl: Union[SSLContext, Literal[False], Fingerprint, None] = None,
211 proxy_headers: Optional[LooseHeaders] = None,
212 traces: Optional[List["Trace"]] = None,
213 trust_env: bool = False,
214 server_hostname: Optional[str] = None,
215 ):
216 match = _CONTAINS_CONTROL_CHAR_RE.search(method)
217 if match:
218 raise ValueError(
219 f"Method cannot contain non-token characters {method!r} "
220 f"(found at least {match.group()!r})"
221 )
222 assert isinstance(url, URL), url
223 assert isinstance(proxy, (URL, type(None))), proxy
224 # FIXME: session is None in tests only, need to fix tests
225 # assert session is not None
226 self._session = cast("ClientSession", session)
227 if params:
228 q = MultiDict(url.query)
229 url2 = url.with_query(params)
230 q.extend(url2.query)
231 url = url.with_query(q)
232 self.original_url = url
233 self.url = url.with_fragment(None)
234 self.method = method.upper()
235 self.chunked = chunked
236 self.compress = compress
237 self.loop = loop
238 self.length = None
239 if response_class is None:
240 real_response_class = ClientResponse
241 else:
242 real_response_class = response_class
243 self.response_class: Type[ClientResponse] = real_response_class
244 self._timer = timer if timer is not None else TimerNoop()
245 self._ssl = ssl
246 self.server_hostname = server_hostname
248 if loop.get_debug():
249 self._source_traceback = traceback.extract_stack(sys._getframe(1))
251 self.update_version(version)
252 self.update_host(url)
253 self.update_headers(headers)
254 self.update_auto_headers(skip_auto_headers)
255 self.update_cookies(cookies)
256 self.update_content_encoding(data)
257 self.update_auth(auth, trust_env)
258 self.update_proxy(proxy, proxy_auth, proxy_headers)
260 self.update_body_from_data(data)
261 if data is not None or self.method not in self.GET_METHODS:
262 self.update_transfer_encoding()
263 self.update_expect_continue(expect100)
264 if traces is None:
265 traces = []
266 self._traces = traces
268 def is_ssl(self) -> bool:
269 return self.url.scheme in ("https", "wss")
271 @property
272 def ssl(self) -> Union["SSLContext", None, Literal[False], Fingerprint]:
273 return self._ssl
275 @property
276 def connection_key(self) -> ConnectionKey:
277 proxy_headers = self.proxy_headers
278 if proxy_headers:
279 h: Optional[int] = hash(tuple((k, v) for k, v in proxy_headers.items()))
280 else:
281 h = None
282 return ConnectionKey(
283 self.host,
284 self.port,
285 self.is_ssl(),
286 self.ssl,
287 self.proxy,
288 self.proxy_auth,
289 h,
290 )
292 @property
293 def host(self) -> str:
294 ret = self.url.raw_host
295 assert ret is not None
296 return ret
298 @property
299 def port(self) -> Optional[int]:
300 return self.url.port
302 @property
303 def request_info(self) -> RequestInfo:
304 headers: CIMultiDictProxy[str] = CIMultiDictProxy(self.headers)
305 return RequestInfo(self.url, self.method, headers, self.original_url)
307 def update_host(self, url: URL) -> None:
308 """Update destination host, port and connection type (ssl)."""
309 # get host/port
310 if not url.raw_host:
311 raise InvalidURL(url)
313 # basic auth info
314 username, password = url.user, url.password
315 if username:
316 self.auth = helpers.BasicAuth(username, password or "")
318 def update_version(self, version: Union[http.HttpVersion, str]) -> None:
319 """Convert request version to two elements tuple.
321 parser HTTP version '1.1' => (1, 1)
322 """
323 if isinstance(version, str):
324 v = [part.strip() for part in version.split(".", 1)]
325 try:
326 version = http.HttpVersion(int(v[0]), int(v[1]))
327 except ValueError:
328 raise ValueError(
329 f"Can not parse http version number: {version}"
330 ) from None
331 self.version = version
333 def update_headers(self, headers: Optional[LooseHeaders]) -> None:
334 """Update request headers."""
335 self.headers: CIMultiDict[str] = CIMultiDict()
337 # add host
338 netloc = cast(str, self.url.raw_host)
339 if helpers.is_ipv6_address(netloc):
340 netloc = f"[{netloc}]"
341 # See https://github.com/aio-libs/aiohttp/issues/3636.
342 netloc = netloc.rstrip(".")
343 if self.url.port is not None and not self.url.is_default_port():
344 netloc += ":" + str(self.url.port)
345 self.headers[hdrs.HOST] = netloc
347 if headers:
348 if isinstance(headers, (dict, MultiDictProxy, MultiDict)):
349 headers = headers.items() # type: ignore[assignment]
351 for key, value in headers: # type: ignore[misc]
352 # A special case for Host header
353 if key.lower() == "host":
354 self.headers[key] = value
355 else:
356 self.headers.add(key, value)
358 def update_auto_headers(self, skip_auto_headers: Iterable[str]) -> None:
359 self.skip_auto_headers = CIMultiDict(
360 (hdr, None) for hdr in sorted(skip_auto_headers)
361 )
362 used_headers = self.headers.copy()
363 used_headers.extend(self.skip_auto_headers) # type: ignore[arg-type]
365 for hdr, val in self.DEFAULT_HEADERS.items():
366 if hdr not in used_headers:
367 self.headers.add(hdr, val)
369 if hdrs.USER_AGENT not in used_headers:
370 self.headers[hdrs.USER_AGENT] = SERVER_SOFTWARE
372 def update_cookies(self, cookies: Optional[LooseCookies]) -> None:
373 """Update request cookies header."""
374 if not cookies:
375 return
377 c: SimpleCookie[str] = SimpleCookie()
378 if hdrs.COOKIE in self.headers:
379 c.load(self.headers.get(hdrs.COOKIE, ""))
380 del self.headers[hdrs.COOKIE]
382 if isinstance(cookies, Mapping):
383 iter_cookies = cookies.items()
384 else:
385 iter_cookies = cookies # type: ignore[assignment]
386 for name, value in iter_cookies:
387 if isinstance(value, Morsel):
388 # Preserve coded_value
389 mrsl_val = value.get(value.key, Morsel())
390 mrsl_val.set(value.key, value.value, value.coded_value)
391 c[name] = mrsl_val
392 else:
393 c[name] = value # type: ignore[assignment]
395 self.headers[hdrs.COOKIE] = c.output(header="", sep=";").strip()
397 def update_content_encoding(self, data: Any) -> None:
398 """Set request content encoding."""
399 if data is None:
400 return
402 enc = self.headers.get(hdrs.CONTENT_ENCODING, "").lower()
403 if enc:
404 if self.compress:
405 raise ValueError(
406 "compress can not be set " "if Content-Encoding header is set"
407 )
408 elif self.compress:
409 if not isinstance(self.compress, str):
410 self.compress = "deflate"
411 self.headers[hdrs.CONTENT_ENCODING] = self.compress
412 self.chunked = True # enable chunked, no need to deal with length
414 def update_transfer_encoding(self) -> None:
415 """Analyze transfer-encoding header."""
416 te = self.headers.get(hdrs.TRANSFER_ENCODING, "").lower()
418 if "chunked" in te:
419 if self.chunked:
420 raise ValueError(
421 "chunked can not be set "
422 'if "Transfer-Encoding: chunked" header is set'
423 )
425 elif self.chunked:
426 if hdrs.CONTENT_LENGTH in self.headers:
427 raise ValueError(
428 "chunked can not be set " "if Content-Length header is set"
429 )
431 self.headers[hdrs.TRANSFER_ENCODING] = "chunked"
432 else:
433 if hdrs.CONTENT_LENGTH not in self.headers:
434 self.headers[hdrs.CONTENT_LENGTH] = str(len(self.body))
436 def update_auth(self, auth: Optional[BasicAuth], trust_env: bool = False) -> None:
437 """Set basic auth."""
438 if auth is None:
439 auth = self.auth
440 if auth is None and trust_env and self.url.host is not None:
441 netrc_obj = netrc_from_env()
442 with contextlib.suppress(LookupError):
443 auth = basicauth_from_netrc(netrc_obj, self.url.host)
444 if auth is None:
445 return
447 if not isinstance(auth, helpers.BasicAuth):
448 raise TypeError("BasicAuth() tuple is required instead")
450 self.headers[hdrs.AUTHORIZATION] = auth.encode()
452 def update_body_from_data(self, body: Any) -> None:
453 if body is None:
454 return
456 # FormData
457 if isinstance(body, FormData):
458 body = body()
460 try:
461 body = payload.PAYLOAD_REGISTRY.get(body, disposition=None)
462 except payload.LookupError:
463 boundary = None
464 if CONTENT_TYPE in self.headers:
465 boundary = parse_mimetype(self.headers[CONTENT_TYPE]).parameters.get(
466 "boundary"
467 )
468 body = FormData(body, boundary=boundary)()
470 self.body = body
472 # enable chunked encoding if needed
473 if not self.chunked:
474 if hdrs.CONTENT_LENGTH not in self.headers:
475 size = body.size
476 if size is None:
477 self.chunked = True
478 else:
479 if hdrs.CONTENT_LENGTH not in self.headers:
480 self.headers[hdrs.CONTENT_LENGTH] = str(size)
482 # copy payload headers
483 assert body.headers
484 for key, value in body.headers.items():
485 if key in self.headers:
486 continue
487 if key in self.skip_auto_headers:
488 continue
489 self.headers[key] = value
491 def update_expect_continue(self, expect: bool = False) -> None:
492 if expect:
493 self.headers[hdrs.EXPECT] = "100-continue"
494 elif self.headers.get(hdrs.EXPECT, "").lower() == "100-continue":
495 expect = True
497 if expect:
498 self._continue = self.loop.create_future()
500 def update_proxy(
501 self,
502 proxy: Optional[URL],
503 proxy_auth: Optional[BasicAuth],
504 proxy_headers: Optional[LooseHeaders],
505 ) -> None:
506 if proxy_auth and not isinstance(proxy_auth, helpers.BasicAuth):
507 raise ValueError("proxy_auth must be None or BasicAuth() tuple")
508 self.proxy = proxy
509 self.proxy_auth = proxy_auth
510 self.proxy_headers = proxy_headers
512 def keep_alive(self) -> bool:
513 if self.version < HttpVersion10:
514 # keep alive not supported at all
515 return False
516 if self.version == HttpVersion10:
517 if self.headers.get(hdrs.CONNECTION) == "keep-alive":
518 return True
519 else: # no headers means we close for Http 1.0
520 return False
521 elif self.headers.get(hdrs.CONNECTION) == "close":
522 return False
524 return True
526 async def write_bytes(
527 self, writer: AbstractStreamWriter, conn: "Connection"
528 ) -> None:
529 """Support coroutines that yields bytes objects."""
530 # 100 response
531 if self._continue is not None:
532 await writer.drain()
533 await self._continue
535 protocol = conn.protocol
536 assert protocol is not None
537 try:
538 if isinstance(self.body, payload.Payload):
539 await self.body.write(writer)
540 else:
541 if isinstance(self.body, (bytes, bytearray)):
542 self.body = (self.body,) # type: ignore[assignment]
544 for chunk in self.body:
545 await writer.write(chunk) # type: ignore[arg-type]
547 await writer.write_eof()
548 except OSError as exc:
549 if exc.errno is None and isinstance(exc, asyncio.TimeoutError):
550 protocol.set_exception(exc)
551 else:
552 new_exc = ClientOSError(
553 exc.errno, "Can not write request body for %s" % self.url
554 )
555 new_exc.__context__ = exc
556 new_exc.__cause__ = exc
557 protocol.set_exception(new_exc)
558 except asyncio.CancelledError as exc:
559 if not conn.closed:
560 protocol.set_exception(exc)
561 except Exception as exc:
562 protocol.set_exception(exc)
563 else:
564 protocol.start_timeout()
565 finally:
566 self._writer = None
568 async def send(self, conn: "Connection") -> "ClientResponse":
569 # Specify request target:
570 # - CONNECT request must send authority form URI
571 # - not CONNECT proxy must send absolute form URI
572 # - most common is origin form URI
573 if self.method == hdrs.METH_CONNECT:
574 connect_host = self.url.raw_host
575 assert connect_host is not None
576 if helpers.is_ipv6_address(connect_host):
577 connect_host = f"[{connect_host}]"
578 path = f"{connect_host}:{self.url.port}"
579 elif self.proxy and not self.is_ssl():
580 path = str(self.url)
581 else:
582 path = self.url.raw_path
583 if self.url.raw_query_string:
584 path += "?" + self.url.raw_query_string
586 protocol = conn.protocol
587 assert protocol is not None
588 writer = StreamWriter(
589 protocol,
590 self.loop,
591 on_chunk_sent=functools.partial(
592 self._on_chunk_request_sent, self.method, self.url
593 ),
594 on_headers_sent=functools.partial(
595 self._on_headers_request_sent, self.method, self.url
596 ),
597 )
599 if self.compress:
600 writer.enable_compression(self.compress)
602 if self.chunked is not None:
603 writer.enable_chunking()
605 # set default content-type
606 if (
607 self.method in self.POST_METHODS
608 and hdrs.CONTENT_TYPE not in self.skip_auto_headers
609 and hdrs.CONTENT_TYPE not in self.headers
610 ):
611 self.headers[hdrs.CONTENT_TYPE] = "application/octet-stream"
613 # set the connection header
614 connection = self.headers.get(hdrs.CONNECTION)
615 if not connection:
616 if self.keep_alive():
617 if self.version == HttpVersion10:
618 connection = "keep-alive"
619 else:
620 if self.version == HttpVersion11:
621 connection = "close"
623 if connection is not None:
624 self.headers[hdrs.CONNECTION] = connection
626 # status + headers
627 status_line = "{0} {1} HTTP/{2[0]}.{2[1]}".format(
628 self.method, path, self.version
629 )
630 await writer.write_headers(status_line, self.headers)
632 self._writer = self.loop.create_task(self.write_bytes(writer, conn))
634 response_class = self.response_class
635 assert response_class is not None
636 self.response = response_class(
637 self.method,
638 self.original_url,
639 writer=self._writer,
640 continue100=self._continue,
641 timer=self._timer,
642 request_info=self.request_info,
643 traces=self._traces,
644 loop=self.loop,
645 session=self._session,
646 )
647 return self.response
649 async def close(self) -> None:
650 if self._writer is not None:
651 try:
652 await self._writer
653 finally:
654 self._writer = None
656 def terminate(self) -> None:
657 if self._writer is not None:
658 if not self.loop.is_closed():
659 self._writer.cancel()
660 self._writer = None
662 async def _on_chunk_request_sent(self, method: str, url: URL, chunk: bytes) -> None:
663 for trace in self._traces:
664 await trace.send_request_chunk_sent(method, url, chunk)
666 async def _on_headers_request_sent(
667 self, method: str, url: URL, headers: "CIMultiDict[str]"
668 ) -> None:
669 for trace in self._traces:
670 await trace.send_request_headers(method, url, headers)
673class ClientResponse(HeadersMixin):
674 # Some of these attributes are None when created,
675 # but will be set by the start() method.
676 # As the end user will likely never see the None values, we cheat the types below.
677 # from the Status-Line of the response
678 version = None # HTTP-Version
679 status: int = None # type: ignore[assignment] # Status-Code
680 reason = None # Reason-Phrase
682 content: StreamReader = None # type: ignore[assignment] # Payload stream
683 _headers: CIMultiDictProxy[str] = None # type: ignore[assignment]
684 _raw_headers: RawHeaders = None # type: ignore[assignment]
686 _connection = None # current connection
687 _source_traceback: Optional[traceback.StackSummary] = None
688 # set up by ClientRequest after ClientResponse object creation
689 # post-init stage allows to not change ctor signature
690 _closed = True # to allow __del__ for non-initialized properly response
691 _released = False
693 def __init__(
694 self,
695 method: str,
696 url: URL,
697 *,
698 writer: "asyncio.Task[None]",
699 continue100: Optional["asyncio.Future[bool]"],
700 timer: Optional[BaseTimerContext],
701 request_info: RequestInfo,
702 traces: List["Trace"],
703 loop: asyncio.AbstractEventLoop,
704 session: "ClientSession",
705 ) -> None:
706 assert isinstance(url, URL)
707 super().__init__()
709 self.method = method
710 self.cookies: SimpleCookie[str] = SimpleCookie()
712 self._real_url = url
713 self._url = url.with_fragment(None)
714 self._body: Optional[bytes] = None
715 self._writer: Optional[asyncio.Task[None]] = writer
716 self._continue = continue100 # None by default
717 self._closed = True
718 self._history: Tuple[ClientResponse, ...] = ()
719 self._request_info = request_info
720 self._timer = timer if timer is not None else TimerNoop()
721 self._cache: Dict[str, Any] = {}
722 self._traces = traces
723 self._loop = loop
724 # store a reference to session #1985
725 self._session: Optional[ClientSession] = session
726 # Save reference to _resolve_charset, so that get_encoding() will still
727 # work after the response has finished reading the body.
728 if session is None:
729 # TODO: Fix session=None in tests (see ClientRequest.__init__).
730 self._resolve_charset: Callable[
731 ["ClientResponse", bytes], str
732 ] = lambda *_: "utf-8"
733 else:
734 self._resolve_charset = session._resolve_charset
735 if loop.get_debug():
736 self._source_traceback = traceback.extract_stack(sys._getframe(1))
738 @reify
739 def url(self) -> URL:
740 return self._url
742 @reify
743 def real_url(self) -> URL:
744 return self._real_url
746 @reify
747 def host(self) -> str:
748 assert self._url.host is not None
749 return self._url.host
751 @reify
752 def headers(self) -> "CIMultiDictProxy[str]":
753 return self._headers
755 @reify
756 def raw_headers(self) -> RawHeaders:
757 return self._raw_headers
759 @reify
760 def request_info(self) -> RequestInfo:
761 return self._request_info
763 @reify
764 def content_disposition(self) -> Optional[ContentDisposition]:
765 raw = self._headers.get(hdrs.CONTENT_DISPOSITION)
766 if raw is None:
767 return None
768 disposition_type, params_dct = multipart.parse_content_disposition(raw)
769 params = MappingProxyType(params_dct)
770 filename = multipart.content_disposition_filename(params)
771 return ContentDisposition(disposition_type, params, filename)
773 def __del__(self, _warnings: Any = warnings) -> None:
774 if self._closed:
775 return
777 if self._connection is not None:
778 self._connection.release()
779 self._cleanup_writer()
781 if self._loop.get_debug():
782 _warnings.warn(
783 f"Unclosed response {self!r}", ResourceWarning, source=self
784 )
785 context = {"client_response": self, "message": "Unclosed response"}
786 if self._source_traceback:
787 context["source_traceback"] = self._source_traceback
788 self._loop.call_exception_handler(context)
790 def __repr__(self) -> str:
791 out = io.StringIO()
792 ascii_encodable_url = str(self.url)
793 if self.reason:
794 ascii_encodable_reason = self.reason.encode(
795 "ascii", "backslashreplace"
796 ).decode("ascii")
797 else:
798 ascii_encodable_reason = self.reason
799 print(
800 "<ClientResponse({}) [{} {}]>".format(
801 ascii_encodable_url, self.status, ascii_encodable_reason
802 ),
803 file=out,
804 )
805 print(self.headers, file=out)
806 return out.getvalue()
808 @property
809 def connection(self) -> Optional["Connection"]:
810 return self._connection
812 @reify
813 def history(self) -> Tuple["ClientResponse", ...]:
814 """A sequence of responses, if redirects occurred."""
815 return self._history
817 @reify
818 def links(self) -> "MultiDictProxy[MultiDictProxy[Union[str, URL]]]":
819 links_str = ", ".join(self.headers.getall("link", []))
821 if not links_str:
822 return MultiDictProxy(MultiDict())
824 links: MultiDict[MultiDictProxy[Union[str, URL]]] = MultiDict()
826 for val in re.split(r",(?=\s*<)", links_str):
827 match = re.match(r"\s*<(.*)>(.*)", val)
828 if match is None: # pragma: no cover
829 # the check exists to suppress mypy error
830 continue
831 url, params_str = match.groups()
832 params = params_str.split(";")[1:]
834 link: MultiDict[Union[str, URL]] = MultiDict()
836 for param in params:
837 match = re.match(r"^\s*(\S*)\s*=\s*(['\"]?)(.*?)(\2)\s*$", param, re.M)
838 if match is None: # pragma: no cover
839 # the check exists to suppress mypy error
840 continue
841 key, _, value, _ = match.groups()
843 link.add(key, value)
845 key = link.get("rel", url)
847 link.add("url", self.url.join(URL(url)))
849 links.add(str(key), MultiDictProxy(link))
851 return MultiDictProxy(links)
853 async def start(self, connection: "Connection") -> "ClientResponse":
854 """Start response processing."""
855 self._closed = False
856 self._protocol = connection.protocol
857 self._connection = connection
859 with self._timer:
860 while True:
861 # read response
862 try:
863 protocol = self._protocol
864 message, payload = await protocol.read() # type: ignore[union-attr]
865 except http.HttpProcessingError as exc:
866 raise ClientResponseError(
867 self.request_info,
868 self.history,
869 status=exc.code,
870 message=exc.message,
871 headers=exc.headers,
872 ) from exc
874 if message.code < 100 or message.code > 199 or message.code == 101:
875 break
877 if self._continue is not None:
878 set_result(self._continue, True)
879 self._continue = None
881 # payload eof handler
882 payload.on_eof(self._response_eof)
884 # response status
885 self.version = message.version
886 self.status = message.code
887 self.reason = message.reason
889 # headers
890 self._headers = message.headers # type is CIMultiDictProxy
891 self._raw_headers = message.raw_headers # type is Tuple[bytes, bytes]
893 # payload
894 self.content = payload
896 # cookies
897 for hdr in self.headers.getall(hdrs.SET_COOKIE, ()):
898 try:
899 self.cookies.load(hdr)
900 except CookieError as exc:
901 client_logger.warning("Can not load response cookies: %s", exc)
902 return self
904 def _response_eof(self) -> None:
905 if self._closed:
906 return
908 if self._connection is not None:
909 # websocket, protocol could be None because
910 # connection could be detached
911 if (
912 self._connection.protocol is not None
913 and self._connection.protocol.upgraded
914 ):
915 return
917 self._connection.release()
918 self._connection = None
920 self._closed = True
921 self._cleanup_writer()
923 @property
924 def closed(self) -> bool:
925 return self._closed
927 def close(self) -> None:
928 if not self._released:
929 self._notify_content()
930 if self._closed:
931 return
933 self._closed = True
934 if self._loop.is_closed():
935 return
937 if self._connection is not None:
938 self._connection.close()
939 self._connection = None
940 self._cleanup_writer()
942 def release(self) -> Any:
943 if not self._released:
944 self._notify_content()
945 if self._closed:
946 return noop()
948 self._closed = True
949 if self._connection is not None:
950 self._connection.release()
951 self._connection = None
953 self._cleanup_writer()
954 return noop()
956 @property
957 def ok(self) -> bool:
958 """Returns ``True`` if ``status`` is less than ``400``, ``False`` if not.
960 This is **not** a check for ``200 OK`` but a check that the response
961 status is under 400.
962 """
963 return 400 > self.status
965 def raise_for_status(self) -> None:
966 if not self.ok:
967 # reason should always be not None for a started response
968 assert self.reason is not None
969 self.release()
970 raise ClientResponseError(
971 self.request_info,
972 self.history,
973 status=self.status,
974 message=self.reason,
975 headers=self.headers,
976 )
978 def _cleanup_writer(self) -> None:
979 if self._writer is not None:
980 self._writer.cancel()
981 self._writer = None
982 self._session = None
984 def _notify_content(self) -> None:
985 content = self.content
986 # content can be None here, but the types are cheated elsewhere.
987 if content and content.exception() is None: # type: ignore[truthy-bool]
988 content.set_exception(ClientConnectionError("Connection closed"))
989 self._released = True
991 async def wait_for_close(self) -> None:
992 if self._writer is not None:
993 try:
994 await self._writer
995 finally:
996 self._writer = None
997 self.release()
999 async def read(self) -> bytes:
1000 """Read response payload."""
1001 if self._body is None:
1002 try:
1003 self._body = await self.content.read()
1004 for trace in self._traces:
1005 await trace.send_response_chunk_received(
1006 self.method, self.url, self._body
1007 )
1008 except BaseException:
1009 self.close()
1010 raise
1011 elif self._released:
1012 raise ClientConnectionError("Connection closed")
1014 return self._body
1016 def get_encoding(self) -> str:
1017 ctype = self.headers.get(hdrs.CONTENT_TYPE, "").lower()
1018 mimetype = helpers.parse_mimetype(ctype)
1020 encoding = mimetype.parameters.get("charset")
1021 if encoding:
1022 with contextlib.suppress(LookupError):
1023 return codecs.lookup(encoding).name
1025 if mimetype.type == "application" and (
1026 mimetype.subtype == "json" or mimetype.subtype == "rdap"
1027 ):
1028 # RFC 7159 states that the default encoding is UTF-8.
1029 # RFC 7483 defines application/rdap+json
1030 return "utf-8"
1032 if self._body is None:
1033 raise RuntimeError(
1034 "Cannot compute fallback encoding of a not yet read body"
1035 )
1037 return self._resolve_charset(self, self._body)
1039 async def text(self, encoding: Optional[str] = None, errors: str = "strict") -> str:
1040 """Read response payload and decode."""
1041 if self._body is None:
1042 await self.read()
1044 if encoding is None:
1045 encoding = self.get_encoding()
1047 return self._body.decode(encoding, errors=errors) # type: ignore[union-attr]
1049 async def json(
1050 self,
1051 *,
1052 encoding: Optional[str] = None,
1053 loads: JSONDecoder = DEFAULT_JSON_DECODER,
1054 content_type: Optional[str] = "application/json",
1055 ) -> Any:
1056 """Read and decodes JSON response."""
1057 if self._body is None:
1058 await self.read()
1060 if content_type:
1061 if not is_expected_content_type(self.content_type, content_type):
1062 raise ContentTypeError(
1063 self.request_info,
1064 self.history,
1065 message=(
1066 "Attempt to decode JSON with "
1067 "unexpected mimetype: %s" % self.content_type
1068 ),
1069 headers=self.headers,
1070 )
1072 if encoding is None:
1073 encoding = self.get_encoding()
1075 return loads(self._body.decode(encoding)) # type: ignore[union-attr]
1077 async def __aenter__(self) -> "ClientResponse":
1078 return self
1080 async def __aexit__(
1081 self,
1082 exc_type: Optional[Type[BaseException]],
1083 exc_val: Optional[BaseException],
1084 exc_tb: Optional[TracebackType],
1085 ) -> None:
1086 # similar to _RequestContextManager, we do not need to check
1087 # for exceptions, response object can close connection
1088 # if state is broken
1089 self.release()