Coverage for /pythoncovmergedfiles/medio/medio/src/aiohttp/aiohttp/web_request.py: 41%

450 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-07 06:52 +0000

1import asyncio 

2import dataclasses 

3import datetime 

4import io 

5import re 

6import socket 

7import string 

8import tempfile 

9import types 

10from http.cookies import SimpleCookie 

11from types import MappingProxyType 

12from typing import ( 

13 TYPE_CHECKING, 

14 Any, 

15 Dict, 

16 Iterator, 

17 Mapping, 

18 MutableMapping, 

19 Optional, 

20 Pattern, 

21 Set, 

22 Tuple, 

23 Union, 

24 cast, 

25) 

26from urllib.parse import parse_qsl 

27 

28from multidict import CIMultiDict, CIMultiDictProxy, MultiDict, MultiDictProxy 

29from typing_extensions import Final 

30from yarl import URL 

31 

32from . import hdrs 

33from .abc import AbstractStreamWriter 

34from .helpers import ( 

35 _SENTINEL, 

36 ETAG_ANY, 

37 LIST_QUOTED_ETAG_RE, 

38 ChainMapProxy, 

39 ETag, 

40 HeadersMixin, 

41 is_expected_content_type, 

42 parse_http_date, 

43 reify, 

44 sentinel, 

45 set_result, 

46) 

47from .http_parser import RawRequestMessage 

48from .http_writer import HttpVersion 

49from .multipart import BodyPartReader, MultipartReader 

50from .streams import EmptyStreamReader, StreamReader 

51from .typedefs import ( 

52 DEFAULT_JSON_DECODER, 

53 JSONDecoder, 

54 LooseHeaders, 

55 RawHeaders, 

56 StrOrURL, 

57) 

58from .web_exceptions import ( 

59 HTTPBadRequest, 

60 HTTPRequestEntityTooLarge, 

61 HTTPUnsupportedMediaType, 

62) 

63from .web_response import StreamResponse 

64 

65__all__ = ("BaseRequest", "FileField", "Request") 

66 

67 

68if TYPE_CHECKING: # pragma: no cover 

69 from .web_app import Application 

70 from .web_protocol import RequestHandler 

71 from .web_urldispatcher import UrlMappingMatchInfo 

72 

73 

74@dataclasses.dataclass(frozen=True) 

75class FileField: 

76 name: str 

77 filename: str 

78 file: io.BufferedReader 

79 content_type: str 

80 headers: "CIMultiDictProxy[str]" 

81 

82 

83_TCHAR: Final[str] = string.digits + string.ascii_letters + r"!#$%&'*+.^_`|~-" 

84# '-' at the end to prevent interpretation as range in a char class 

85 

86_TOKEN: Final[str] = rf"[{_TCHAR}]+" 

87 

88_QDTEXT: Final[str] = r"[{}]".format( 

89 r"".join(chr(c) for c in (0x09, 0x20, 0x21) + tuple(range(0x23, 0x7F))) 

90) 

91# qdtext includes 0x5C to escape 0x5D ('\]') 

92# qdtext excludes obs-text (because obsoleted, and encoding not specified) 

93 

94_QUOTED_PAIR: Final[str] = r"\\[\t !-~]" 

95 

96_QUOTED_STRING: Final[str] = r'"(?:{quoted_pair}|{qdtext})*"'.format( 

97 qdtext=_QDTEXT, quoted_pair=_QUOTED_PAIR 

98) 

99 

100_FORWARDED_PAIR: Final[ 

101 str 

102] = r"({token})=({token}|{quoted_string})(:\d{{1,4}})?".format( 

103 token=_TOKEN, quoted_string=_QUOTED_STRING 

104) 

105 

106_QUOTED_PAIR_REPLACE_RE: Final[Pattern[str]] = re.compile(r"\\([\t !-~])") 

107# same pattern as _QUOTED_PAIR but contains a capture group 

108 

109_FORWARDED_PAIR_RE: Final[Pattern[str]] = re.compile(_FORWARDED_PAIR) 

110 

111############################################################ 

112# HTTP Request 

113############################################################ 

114 

115 

116class BaseRequest(MutableMapping[str, Any], HeadersMixin): 

117 POST_METHODS = { 

118 hdrs.METH_PATCH, 

119 hdrs.METH_POST, 

120 hdrs.METH_PUT, 

121 hdrs.METH_TRACE, 

122 hdrs.METH_DELETE, 

123 } 

124 

125 __slots__ = ( 

126 "_message", 

127 "_protocol", 

128 "_payload_writer", 

129 "_payload", 

130 "_headers", 

131 "_method", 

132 "_version", 

133 "_rel_url", 

134 "_post", 

135 "_read_bytes", 

136 "_state", 

137 "_cache", 

138 "_task", 

139 "_client_max_size", 

140 "_loop", 

141 "_transport_sslcontext", 

142 "_transport_peername", 

143 "_disconnection_waiters", 

144 "__weakref__", 

145 ) 

146 

147 def __init__( 

148 self, 

149 message: RawRequestMessage, 

150 payload: StreamReader, 

151 protocol: "RequestHandler", 

152 payload_writer: AbstractStreamWriter, 

153 task: "asyncio.Task[None]", 

154 loop: asyncio.AbstractEventLoop, 

155 *, 

156 client_max_size: int = 1024**2, 

157 state: Optional[Dict[str, Any]] = None, 

158 scheme: Optional[str] = None, 

159 host: Optional[str] = None, 

160 remote: Optional[str] = None, 

161 ) -> None: 

162 super().__init__() 

163 if state is None: 

164 state = {} 

165 self._message = message 

166 self._protocol = protocol 

167 self._payload_writer = payload_writer 

168 

169 self._payload = payload 

170 self._headers = message.headers 

171 self._method = message.method 

172 self._version = message.version 

173 self._cache: Dict[str, Any] = {} 

174 url = message.url 

175 if url.is_absolute(): 

176 # absolute URL is given, 

177 # override auto-calculating url, host, and scheme 

178 # all other properties should be good 

179 self._cache["url"] = url 

180 self._cache["host"] = url.host 

181 self._cache["scheme"] = url.scheme 

182 self._rel_url = url.relative() 

183 else: 

184 self._rel_url = message.url 

185 self._post: Optional[MultiDictProxy[Union[str, bytes, FileField]]] = None 

186 self._read_bytes: Optional[bytes] = None 

187 

188 self._state = state 

189 self._task = task 

190 self._client_max_size = client_max_size 

191 self._loop = loop 

192 self._disconnection_waiters: Set[asyncio.Future[None]] = set() 

193 

194 transport = self._protocol.transport 

195 assert transport is not None 

196 self._transport_sslcontext = transport.get_extra_info("sslcontext") 

197 self._transport_peername = transport.get_extra_info("peername") 

198 

199 if scheme is not None: 

200 self._cache["scheme"] = scheme 

201 if host is not None: 

202 self._cache["host"] = host 

203 if remote is not None: 

204 self._cache["remote"] = remote 

205 

206 def clone( 

207 self, 

208 *, 

209 method: Union[str, _SENTINEL] = sentinel, 

210 rel_url: Union[StrOrURL, _SENTINEL] = sentinel, 

211 headers: Union[LooseHeaders, _SENTINEL] = sentinel, 

212 scheme: Union[str, _SENTINEL] = sentinel, 

213 host: Union[str, _SENTINEL] = sentinel, 

214 remote: Union[str, _SENTINEL] = sentinel, 

215 client_max_size: Union[int, _SENTINEL] = sentinel, 

216 ) -> "BaseRequest": 

217 """Clone itself with replacement some attributes. 

218 

219 Creates and returns a new instance of Request object. If no parameters 

220 are given, an exact copy is returned. If a parameter is not passed, it 

221 will reuse the one from the current request object. 

222 """ 

223 if self._read_bytes: 

224 raise RuntimeError("Cannot clone request " "after reading its content") 

225 

226 dct: Dict[str, Any] = {} 

227 if method is not sentinel: 

228 dct["method"] = method 

229 if rel_url is not sentinel: 

230 new_url: URL = URL(rel_url) 

231 dct["url"] = new_url 

232 dct["path"] = str(new_url) 

233 if headers is not sentinel: 

234 # a copy semantic 

235 new_headers = CIMultiDictProxy(CIMultiDict(headers)) 

236 dct["headers"] = new_headers 

237 dct["raw_headers"] = tuple( 

238 (k.encode("utf-8"), v.encode("utf-8")) for k, v in new_headers.items() 

239 ) 

240 

241 message = self._message._replace(**dct) 

242 

243 kwargs: Dict[str, str] = {} 

244 if scheme is not sentinel: 

245 kwargs["scheme"] = scheme 

246 if host is not sentinel: 

247 kwargs["host"] = host 

248 if remote is not sentinel: 

249 kwargs["remote"] = remote 

250 if client_max_size is sentinel: 

251 client_max_size = self._client_max_size 

252 

253 return self.__class__( 

254 message, 

255 self._payload, 

256 self._protocol, 

257 self._payload_writer, 

258 self._task, 

259 self._loop, 

260 client_max_size=client_max_size, 

261 state=self._state.copy(), 

262 **kwargs, 

263 ) 

264 

265 @property 

266 def task(self) -> "asyncio.Task[None]": 

267 return self._task 

268 

269 @property 

270 def protocol(self) -> "RequestHandler": 

271 return self._protocol 

272 

273 @property 

274 def transport(self) -> Optional[asyncio.Transport]: 

275 if self._protocol is None: 

276 return None 

277 return self._protocol.transport 

278 

279 @property 

280 def writer(self) -> AbstractStreamWriter: 

281 return self._payload_writer 

282 

283 @property 

284 def client_max_size(self) -> int: 

285 return self._client_max_size 

286 

287 @reify 

288 def rel_url(self) -> URL: 

289 return self._rel_url 

290 

291 # MutableMapping API 

292 

293 def __getitem__(self, key: str) -> Any: 

294 return self._state[key] 

295 

296 def __setitem__(self, key: str, value: Any) -> None: 

297 self._state[key] = value 

298 

299 def __delitem__(self, key: str) -> None: 

300 del self._state[key] 

301 

302 def __len__(self) -> int: 

303 return len(self._state) 

304 

305 def __iter__(self) -> Iterator[str]: 

306 return iter(self._state) 

307 

308 ######## 

309 

310 @reify 

311 def secure(self) -> bool: 

312 """A bool indicating if the request is handled with SSL.""" 

313 return self.scheme == "https" 

314 

315 @reify 

316 def forwarded(self) -> Tuple[Mapping[str, str], ...]: 

317 """A tuple containing all parsed Forwarded header(s). 

318 

319 Makes an effort to parse Forwarded headers as specified by RFC 7239: 

320 

321 - It adds one (immutable) dictionary per Forwarded 'field-value', ie 

322 per proxy. The element corresponds to the data in the Forwarded 

323 field-value added by the first proxy encountered by the client. Each 

324 subsequent item corresponds to those added by later proxies. 

325 - It checks that every value has valid syntax in general as specified 

326 in section 4: either a 'token' or a 'quoted-string'. 

327 - It un-escapes found escape sequences. 

328 - It does NOT validate 'by' and 'for' contents as specified in section 

329 6. 

330 - It does NOT validate 'host' contents (Host ABNF). 

331 - It does NOT validate 'proto' contents for valid URI scheme names. 

332 

333 Returns a tuple containing one or more immutable dicts 

334 """ 

335 elems = [] 

336 for field_value in self._message.headers.getall(hdrs.FORWARDED, ()): 

337 length = len(field_value) 

338 pos = 0 

339 need_separator = False 

340 elem: Dict[str, str] = {} 

341 elems.append(types.MappingProxyType(elem)) 

342 while 0 <= pos < length: 

343 match = _FORWARDED_PAIR_RE.match(field_value, pos) 

344 if match is not None: # got a valid forwarded-pair 

345 if need_separator: 

346 # bad syntax here, skip to next comma 

347 pos = field_value.find(",", pos) 

348 else: 

349 name, value, port = match.groups() 

350 if value[0] == '"': 

351 # quoted string: remove quotes and unescape 

352 value = _QUOTED_PAIR_REPLACE_RE.sub(r"\1", value[1:-1]) 

353 if port: 

354 value += port 

355 elem[name.lower()] = value 

356 pos += len(match.group(0)) 

357 need_separator = True 

358 elif field_value[pos] == ",": # next forwarded-element 

359 need_separator = False 

360 elem = {} 

361 elems.append(types.MappingProxyType(elem)) 

362 pos += 1 

363 elif field_value[pos] == ";": # next forwarded-pair 

364 need_separator = False 

365 pos += 1 

366 elif field_value[pos] in " \t": 

367 # Allow whitespace even between forwarded-pairs, though 

368 # RFC 7239 doesn't. This simplifies code and is in line 

369 # with Postel's law. 

370 pos += 1 

371 else: 

372 # bad syntax here, skip to next comma 

373 pos = field_value.find(",", pos) 

374 return tuple(elems) 

375 

376 @reify 

377 def scheme(self) -> str: 

378 """A string representing the scheme of the request. 

379 

380 Hostname is resolved in this order: 

381 

382 - overridden value by .clone(scheme=new_scheme) call. 

383 - type of connection to peer: HTTPS if socket is SSL, HTTP otherwise. 

384 

385 'http' or 'https'. 

386 """ 

387 if self._transport_sslcontext: 

388 return "https" 

389 else: 

390 return "http" 

391 

392 @reify 

393 def method(self) -> str: 

394 """Read only property for getting HTTP method. 

395 

396 The value is upper-cased str like 'GET', 'POST', 'PUT' etc. 

397 """ 

398 return self._method 

399 

400 @reify 

401 def version(self) -> HttpVersion: 

402 """Read only property for getting HTTP version of request. 

403 

404 Returns aiohttp.protocol.HttpVersion instance. 

405 """ 

406 return self._version 

407 

408 @reify 

409 def host(self) -> str: 

410 """Hostname of the request. 

411 

412 Hostname is resolved in this order: 

413 

414 - overridden value by .clone(host=new_host) call. 

415 - HOST HTTP header 

416 - socket.getfqdn() value 

417 """ 

418 host = self._message.headers.get(hdrs.HOST) 

419 if host is not None: 

420 return host 

421 return socket.getfqdn() 

422 

423 @reify 

424 def remote(self) -> Optional[str]: 

425 """Remote IP of client initiated HTTP request. 

426 

427 The IP is resolved in this order: 

428 

429 - overridden value by .clone(remote=new_remote) call. 

430 - peername of opened socket 

431 """ 

432 if self._transport_peername is None: 

433 return None 

434 if isinstance(self._transport_peername, (list, tuple)): 

435 return str(self._transport_peername[0]) 

436 return str(self._transport_peername) 

437 

438 @reify 

439 def url(self) -> URL: 

440 url = URL.build(scheme=self.scheme, host=self.host) 

441 return url.join(self._rel_url) 

442 

443 @reify 

444 def path(self) -> str: 

445 """The URL including *PATH INFO* without the host or scheme. 

446 

447 E.g., ``/app/blog`` 

448 """ 

449 return self._rel_url.path 

450 

451 @reify 

452 def path_qs(self) -> str: 

453 """The URL including PATH_INFO and the query string. 

454 

455 E.g, /app/blog?id=10 

456 """ 

457 return str(self._rel_url) 

458 

459 @reify 

460 def raw_path(self) -> str: 

461 """The URL including raw *PATH INFO* without the host or scheme. 

462 

463 Warning, the path is unquoted and may contains non valid URL characters 

464 

465 E.g., ``/my%2Fpath%7Cwith%21some%25strange%24characters`` 

466 """ 

467 return self._message.path 

468 

469 @reify 

470 def query(self) -> MultiDictProxy[str]: 

471 """A multidict with all the variables in the query string.""" 

472 return MultiDictProxy(self._rel_url.query) 

473 

474 @reify 

475 def query_string(self) -> str: 

476 """The query string in the URL. 

477 

478 E.g., id=10 

479 """ 

480 return self._rel_url.query_string 

481 

482 @reify 

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

484 """A case-insensitive multidict proxy with all headers.""" 

485 return self._headers 

486 

487 @reify 

488 def raw_headers(self) -> RawHeaders: 

489 """A sequence of pairs for all headers.""" 

490 return self._message.raw_headers 

491 

492 @reify 

493 def if_modified_since(self) -> Optional[datetime.datetime]: 

494 """The value of If-Modified-Since HTTP header, or None. 

495 

496 This header is represented as a `datetime` object. 

497 """ 

498 return parse_http_date(self.headers.get(hdrs.IF_MODIFIED_SINCE)) 

499 

500 @reify 

501 def if_unmodified_since(self) -> Optional[datetime.datetime]: 

502 """The value of If-Unmodified-Since HTTP header, or None. 

503 

504 This header is represented as a `datetime` object. 

505 """ 

506 return parse_http_date(self.headers.get(hdrs.IF_UNMODIFIED_SINCE)) 

507 

508 @staticmethod 

509 def _etag_values(etag_header: str) -> Iterator[ETag]: 

510 """Extract `ETag` objects from raw header.""" 

511 if etag_header == ETAG_ANY: 

512 yield ETag( 

513 is_weak=False, 

514 value=ETAG_ANY, 

515 ) 

516 else: 

517 for match in LIST_QUOTED_ETAG_RE.finditer(etag_header): 

518 is_weak, value, garbage = match.group(2, 3, 4) 

519 # Any symbol captured by 4th group means 

520 # that the following sequence is invalid. 

521 if garbage: 

522 break 

523 

524 yield ETag( 

525 is_weak=bool(is_weak), 

526 value=value, 

527 ) 

528 

529 @classmethod 

530 def _if_match_or_none_impl( 

531 cls, header_value: Optional[str] 

532 ) -> Optional[Tuple[ETag, ...]]: 

533 if not header_value: 

534 return None 

535 

536 return tuple(cls._etag_values(header_value)) 

537 

538 @reify 

539 def if_match(self) -> Optional[Tuple[ETag, ...]]: 

540 """The value of If-Match HTTP header, or None. 

541 

542 This header is represented as a `tuple` of `ETag` objects. 

543 """ 

544 return self._if_match_or_none_impl(self.headers.get(hdrs.IF_MATCH)) 

545 

546 @reify 

547 def if_none_match(self) -> Optional[Tuple[ETag, ...]]: 

548 """The value of If-None-Match HTTP header, or None. 

549 

550 This header is represented as a `tuple` of `ETag` objects. 

551 """ 

552 return self._if_match_or_none_impl(self.headers.get(hdrs.IF_NONE_MATCH)) 

553 

554 @reify 

555 def if_range(self) -> Optional[datetime.datetime]: 

556 """The value of If-Range HTTP header, or None. 

557 

558 This header is represented as a `datetime` object. 

559 """ 

560 return parse_http_date(self.headers.get(hdrs.IF_RANGE)) 

561 

562 @reify 

563 def keep_alive(self) -> bool: 

564 """Is keepalive enabled by client?""" 

565 return not self._message.should_close 

566 

567 @reify 

568 def cookies(self) -> Mapping[str, str]: 

569 """Return request cookies. 

570 

571 A read-only dictionary-like object. 

572 """ 

573 raw = self.headers.get(hdrs.COOKIE, "") 

574 parsed: SimpleCookie[str] = SimpleCookie(raw) 

575 return MappingProxyType({key: val.value for key, val in parsed.items()}) 

576 

577 @reify 

578 def http_range(self) -> slice: 

579 """The content of Range HTTP header. 

580 

581 Return a slice instance. 

582 

583 """ 

584 rng = self._headers.get(hdrs.RANGE) 

585 start, end = None, None 

586 if rng is not None: 

587 try: 

588 pattern = r"^bytes=(\d*)-(\d*)$" 

589 start, end = re.findall(pattern, rng)[0] 

590 except IndexError: # pattern was not found in header 

591 raise ValueError("range not in acceptable format") 

592 

593 end = int(end) if end else None 

594 start = int(start) if start else None 

595 

596 if start is None and end is not None: 

597 # end with no start is to return tail of content 

598 start = -end 

599 end = None 

600 

601 if start is not None and end is not None: 

602 # end is inclusive in range header, exclusive for slice 

603 end += 1 

604 

605 if start >= end: 

606 raise ValueError("start cannot be after end") 

607 

608 if start is end is None: # No valid range supplied 

609 raise ValueError("No start or end of range specified") 

610 

611 return slice(start, end, 1) 

612 

613 @reify 

614 def content(self) -> StreamReader: 

615 """Return raw payload stream.""" 

616 return self._payload 

617 

618 @property 

619 def can_read_body(self) -> bool: 

620 """Return True if request's HTTP BODY can be read, False otherwise.""" 

621 return not self._payload.at_eof() 

622 

623 @reify 

624 def body_exists(self) -> bool: 

625 """Return True if request has HTTP BODY, False otherwise.""" 

626 return type(self._payload) is not EmptyStreamReader 

627 

628 async def release(self) -> None: 

629 """Release request. 

630 

631 Eat unread part of HTTP BODY if present. 

632 """ 

633 while not self._payload.at_eof(): 

634 await self._payload.readany() 

635 

636 async def read(self) -> bytes: 

637 """Read request body if present. 

638 

639 Returns bytes object with full request content. 

640 """ 

641 if self._read_bytes is None: 

642 body = bytearray() 

643 while True: 

644 chunk = await self._payload.readany() 

645 body.extend(chunk) 

646 if self._client_max_size: 

647 body_size = len(body) 

648 if body_size > self._client_max_size: 

649 raise HTTPRequestEntityTooLarge( 

650 max_size=self._client_max_size, actual_size=body_size 

651 ) 

652 if not chunk: 

653 break 

654 self._read_bytes = bytes(body) 

655 return self._read_bytes 

656 

657 async def text(self) -> str: 

658 """Return BODY as text using encoding from .charset.""" 

659 bytes_body = await self.read() 

660 encoding = self.charset or "utf-8" 

661 try: 

662 return bytes_body.decode(encoding) 

663 except LookupError: 

664 raise HTTPUnsupportedMediaType() 

665 

666 async def json( 

667 self, 

668 *, 

669 loads: JSONDecoder = DEFAULT_JSON_DECODER, 

670 content_type: Optional[str] = "application/json", 

671 ) -> Any: 

672 """Return BODY as JSON.""" 

673 body = await self.text() 

674 if content_type: 

675 if not is_expected_content_type(self.content_type, content_type): 

676 raise HTTPBadRequest( 

677 text=( 

678 "Attempt to decode JSON with " 

679 "unexpected mimetype: %s" % self.content_type 

680 ) 

681 ) 

682 

683 return loads(body) 

684 

685 async def multipart(self) -> MultipartReader: 

686 """Return async iterator to process BODY as multipart.""" 

687 return MultipartReader(self._headers, self._payload) 

688 

689 async def post(self) -> "MultiDictProxy[Union[str, bytes, FileField]]": 

690 """Return POST parameters.""" 

691 if self._post is not None: 

692 return self._post 

693 if self._method not in self.POST_METHODS: 

694 self._post = MultiDictProxy(MultiDict()) 

695 return self._post 

696 

697 content_type = self.content_type 

698 if content_type not in ( 

699 "", 

700 "application/x-www-form-urlencoded", 

701 "multipart/form-data", 

702 ): 

703 self._post = MultiDictProxy(MultiDict()) 

704 return self._post 

705 

706 out: MultiDict[Union[str, bytes, FileField]] = MultiDict() 

707 

708 if content_type == "multipart/form-data": 

709 multipart = await self.multipart() 

710 max_size = self._client_max_size 

711 

712 field = await multipart.next() 

713 while field is not None: 

714 size = 0 

715 field_ct = field.headers.get(hdrs.CONTENT_TYPE) 

716 

717 if isinstance(field, BodyPartReader): 

718 assert field.name is not None 

719 

720 # Note that according to RFC 7578, the Content-Type header 

721 # is optional, even for files, so we can't assume it's 

722 # present. 

723 # https://tools.ietf.org/html/rfc7578#section-4.4 

724 if field.filename: 

725 # store file in temp file 

726 tmp = tempfile.TemporaryFile() 

727 chunk = await field.read_chunk(size=2**16) 

728 while chunk: 

729 chunk = field.decode(chunk) 

730 tmp.write(chunk) 

731 size += len(chunk) 

732 if 0 < max_size < size: 

733 tmp.close() 

734 raise HTTPRequestEntityTooLarge( 

735 max_size=max_size, actual_size=size 

736 ) 

737 chunk = await field.read_chunk(size=2**16) 

738 tmp.seek(0) 

739 

740 if field_ct is None: 

741 field_ct = "application/octet-stream" 

742 

743 ff = FileField( 

744 field.name, 

745 field.filename, 

746 cast(io.BufferedReader, tmp), 

747 field_ct, 

748 field.headers, 

749 ) 

750 out.add(field.name, ff) 

751 else: 

752 # deal with ordinary data 

753 value = await field.read(decode=True) 

754 if field_ct is None or field_ct.startswith("text/"): 

755 charset = field.get_charset(default="utf-8") 

756 out.add(field.name, value.decode(charset)) 

757 else: 

758 out.add(field.name, value) 

759 size += len(value) 

760 if 0 < max_size < size: 

761 raise HTTPRequestEntityTooLarge( 

762 max_size=max_size, actual_size=size 

763 ) 

764 else: 

765 raise ValueError( 

766 "To decode nested multipart you need " "to use custom reader", 

767 ) 

768 

769 field = await multipart.next() 

770 else: 

771 data = await self.read() 

772 if data: 

773 charset = self.charset or "utf-8" 

774 bytes_query = data.rstrip() 

775 try: 

776 query = bytes_query.decode(charset) 

777 except LookupError: 

778 raise HTTPUnsupportedMediaType() 

779 out.extend( 

780 parse_qsl(qs=query, keep_blank_values=True, encoding=charset) 

781 ) 

782 

783 self._post = MultiDictProxy(out) 

784 return self._post 

785 

786 def get_extra_info(self, name: str, default: Any = None) -> Any: 

787 """Extra info from protocol transport""" 

788 protocol = self._protocol 

789 if protocol is None: 

790 return default 

791 

792 transport = protocol.transport 

793 if transport is None: 

794 return default 

795 

796 return transport.get_extra_info(name, default) 

797 

798 def __repr__(self) -> str: 

799 ascii_encodable_path = self.path.encode("ascii", "backslashreplace").decode( 

800 "ascii" 

801 ) 

802 return "<{} {} {} >".format( 

803 self.__class__.__name__, self._method, ascii_encodable_path 

804 ) 

805 

806 def __eq__(self, other: object) -> bool: 

807 return id(self) == id(other) 

808 

809 def __bool__(self) -> bool: 

810 return True 

811 

812 async def _prepare_hook(self, response: StreamResponse) -> None: 

813 return 

814 

815 def _cancel(self, exc: BaseException) -> None: 

816 self._payload.set_exception(exc) 

817 for fut in self._disconnection_waiters: 

818 set_result(fut, None) 

819 

820 def _finish(self) -> None: 

821 for fut in self._disconnection_waiters: 

822 fut.cancel() 

823 

824 if self._post is None or self.content_type != "multipart/form-data": 

825 return 

826 

827 # NOTE: Release file descriptors for the 

828 # NOTE: `tempfile.Temporaryfile`-created `_io.BufferedRandom` 

829 # NOTE: instances of files sent within multipart request body 

830 # NOTE: via HTTP POST request. 

831 for file_name, file_field_object in self._post.items(): 

832 if not isinstance(file_field_object, FileField): 

833 continue 

834 

835 file_field_object.file.close() 

836 

837 async def wait_for_disconnection(self) -> None: 

838 loop = asyncio.get_event_loop() 

839 fut: asyncio.Future[None] = loop.create_future() 

840 self._disconnection_waiters.add(fut) 

841 try: 

842 await fut 

843 finally: 

844 self._disconnection_waiters.remove(fut) 

845 

846 

847class Request(BaseRequest): 

848 __slots__ = ("_match_info",) 

849 

850 def __init__(self, *args: Any, **kwargs: Any) -> None: 

851 super().__init__(*args, **kwargs) 

852 

853 # matchdict, route_name, handler 

854 # or information about traversal lookup 

855 

856 # initialized after route resolving 

857 self._match_info: Optional[UrlMappingMatchInfo] = None 

858 

859 def clone( 

860 self, 

861 *, 

862 method: Union[str, _SENTINEL] = sentinel, 

863 rel_url: Union[StrOrURL, _SENTINEL] = sentinel, 

864 headers: Union[LooseHeaders, _SENTINEL] = sentinel, 

865 scheme: Union[str, _SENTINEL] = sentinel, 

866 host: Union[str, _SENTINEL] = sentinel, 

867 remote: Union[str, _SENTINEL] = sentinel, 

868 client_max_size: Union[int, _SENTINEL] = sentinel, 

869 ) -> "Request": 

870 ret = super().clone( 

871 method=method, 

872 rel_url=rel_url, 

873 headers=headers, 

874 scheme=scheme, 

875 host=host, 

876 remote=remote, 

877 client_max_size=client_max_size, 

878 ) 

879 new_ret = cast(Request, ret) 

880 new_ret._match_info = self._match_info 

881 return new_ret 

882 

883 @reify 

884 def match_info(self) -> "UrlMappingMatchInfo": 

885 """Result of route resolving.""" 

886 match_info = self._match_info 

887 assert match_info is not None 

888 return match_info 

889 

890 @property 

891 def app(self) -> "Application": 

892 """Application instance.""" 

893 match_info = self._match_info 

894 assert match_info is not None 

895 return match_info.current_app 

896 

897 @property 

898 def config_dict(self) -> ChainMapProxy: 

899 match_info = self._match_info 

900 assert match_info is not None 

901 lst = match_info.apps 

902 app = self.app 

903 idx = lst.index(app) 

904 sublist = list(reversed(lst[: idx + 1])) 

905 return ChainMapProxy(sublist) 

906 

907 async def _prepare_hook(self, response: StreamResponse) -> None: 

908 match_info = self._match_info 

909 if match_info is None: 

910 return 

911 for app in match_info._apps: 

912 await app.on_response_prepare.send(self, response)