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

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) 

29 

30from multidict import CIMultiDict, CIMultiDictProxy, MultiDict, MultiDictProxy 

31from yarl import URL 

32 

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) 

69 

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] 

76 

77 

78__all__ = ("ClientRequest", "ClientResponse", "RequestInfo", "Fingerprint") 

79 

80 

81if TYPE_CHECKING: # pragma: no cover 

82 from .client import ClientSession 

83 from .connector import Connection 

84 from .tracing import Trace 

85 

86 

87_CONTAINS_CONTROL_CHAR_RE = re.compile(r"[^-!#$%&'*+.^_`|~0-9a-zA-Z]") 

88 

89 

90def _gen_default_accept_encoding() -> str: 

91 return "gzip, deflate, br" if HAS_BROTLI else "gzip, deflate" 

92 

93 

94@dataclasses.dataclass(frozen=True) 

95class ContentDisposition: 

96 type: Optional[str] 

97 parameters: "MappingProxyType[str, str]" 

98 filename: Optional[str] 

99 

100 

101@dataclasses.dataclass(frozen=True) 

102class RequestInfo: 

103 url: URL 

104 method: str 

105 headers: "CIMultiDictProxy[str]" 

106 real_url: URL 

107 

108 

109class Fingerprint: 

110 HASHFUNC_BY_DIGESTLEN = { 

111 16: md5, 

112 20: sha1, 

113 32: sha256, 

114 } 

115 

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 

127 

128 @property 

129 def fingerprint(self) -> bytes: 

130 return self._fingerprint 

131 

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) 

141 

142 

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) 

147 

148 

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) 

160 

161 

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}) 

171 

172 DEFAULT_HEADERS = { 

173 hdrs.ACCEPT: "*/*", 

174 hdrs.ACCEPT_ENCODING: _gen_default_accept_encoding(), 

175 } 

176 

177 body = b"" 

178 auth = None 

179 response = None 

180 

181 _writer = None # async task for streaming data 

182 _continue = None # waiter future for '100 Continue' response 

183 

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. 

188 

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 

247 

248 if loop.get_debug(): 

249 self._source_traceback = traceback.extract_stack(sys._getframe(1)) 

250 

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) 

259 

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 

267 

268 def is_ssl(self) -> bool: 

269 return self.url.scheme in ("https", "wss") 

270 

271 @property 

272 def ssl(self) -> Union["SSLContext", None, Literal[False], Fingerprint]: 

273 return self._ssl 

274 

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 ) 

291 

292 @property 

293 def host(self) -> str: 

294 ret = self.url.raw_host 

295 assert ret is not None 

296 return ret 

297 

298 @property 

299 def port(self) -> Optional[int]: 

300 return self.url.port 

301 

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) 

306 

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) 

312 

313 # basic auth info 

314 username, password = url.user, url.password 

315 if username: 

316 self.auth = helpers.BasicAuth(username, password or "") 

317 

318 def update_version(self, version: Union[http.HttpVersion, str]) -> None: 

319 """Convert request version to two elements tuple. 

320 

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 

332 

333 def update_headers(self, headers: Optional[LooseHeaders]) -> None: 

334 """Update request headers.""" 

335 self.headers: CIMultiDict[str] = CIMultiDict() 

336 

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 

346 

347 if headers: 

348 if isinstance(headers, (dict, MultiDictProxy, MultiDict)): 

349 headers = headers.items() # type: ignore[assignment] 

350 

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) 

357 

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] 

364 

365 for hdr, val in self.DEFAULT_HEADERS.items(): 

366 if hdr not in used_headers: 

367 self.headers.add(hdr, val) 

368 

369 if hdrs.USER_AGENT not in used_headers: 

370 self.headers[hdrs.USER_AGENT] = SERVER_SOFTWARE 

371 

372 def update_cookies(self, cookies: Optional[LooseCookies]) -> None: 

373 """Update request cookies header.""" 

374 if not cookies: 

375 return 

376 

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] 

381 

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] 

394 

395 self.headers[hdrs.COOKIE] = c.output(header="", sep=";").strip() 

396 

397 def update_content_encoding(self, data: Any) -> None: 

398 """Set request content encoding.""" 

399 if data is None: 

400 return 

401 

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 

413 

414 def update_transfer_encoding(self) -> None: 

415 """Analyze transfer-encoding header.""" 

416 te = self.headers.get(hdrs.TRANSFER_ENCODING, "").lower() 

417 

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 ) 

424 

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 ) 

430 

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)) 

435 

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 

446 

447 if not isinstance(auth, helpers.BasicAuth): 

448 raise TypeError("BasicAuth() tuple is required instead") 

449 

450 self.headers[hdrs.AUTHORIZATION] = auth.encode() 

451 

452 def update_body_from_data(self, body: Any) -> None: 

453 if body is None: 

454 return 

455 

456 # FormData 

457 if isinstance(body, FormData): 

458 body = body() 

459 

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)() 

469 

470 self.body = body 

471 

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) 

481 

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 

490 

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 

496 

497 if expect: 

498 self._continue = self.loop.create_future() 

499 

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 

511 

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 

523 

524 return True 

525 

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 

534 

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] 

543 

544 for chunk in self.body: 

545 await writer.write(chunk) # type: ignore[arg-type] 

546 

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 

567 

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 

585 

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 ) 

598 

599 if self.compress: 

600 writer.enable_compression(self.compress) 

601 

602 if self.chunked is not None: 

603 writer.enable_chunking() 

604 

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" 

612 

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" 

622 

623 if connection is not None: 

624 self.headers[hdrs.CONNECTION] = connection 

625 

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) 

631 

632 self._writer = self.loop.create_task(self.write_bytes(writer, conn)) 

633 

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 

648 

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 

655 

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 

661 

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) 

665 

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) 

671 

672 

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 

681 

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] 

685 

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 

692 

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__() 

708 

709 self.method = method 

710 self.cookies: SimpleCookie[str] = SimpleCookie() 

711 

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)) 

737 

738 @reify 

739 def url(self) -> URL: 

740 return self._url 

741 

742 @reify 

743 def real_url(self) -> URL: 

744 return self._real_url 

745 

746 @reify 

747 def host(self) -> str: 

748 assert self._url.host is not None 

749 return self._url.host 

750 

751 @reify 

752 def headers(self) -> "CIMultiDictProxy[str]": 

753 return self._headers 

754 

755 @reify 

756 def raw_headers(self) -> RawHeaders: 

757 return self._raw_headers 

758 

759 @reify 

760 def request_info(self) -> RequestInfo: 

761 return self._request_info 

762 

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) 

772 

773 def __del__(self, _warnings: Any = warnings) -> None: 

774 if self._closed: 

775 return 

776 

777 if self._connection is not None: 

778 self._connection.release() 

779 self._cleanup_writer() 

780 

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) 

789 

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() 

807 

808 @property 

809 def connection(self) -> Optional["Connection"]: 

810 return self._connection 

811 

812 @reify 

813 def history(self) -> Tuple["ClientResponse", ...]: 

814 """A sequence of responses, if redirects occurred.""" 

815 return self._history 

816 

817 @reify 

818 def links(self) -> "MultiDictProxy[MultiDictProxy[Union[str, URL]]]": 

819 links_str = ", ".join(self.headers.getall("link", [])) 

820 

821 if not links_str: 

822 return MultiDictProxy(MultiDict()) 

823 

824 links: MultiDict[MultiDictProxy[Union[str, URL]]] = MultiDict() 

825 

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:] 

833 

834 link: MultiDict[Union[str, URL]] = MultiDict() 

835 

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() 

842 

843 link.add(key, value) 

844 

845 key = link.get("rel", url) 

846 

847 link.add("url", self.url.join(URL(url))) 

848 

849 links.add(str(key), MultiDictProxy(link)) 

850 

851 return MultiDictProxy(links) 

852 

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 

858 

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 

873 

874 if message.code < 100 or message.code > 199 or message.code == 101: 

875 break 

876 

877 if self._continue is not None: 

878 set_result(self._continue, True) 

879 self._continue = None 

880 

881 # payload eof handler 

882 payload.on_eof(self._response_eof) 

883 

884 # response status 

885 self.version = message.version 

886 self.status = message.code 

887 self.reason = message.reason 

888 

889 # headers 

890 self._headers = message.headers # type is CIMultiDictProxy 

891 self._raw_headers = message.raw_headers # type is Tuple[bytes, bytes] 

892 

893 # payload 

894 self.content = payload 

895 

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 

903 

904 def _response_eof(self) -> None: 

905 if self._closed: 

906 return 

907 

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 

916 

917 self._connection.release() 

918 self._connection = None 

919 

920 self._closed = True 

921 self._cleanup_writer() 

922 

923 @property 

924 def closed(self) -> bool: 

925 return self._closed 

926 

927 def close(self) -> None: 

928 if not self._released: 

929 self._notify_content() 

930 if self._closed: 

931 return 

932 

933 self._closed = True 

934 if self._loop.is_closed(): 

935 return 

936 

937 if self._connection is not None: 

938 self._connection.close() 

939 self._connection = None 

940 self._cleanup_writer() 

941 

942 def release(self) -> Any: 

943 if not self._released: 

944 self._notify_content() 

945 if self._closed: 

946 return noop() 

947 

948 self._closed = True 

949 if self._connection is not None: 

950 self._connection.release() 

951 self._connection = None 

952 

953 self._cleanup_writer() 

954 return noop() 

955 

956 @property 

957 def ok(self) -> bool: 

958 """Returns ``True`` if ``status`` is less than ``400``, ``False`` if not. 

959 

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 

964 

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 ) 

977 

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 

983 

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 

990 

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() 

998 

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") 

1013 

1014 return self._body 

1015 

1016 def get_encoding(self) -> str: 

1017 ctype = self.headers.get(hdrs.CONTENT_TYPE, "").lower() 

1018 mimetype = helpers.parse_mimetype(ctype) 

1019 

1020 encoding = mimetype.parameters.get("charset") 

1021 if encoding: 

1022 with contextlib.suppress(LookupError): 

1023 return codecs.lookup(encoding).name 

1024 

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" 

1031 

1032 if self._body is None: 

1033 raise RuntimeError( 

1034 "Cannot compute fallback encoding of a not yet read body" 

1035 ) 

1036 

1037 return self._resolve_charset(self, self._body) 

1038 

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() 

1043 

1044 if encoding is None: 

1045 encoding = self.get_encoding() 

1046 

1047 return self._body.decode(encoding, errors=errors) # type: ignore[union-attr] 

1048 

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() 

1059 

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 ) 

1071 

1072 if encoding is None: 

1073 encoding = self.get_encoding() 

1074 

1075 return loads(self._body.decode(encoding)) # type: ignore[union-attr] 

1076 

1077 async def __aenter__(self) -> "ClientResponse": 

1078 return self 

1079 

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()