You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

956 lines
31 KiB

4 years ago
  1. import asyncio
  2. import codecs
  3. import io
  4. import re
  5. import sys
  6. import traceback
  7. import warnings
  8. from hashlib import md5, sha1, sha256
  9. from http.cookies import CookieError, Morsel, SimpleCookie
  10. from types import MappingProxyType
  11. from typing import Any, Optional, Tuple
  12. import attr
  13. from multidict import CIMultiDict, CIMultiDictProxy, MultiDict, MultiDictProxy
  14. from yarl import URL
  15. from . import hdrs, helpers, http, multipart, payload
  16. from .client_exceptions import (ClientConnectionError, ClientOSError,
  17. ClientResponseError, ContentTypeError,
  18. InvalidURL, ServerFingerprintMismatch)
  19. from .formdata import FormData
  20. from .helpers import PY_36, HeadersMixin, TimerNoop, noop, reify, set_result
  21. from .http import SERVER_SOFTWARE, HttpVersion10, HttpVersion11, StreamWriter
  22. from .log import client_logger
  23. from .streams import StreamReader # noqa
  24. from .typedefs import DEFAULT_JSON_DECODER, JSONDecoder, RawHeaders
  25. try:
  26. import ssl
  27. except ImportError: # pragma: no cover
  28. ssl = None # type: ignore
  29. try:
  30. import cchardet as chardet
  31. except ImportError: # pragma: no cover
  32. import chardet
  33. __all__ = ('ClientRequest', 'ClientResponse', 'RequestInfo', 'Fingerprint')
  34. json_re = re.compile(r'^application/(?:[\w.+-]+?\+)?json')
  35. @attr.s(frozen=True, slots=True)
  36. class ContentDisposition:
  37. type = attr.ib(type=str)
  38. parameters = attr.ib(type=MappingProxyType)
  39. filename = attr.ib(type=str)
  40. @attr.s(frozen=True, slots=True)
  41. class RequestInfo:
  42. url = attr.ib(type=URL)
  43. method = attr.ib(type=str)
  44. headers = attr.ib(type=CIMultiDictProxy)
  45. real_url = attr.ib(type=URL)
  46. @real_url.default
  47. def real_url_default(self):
  48. return self.url
  49. class Fingerprint:
  50. HASHFUNC_BY_DIGESTLEN = {
  51. 16: md5,
  52. 20: sha1,
  53. 32: sha256,
  54. }
  55. def __init__(self, fingerprint):
  56. digestlen = len(fingerprint)
  57. hashfunc = self.HASHFUNC_BY_DIGESTLEN.get(digestlen)
  58. if not hashfunc:
  59. raise ValueError('fingerprint has invalid length')
  60. elif hashfunc is md5 or hashfunc is sha1:
  61. raise ValueError('md5 and sha1 are insecure and '
  62. 'not supported. Use sha256.')
  63. self._hashfunc = hashfunc
  64. self._fingerprint = fingerprint
  65. @property
  66. def fingerprint(self):
  67. return self._fingerprint
  68. def check(self, transport):
  69. if not transport.get_extra_info('sslcontext'):
  70. return
  71. sslobj = transport.get_extra_info('ssl_object')
  72. cert = sslobj.getpeercert(binary_form=True)
  73. got = self._hashfunc(cert).digest()
  74. if got != self._fingerprint:
  75. host, port, *_ = transport.get_extra_info('peername')
  76. raise ServerFingerprintMismatch(self._fingerprint,
  77. got, host, port)
  78. if ssl is not None:
  79. SSL_ALLOWED_TYPES = (ssl.SSLContext, bool, Fingerprint, type(None))
  80. else: # pragma: no cover
  81. SSL_ALLOWED_TYPES = type(None)
  82. def _merge_ssl_params(ssl, verify_ssl, ssl_context, fingerprint):
  83. if verify_ssl is not None and not verify_ssl:
  84. warnings.warn("verify_ssl is deprecated, use ssl=False instead",
  85. DeprecationWarning,
  86. stacklevel=3)
  87. if ssl is not None:
  88. raise ValueError("verify_ssl, ssl_context, fingerprint and ssl "
  89. "parameters are mutually exclusive")
  90. else:
  91. ssl = False
  92. if ssl_context is not None:
  93. warnings.warn("ssl_context is deprecated, use ssl=context instead",
  94. DeprecationWarning,
  95. stacklevel=3)
  96. if ssl is not None:
  97. raise ValueError("verify_ssl, ssl_context, fingerprint and ssl "
  98. "parameters are mutually exclusive")
  99. else:
  100. ssl = ssl_context
  101. if fingerprint is not None:
  102. warnings.warn("fingerprint is deprecated, "
  103. "use ssl=Fingerprint(fingerprint) instead",
  104. DeprecationWarning,
  105. stacklevel=3)
  106. if ssl is not None:
  107. raise ValueError("verify_ssl, ssl_context, fingerprint and ssl "
  108. "parameters are mutually exclusive")
  109. else:
  110. ssl = Fingerprint(fingerprint)
  111. if not isinstance(ssl, SSL_ALLOWED_TYPES):
  112. raise TypeError("ssl should be SSLContext, bool, Fingerprint or None, "
  113. "got {!r} instead.".format(ssl))
  114. return ssl
  115. @attr.s(slots=True, frozen=True)
  116. class ConnectionKey:
  117. # the key should contain an information about used proxy / TLS
  118. # to prevent reusing wrong connections from a pool
  119. host = attr.ib(type=str)
  120. port = attr.ib(type=int)
  121. is_ssl = attr.ib(type=bool)
  122. ssl = attr.ib() # SSLContext or None
  123. proxy = attr.ib() # URL or None
  124. proxy_auth = attr.ib() # BasicAuth
  125. proxy_headers_hash = attr.ib(type=int) # hash(CIMultiDict)
  126. def _is_expected_content_type(response_content_type, expected_content_type):
  127. if expected_content_type == 'application/json':
  128. return json_re.match(response_content_type)
  129. return expected_content_type in response_content_type
  130. class ClientRequest:
  131. GET_METHODS = {
  132. hdrs.METH_GET,
  133. hdrs.METH_HEAD,
  134. hdrs.METH_OPTIONS,
  135. hdrs.METH_TRACE,
  136. }
  137. POST_METHODS = {hdrs.METH_PATCH, hdrs.METH_POST, hdrs.METH_PUT}
  138. ALL_METHODS = GET_METHODS.union(POST_METHODS).union({hdrs.METH_DELETE})
  139. DEFAULT_HEADERS = {
  140. hdrs.ACCEPT: '*/*',
  141. hdrs.ACCEPT_ENCODING: 'gzip, deflate',
  142. }
  143. body = b''
  144. auth = None
  145. response = None
  146. response_class = None
  147. _writer = None # async task for streaming data
  148. _continue = None # waiter future for '100 Continue' response
  149. # N.B.
  150. # Adding __del__ method with self._writer closing doesn't make sense
  151. # because _writer is instance method, thus it keeps a reference to self.
  152. # Until writer has finished finalizer will not be called.
  153. def __init__(self, method, url, *,
  154. params=None, headers=None, skip_auto_headers=frozenset(),
  155. data=None, cookies=None,
  156. auth=None, version=http.HttpVersion11, compress=None,
  157. chunked=None, expect100=False,
  158. loop=None, response_class=None,
  159. proxy=None, proxy_auth=None,
  160. timer=None, session=None,
  161. ssl=None,
  162. proxy_headers=None,
  163. traces=None):
  164. if loop is None:
  165. loop = asyncio.get_event_loop()
  166. assert isinstance(url, URL), url
  167. assert isinstance(proxy, (URL, type(None))), proxy
  168. self._session = session
  169. if params:
  170. q = MultiDict(url.query)
  171. url2 = url.with_query(params)
  172. q.extend(url2.query)
  173. url = url.with_query(q)
  174. self.original_url = url
  175. self.url = url.with_fragment(None)
  176. self.method = method.upper()
  177. self.chunked = chunked
  178. self.compress = compress
  179. self.loop = loop
  180. self.length = None
  181. self.response_class = response_class or ClientResponse
  182. self._timer = timer if timer is not None else TimerNoop()
  183. self._ssl = ssl
  184. if loop.get_debug():
  185. self._source_traceback = traceback.extract_stack(sys._getframe(1))
  186. self.update_version(version)
  187. self.update_host(url)
  188. self.update_headers(headers)
  189. self.update_auto_headers(skip_auto_headers)
  190. self.update_cookies(cookies)
  191. self.update_content_encoding(data)
  192. self.update_auth(auth)
  193. self.update_proxy(proxy, proxy_auth, proxy_headers)
  194. self.update_body_from_data(data)
  195. if data or self.method not in self.GET_METHODS:
  196. self.update_transfer_encoding()
  197. self.update_expect_continue(expect100)
  198. if traces is None:
  199. traces = []
  200. self._traces = traces
  201. def is_ssl(self):
  202. return self.url.scheme in ('https', 'wss')
  203. @property
  204. def ssl(self):
  205. return self._ssl
  206. @property
  207. def connection_key(self):
  208. proxy_headers = self.proxy_headers
  209. if proxy_headers:
  210. h = hash(tuple((k, v) for k, v in proxy_headers.items()))
  211. else:
  212. h = None
  213. return ConnectionKey(self.host, self.port, self.is_ssl(),
  214. self.ssl,
  215. self.proxy, self.proxy_auth, h)
  216. @property
  217. def host(self):
  218. return self.url.host
  219. @property
  220. def port(self):
  221. return self.url.port
  222. @property
  223. def request_info(self):
  224. return RequestInfo(self.url, self.method,
  225. self.headers, self.original_url)
  226. def update_host(self, url):
  227. """Update destination host, port and connection type (ssl)."""
  228. # get host/port
  229. if not url.host:
  230. raise InvalidURL(url)
  231. # basic auth info
  232. username, password = url.user, url.password
  233. if username:
  234. self.auth = helpers.BasicAuth(username, password or '')
  235. def update_version(self, version):
  236. """Convert request version to two elements tuple.
  237. parser HTTP version '1.1' => (1, 1)
  238. """
  239. if isinstance(version, str):
  240. v = [l.strip() for l in version.split('.', 1)]
  241. try:
  242. version = int(v[0]), int(v[1])
  243. except ValueError:
  244. raise ValueError(
  245. 'Can not parse http version number: {}'
  246. .format(version)) from None
  247. self.version = version
  248. def update_headers(self, headers):
  249. """Update request headers."""
  250. self.headers = CIMultiDict()
  251. if headers:
  252. if isinstance(headers, (dict, MultiDictProxy, MultiDict)):
  253. headers = headers.items()
  254. for key, value in headers:
  255. self.headers.add(key, value)
  256. def update_auto_headers(self, skip_auto_headers):
  257. self.skip_auto_headers = CIMultiDict(
  258. (hdr, None) for hdr in sorted(skip_auto_headers))
  259. used_headers = self.headers.copy()
  260. used_headers.extend(self.skip_auto_headers)
  261. for hdr, val in self.DEFAULT_HEADERS.items():
  262. if hdr not in used_headers:
  263. self.headers.add(hdr, val)
  264. # add host
  265. if hdrs.HOST not in used_headers:
  266. netloc = self.url.raw_host
  267. if not self.url.is_default_port():
  268. netloc += ':' + str(self.url.port)
  269. self.headers[hdrs.HOST] = netloc
  270. if hdrs.USER_AGENT not in used_headers:
  271. self.headers[hdrs.USER_AGENT] = SERVER_SOFTWARE
  272. def update_cookies(self, cookies):
  273. """Update request cookies header."""
  274. if not cookies:
  275. return
  276. c = SimpleCookie()
  277. if hdrs.COOKIE in self.headers:
  278. c.load(self.headers.get(hdrs.COOKIE, ''))
  279. del self.headers[hdrs.COOKIE]
  280. for name, value in cookies.items():
  281. if isinstance(value, Morsel):
  282. # Preserve coded_value
  283. mrsl_val = value.get(value.key, Morsel())
  284. mrsl_val.set(value.key, value.value, value.coded_value)
  285. c[name] = mrsl_val
  286. else:
  287. c[name] = value
  288. self.headers[hdrs.COOKIE] = c.output(header='', sep=';').strip()
  289. def update_content_encoding(self, data):
  290. """Set request content encoding."""
  291. if not data:
  292. return
  293. enc = self.headers.get(hdrs.CONTENT_ENCODING, '').lower()
  294. if enc:
  295. if self.compress:
  296. raise ValueError(
  297. 'compress can not be set '
  298. 'if Content-Encoding header is set')
  299. elif self.compress:
  300. if not isinstance(self.compress, str):
  301. self.compress = 'deflate'
  302. self.headers[hdrs.CONTENT_ENCODING] = self.compress
  303. self.chunked = True # enable chunked, no need to deal with length
  304. def update_transfer_encoding(self):
  305. """Analyze transfer-encoding header."""
  306. te = self.headers.get(hdrs.TRANSFER_ENCODING, '').lower()
  307. if 'chunked' in te:
  308. if self.chunked:
  309. raise ValueError(
  310. 'chunked can not be set '
  311. 'if "Transfer-Encoding: chunked" header is set')
  312. elif self.chunked:
  313. if hdrs.CONTENT_LENGTH in self.headers:
  314. raise ValueError(
  315. 'chunked can not be set '
  316. 'if Content-Length header is set')
  317. self.headers[hdrs.TRANSFER_ENCODING] = 'chunked'
  318. else:
  319. if hdrs.CONTENT_LENGTH not in self.headers:
  320. self.headers[hdrs.CONTENT_LENGTH] = str(len(self.body))
  321. def update_auth(self, auth):
  322. """Set basic auth."""
  323. if auth is None:
  324. auth = self.auth
  325. if auth is None:
  326. return
  327. if not isinstance(auth, helpers.BasicAuth):
  328. raise TypeError('BasicAuth() tuple is required instead')
  329. self.headers[hdrs.AUTHORIZATION] = auth.encode()
  330. def update_body_from_data(self, body):
  331. if not body:
  332. return
  333. # FormData
  334. if isinstance(body, FormData):
  335. body = body()
  336. try:
  337. body = payload.PAYLOAD_REGISTRY.get(body, disposition=None)
  338. except payload.LookupError:
  339. body = FormData(body)()
  340. self.body = body
  341. # enable chunked encoding if needed
  342. if not self.chunked:
  343. if hdrs.CONTENT_LENGTH not in self.headers:
  344. size = body.size
  345. if size is None:
  346. self.chunked = True
  347. else:
  348. if hdrs.CONTENT_LENGTH not in self.headers:
  349. self.headers[hdrs.CONTENT_LENGTH] = str(size)
  350. # set content-type
  351. if (hdrs.CONTENT_TYPE not in self.headers and
  352. hdrs.CONTENT_TYPE not in self.skip_auto_headers):
  353. self.headers[hdrs.CONTENT_TYPE] = body.content_type
  354. # copy payload headers
  355. if body.headers:
  356. for (key, value) in body.headers.items():
  357. if key not in self.headers:
  358. self.headers[key] = value
  359. def update_expect_continue(self, expect=False):
  360. if expect:
  361. self.headers[hdrs.EXPECT] = '100-continue'
  362. elif self.headers.get(hdrs.EXPECT, '').lower() == '100-continue':
  363. expect = True
  364. if expect:
  365. self._continue = self.loop.create_future()
  366. def update_proxy(self, proxy, proxy_auth, proxy_headers):
  367. if proxy and not proxy.scheme == 'http':
  368. raise ValueError("Only http proxies are supported")
  369. if proxy_auth and not isinstance(proxy_auth, helpers.BasicAuth):
  370. raise ValueError("proxy_auth must be None or BasicAuth() tuple")
  371. self.proxy = proxy
  372. self.proxy_auth = proxy_auth
  373. self.proxy_headers = proxy_headers
  374. def keep_alive(self):
  375. if self.version < HttpVersion10:
  376. # keep alive not supported at all
  377. return False
  378. if self.version == HttpVersion10:
  379. if self.headers.get(hdrs.CONNECTION) == 'keep-alive':
  380. return True
  381. else: # no headers means we close for Http 1.0
  382. return False
  383. elif self.headers.get(hdrs.CONNECTION) == 'close':
  384. return False
  385. return True
  386. async def write_bytes(self, writer, conn):
  387. """Support coroutines that yields bytes objects."""
  388. # 100 response
  389. if self._continue is not None:
  390. await writer.drain()
  391. await self._continue
  392. try:
  393. if isinstance(self.body, payload.Payload):
  394. await self.body.write(writer)
  395. else:
  396. if isinstance(self.body, (bytes, bytearray)):
  397. self.body = (self.body,)
  398. for chunk in self.body:
  399. await writer.write(chunk)
  400. await writer.write_eof()
  401. except OSError as exc:
  402. new_exc = ClientOSError(
  403. exc.errno,
  404. 'Can not write request body for %s' % self.url)
  405. new_exc.__context__ = exc
  406. new_exc.__cause__ = exc
  407. conn.protocol.set_exception(new_exc)
  408. except asyncio.CancelledError as exc:
  409. if not conn.closed:
  410. conn.protocol.set_exception(exc)
  411. except Exception as exc:
  412. conn.protocol.set_exception(exc)
  413. finally:
  414. self._writer = None
  415. async def send(self, conn):
  416. # Specify request target:
  417. # - CONNECT request must send authority form URI
  418. # - not CONNECT proxy must send absolute form URI
  419. # - most common is origin form URI
  420. if self.method == hdrs.METH_CONNECT:
  421. path = '{}:{}'.format(self.url.raw_host, self.url.port)
  422. elif self.proxy and not self.is_ssl():
  423. path = str(self.url)
  424. else:
  425. path = self.url.raw_path
  426. if self.url.raw_query_string:
  427. path += '?' + self.url.raw_query_string
  428. writer = StreamWriter(
  429. conn.protocol, self.loop,
  430. on_chunk_sent=self._on_chunk_request_sent
  431. )
  432. if self.compress:
  433. writer.enable_compression(self.compress)
  434. if self.chunked is not None:
  435. writer.enable_chunking()
  436. # set default content-type
  437. if (self.method in self.POST_METHODS and
  438. hdrs.CONTENT_TYPE not in self.skip_auto_headers and
  439. hdrs.CONTENT_TYPE not in self.headers):
  440. self.headers[hdrs.CONTENT_TYPE] = 'application/octet-stream'
  441. # set the connection header
  442. connection = self.headers.get(hdrs.CONNECTION)
  443. if not connection:
  444. if self.keep_alive():
  445. if self.version == HttpVersion10:
  446. connection = 'keep-alive'
  447. else:
  448. if self.version == HttpVersion11:
  449. connection = 'close'
  450. if connection is not None:
  451. self.headers[hdrs.CONNECTION] = connection
  452. # status + headers
  453. status_line = '{0} {1} HTTP/{2[0]}.{2[1]}'.format(
  454. self.method, path, self.version)
  455. await writer.write_headers(status_line, self.headers)
  456. self._writer = self.loop.create_task(self.write_bytes(writer, conn))
  457. self.response = self.response_class(
  458. self.method, self.original_url,
  459. writer=self._writer, continue100=self._continue, timer=self._timer,
  460. request_info=self.request_info,
  461. traces=self._traces,
  462. loop=self.loop,
  463. session=self._session
  464. )
  465. return self.response
  466. async def close(self):
  467. if self._writer is not None:
  468. try:
  469. await self._writer
  470. finally:
  471. self._writer = None
  472. def terminate(self):
  473. if self._writer is not None:
  474. if not self.loop.is_closed():
  475. self._writer.cancel()
  476. self._writer = None
  477. async def _on_chunk_request_sent(self, chunk):
  478. for trace in self._traces:
  479. await trace.send_request_chunk_sent(chunk)
  480. class ClientResponse(HeadersMixin):
  481. # from the Status-Line of the response
  482. version = None # HTTP-Version
  483. status = None # type: int # Status-Code
  484. reason = None # Reason-Phrase
  485. content = None # type: StreamReader # Payload stream
  486. _headers = None # type: CIMultiDictProxy # Response headers
  487. _raw_headers = None # type: RawHeaders # Response raw headers
  488. _connection = None # current connection
  489. _source_traceback = None
  490. # setted up by ClientRequest after ClientResponse object creation
  491. # post-init stage allows to not change ctor signature
  492. _closed = True # to allow __del__ for non-initialized properly response
  493. _released = False
  494. def __init__(self, method, url, *,
  495. writer, continue100, timer,
  496. request_info,
  497. traces, loop, session):
  498. assert isinstance(url, URL)
  499. self.method = method
  500. self.cookies = SimpleCookie()
  501. self._real_url = url
  502. self._url = url.with_fragment(None)
  503. self._body = None # type: bytes
  504. self._writer = writer
  505. self._continue = continue100 # None by default
  506. self._closed = True
  507. self._history = ()
  508. self._request_info = request_info
  509. self._timer = timer if timer is not None else TimerNoop()
  510. self._cache = {} # required for @reify method decorator
  511. self._traces = traces
  512. self._loop = loop
  513. self._session = session # store a reference to session #1985
  514. if loop.get_debug():
  515. self._source_traceback = traceback.extract_stack(sys._getframe(1))
  516. @reify
  517. def url(self) -> URL:
  518. return self._url
  519. @reify
  520. def url_obj(self) -> URL:
  521. warnings.warn(
  522. "Deprecated, use .url #1654", DeprecationWarning, stacklevel=2)
  523. return self._url
  524. @reify
  525. def real_url(self) -> URL:
  526. return self._real_url
  527. @reify
  528. def host(self) -> str:
  529. return self._url.host
  530. @reify
  531. def headers(self) -> CIMultiDictProxy:
  532. return self._headers
  533. @reify
  534. def raw_headers(self) -> RawHeaders:
  535. return self._raw_headers
  536. @reify
  537. def request_info(self) -> RequestInfo:
  538. return self._request_info
  539. @reify
  540. def content_disposition(self) -> Optional[ContentDisposition]:
  541. raw = self._headers.get(hdrs.CONTENT_DISPOSITION)
  542. if raw is None:
  543. return None
  544. disposition_type, params = multipart.parse_content_disposition(raw)
  545. params = MappingProxyType(params)
  546. filename = multipart.content_disposition_filename(params)
  547. return ContentDisposition(disposition_type, params, filename)
  548. def __del__(self, _warnings=warnings):
  549. if self._closed:
  550. return
  551. if self._connection is not None:
  552. self._connection.release()
  553. self._cleanup_writer()
  554. if self._loop.get_debug():
  555. if PY_36:
  556. kwargs = {'source': self}
  557. else:
  558. kwargs = {}
  559. _warnings.warn("Unclosed response {!r}".format(self),
  560. ResourceWarning,
  561. **kwargs)
  562. context = {'client_response': self,
  563. 'message': 'Unclosed response'}
  564. if self._source_traceback:
  565. context['source_traceback'] = self._source_traceback
  566. self._loop.call_exception_handler(context)
  567. def __repr__(self):
  568. out = io.StringIO()
  569. ascii_encodable_url = str(self.url)
  570. if self.reason:
  571. ascii_encodable_reason = self.reason.encode('ascii',
  572. 'backslashreplace') \
  573. .decode('ascii')
  574. else:
  575. ascii_encodable_reason = self.reason
  576. print('<ClientResponse({}) [{} {}]>'.format(
  577. ascii_encodable_url, self.status, ascii_encodable_reason),
  578. file=out)
  579. print(self.headers, file=out)
  580. return out.getvalue()
  581. @property
  582. def connection(self):
  583. return self._connection
  584. @reify
  585. def history(self) -> Tuple['ClientResponse', ...]:
  586. """A sequence of of responses, if redirects occurred."""
  587. return self._history
  588. @reify
  589. def links(self) -> MultiDictProxy:
  590. links_str = ", ".join(self.headers.getall("link", []))
  591. links = MultiDict() # type: MultiDict
  592. if not links_str:
  593. return MultiDictProxy(links)
  594. for val in re.split(r",(?=\s*<)", links_str):
  595. match = re.match(r"\s*<(.*)>(.*)", val)
  596. if match is None: # pragma: no cover
  597. # the check exists to suppress mypy error
  598. continue
  599. url, params_str = match.groups()
  600. params = params_str.split(";")[1:]
  601. link = MultiDict() # type: MultiDict
  602. for param in params:
  603. match = re.match(
  604. r"^\s*(\S*)\s*=\s*(['\"]?)(.*?)(\2)\s*$",
  605. param, re.M
  606. )
  607. if match is None: # pragma: no cover
  608. # the check exists to suppress mypy error
  609. continue
  610. key, _, value, _ = match.groups()
  611. link.add(key, value)
  612. key = link.get("rel", url)
  613. link.add("url", self.url.join(URL(url)))
  614. links.add(key, MultiDictProxy(link))
  615. return MultiDictProxy(links)
  616. async def start(self, connection):
  617. """Start response processing."""
  618. self._closed = False
  619. self._protocol = connection.protocol
  620. self._connection = connection
  621. with self._timer:
  622. while True:
  623. # read response
  624. try:
  625. message, payload = await self._protocol.read()
  626. except http.HttpProcessingError as exc:
  627. raise ClientResponseError(
  628. self.request_info, self.history,
  629. status=exc.code,
  630. message=exc.message, headers=exc.headers) from exc
  631. if (message.code < 100 or
  632. message.code > 199 or message.code == 101):
  633. break
  634. if self._continue is not None:
  635. set_result(self._continue, True)
  636. self._continue = None
  637. # payload eof handler
  638. payload.on_eof(self._response_eof)
  639. # response status
  640. self.version = message.version
  641. self.status = message.code
  642. self.reason = message.reason
  643. # headers
  644. self._headers = message.headers # type is CIMultiDictProxy
  645. self._raw_headers = message.raw_headers # type is Tuple[bytes, bytes]
  646. # payload
  647. self.content = payload
  648. # cookies
  649. for hdr in self.headers.getall(hdrs.SET_COOKIE, ()):
  650. try:
  651. self.cookies.load(hdr)
  652. except CookieError as exc:
  653. client_logger.warning(
  654. 'Can not load response cookies: %s', exc)
  655. return self
  656. def _response_eof(self):
  657. if self._closed:
  658. return
  659. if self._connection is not None:
  660. # websocket, protocol could be None because
  661. # connection could be detached
  662. if (self._connection.protocol is not None and
  663. self._connection.protocol.upgraded):
  664. return
  665. self._connection.release()
  666. self._connection = None
  667. self._closed = True
  668. self._cleanup_writer()
  669. @property
  670. def closed(self) -> bool:
  671. return self._closed
  672. def close(self) -> None:
  673. if not self._released:
  674. self._notify_content()
  675. if self._closed:
  676. return
  677. self._closed = True
  678. if self._loop is None or self._loop.is_closed():
  679. return
  680. if self._connection is not None:
  681. self._connection.close()
  682. self._connection = None
  683. self._cleanup_writer()
  684. def release(self) -> Any:
  685. if not self._released:
  686. self._notify_content()
  687. if self._closed:
  688. return noop()
  689. self._closed = True
  690. if self._connection is not None:
  691. self._connection.release()
  692. self._connection = None
  693. self._cleanup_writer()
  694. return noop()
  695. def raise_for_status(self) -> None:
  696. if 400 <= self.status:
  697. raise ClientResponseError(
  698. self.request_info,
  699. self.history,
  700. status=self.status,
  701. message=self.reason,
  702. headers=self.headers)
  703. def _cleanup_writer(self):
  704. if self._writer is not None:
  705. self._writer.cancel()
  706. self._writer = None
  707. self._session = None
  708. def _notify_content(self):
  709. content = self.content
  710. if content and content.exception() is None:
  711. content.set_exception(
  712. ClientConnectionError('Connection closed'))
  713. self._released = True
  714. async def wait_for_close(self) -> None:
  715. if self._writer is not None:
  716. try:
  717. await self._writer
  718. finally:
  719. self._writer = None
  720. self.release()
  721. async def read(self) -> bytes:
  722. """Read response payload."""
  723. if self._body is None:
  724. try:
  725. self._body = await self.content.read()
  726. for trace in self._traces:
  727. await trace.send_response_chunk_received(self._body)
  728. except BaseException:
  729. self.close()
  730. raise
  731. elif self._released:
  732. raise ClientConnectionError('Connection closed')
  733. return self._body
  734. def get_encoding(self) -> str:
  735. ctype = self.headers.get(hdrs.CONTENT_TYPE, '').lower()
  736. mimetype = helpers.parse_mimetype(ctype)
  737. encoding = mimetype.parameters.get('charset')
  738. if encoding:
  739. try:
  740. codecs.lookup(encoding)
  741. except LookupError:
  742. encoding = None
  743. if not encoding:
  744. if mimetype.type == 'application' and mimetype.subtype == 'json':
  745. # RFC 7159 states that the default encoding is UTF-8.
  746. encoding = 'utf-8'
  747. else:
  748. encoding = chardet.detect(self._body)['encoding']
  749. if not encoding:
  750. encoding = 'utf-8'
  751. return encoding
  752. async def text(self,
  753. encoding: Optional[str]=None, errors: str='strict') -> str:
  754. """Read response payload and decode."""
  755. if self._body is None:
  756. await self.read()
  757. if encoding is None:
  758. encoding = self.get_encoding()
  759. return self._body.decode(encoding, errors=errors)
  760. async def json(self, *, encoding: str=None,
  761. loads: JSONDecoder=DEFAULT_JSON_DECODER,
  762. content_type: Optional[str]='application/json') -> Any:
  763. """Read and decodes JSON response."""
  764. if self._body is None:
  765. await self.read()
  766. if content_type:
  767. ctype = self.headers.get(hdrs.CONTENT_TYPE, '').lower()
  768. if not _is_expected_content_type(ctype, content_type):
  769. raise ContentTypeError(
  770. self.request_info,
  771. self.history,
  772. message=('Attempt to decode JSON with '
  773. 'unexpected mimetype: %s' % ctype),
  774. headers=self.headers)
  775. stripped = self._body.strip()
  776. if not stripped:
  777. return None
  778. if encoding is None:
  779. encoding = self.get_encoding()
  780. return loads(stripped.decode(encoding))
  781. async def __aenter__(self) -> 'ClientResponse':
  782. return self
  783. async def __aexit__(self, exc_type, exc_val, exc_tb):
  784. # similar to _RequestContextManager, we do not need to check
  785. # for exceptions, response object can closes connection
  786. # is state is broken
  787. self.release()