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

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

434 statements  

1import asyncio 

2import datetime 

3import io 

4import re 

5import socket 

6import string 

7import sys 

8import tempfile 

9import types 

10from http.cookies import SimpleCookie 

11from types import MappingProxyType 

12from typing import ( 

13 TYPE_CHECKING, 

14 Any, 

15 Dict, 

16 Final, 

17 Iterator, 

18 Mapping, 

19 MutableMapping, 

20 Optional, 

21 Pattern, 

22 Tuple, 

23 Union, 

24 cast, 

25) 

26from urllib.parse import parse_qsl 

27 

28from multidict import CIMultiDict, CIMultiDictProxy, MultiDict, MultiDictProxy 

29from yarl import URL 

30 

31from . import hdrs 

32from .abc import AbstractStreamWriter 

33from .helpers import ( 

34 _SENTINEL, 

35 ETAG_ANY, 

36 LIST_QUOTED_ETAG_RE, 

37 ChainMapProxy, 

38 ETag, 

39 HeadersMixin, 

40 frozen_dataclass_decorator, 

41 is_expected_content_type, 

42 parse_http_date, 

43 reify, 

44 sentinel, 

45 set_exception, 

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 

65if sys.version_info >= (3, 11): 

66 from typing import Self 

67else: 

68 Self = Any 

69 

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

71 

72 

73if TYPE_CHECKING: 

74 from .web_app import Application 

75 from .web_protocol import RequestHandler 

76 from .web_urldispatcher import UrlMappingMatchInfo 

77 

78 

79@frozen_dataclass_decorator 

80class FileField: 

81 name: str 

82 filename: str 

83 file: io.BufferedReader 

84 content_type: str 

85 headers: CIMultiDictProxy[str] 

86 

87 

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

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

90 

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

92 

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

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

95) 

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

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

98 

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

100 

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

102 qdtext=_QDTEXT, quoted_pair=_QUOTED_PAIR 

103) 

104 

105_FORWARDED_PAIR: Final[str] = ( 

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

107 token=_TOKEN, quoted_string=_QUOTED_STRING 

108 ) 

109) 

110 

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

112# same pattern as _QUOTED_PAIR but contains a capture group 

113 

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

115 

116############################################################ 

117# HTTP Request 

118############################################################ 

119 

120 

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

122 POST_METHODS = { 

123 hdrs.METH_PATCH, 

124 hdrs.METH_POST, 

125 hdrs.METH_PUT, 

126 hdrs.METH_TRACE, 

127 hdrs.METH_DELETE, 

128 } 

129 

130 _post: Optional[MultiDictProxy[Union[str, bytes, FileField]]] = None 

131 _read_bytes: Optional[bytes] = None 

132 

133 def __init__( 

134 self, 

135 message: RawRequestMessage, 

136 payload: StreamReader, 

137 protocol: "RequestHandler[Self]", 

138 payload_writer: AbstractStreamWriter, 

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

140 loop: asyncio.AbstractEventLoop, 

141 *, 

142 client_max_size: int = 1024**2, 

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

144 scheme: Optional[str] = None, 

145 host: Optional[str] = None, 

146 remote: Optional[str] = None, 

147 ) -> None: 

148 self._message = message 

149 self._protocol = protocol 

150 self._payload_writer = payload_writer 

151 

152 self._payload = payload 

153 self._headers: CIMultiDictProxy[str] = message.headers 

154 self._method = message.method 

155 self._version = message.version 

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

157 url = message.url 

158 if url.absolute: 

159 if scheme is not None: 

160 url = url.with_scheme(scheme) 

161 if host is not None: 

162 url = url.with_host(host) 

163 # absolute URL is given, 

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

165 # all other properties should be good 

166 self._cache["url"] = url 

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

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

169 self._rel_url = url.relative() 

170 else: 

171 self._rel_url = url 

172 if scheme is not None: 

173 self._cache["scheme"] = scheme 

174 if host is not None: 

175 self._cache["host"] = host 

176 

177 self._state = {} if state is None else state 

178 self._task = task 

179 self._client_max_size = client_max_size 

180 self._loop = loop 

181 

182 self._transport_sslcontext = protocol.ssl_context 

183 self._transport_peername = protocol.peername 

184 

185 if remote is not None: 

186 self._cache["remote"] = remote 

187 

188 def clone( 

189 self, 

190 *, 

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

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

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

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

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

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

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

198 ) -> "BaseRequest": 

199 """Clone itself with replacement some attributes. 

200 

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

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

203 will reuse the one from the current request object. 

204 """ 

205 if self._read_bytes: 

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

207 

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

209 if method is not sentinel: 

210 dct["method"] = method 

211 if rel_url is not sentinel: 

212 new_url: URL = URL(rel_url) 

213 dct["url"] = new_url 

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

215 if headers is not sentinel: 

216 # a copy semantic 

217 new_headers = CIMultiDictProxy(CIMultiDict(headers)) 

218 dct["headers"] = new_headers 

219 dct["raw_headers"] = tuple( 

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

221 ) 

222 

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

224 

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

226 if scheme is not sentinel: 

227 kwargs["scheme"] = scheme 

228 if host is not sentinel: 

229 kwargs["host"] = host 

230 if remote is not sentinel: 

231 kwargs["remote"] = remote 

232 if client_max_size is sentinel: 

233 client_max_size = self._client_max_size 

234 

235 return self.__class__( 

236 message, 

237 self._payload, 

238 self._protocol, # type: ignore[arg-type] 

239 self._payload_writer, 

240 self._task, 

241 self._loop, 

242 client_max_size=client_max_size, 

243 state=self._state.copy(), 

244 **kwargs, 

245 ) 

246 

247 @property 

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

249 return self._task 

250 

251 @property 

252 def protocol(self) -> "RequestHandler[Self]": 

253 return self._protocol 

254 

255 @property 

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

257 return self._protocol.transport 

258 

259 @property 

260 def writer(self) -> AbstractStreamWriter: 

261 return self._payload_writer 

262 

263 @property 

264 def client_max_size(self) -> int: 

265 return self._client_max_size 

266 

267 @reify 

268 def rel_url(self) -> URL: 

269 return self._rel_url 

270 

271 # MutableMapping API 

272 

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

274 return self._state[key] 

275 

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

277 self._state[key] = value 

278 

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

280 del self._state[key] 

281 

282 def __len__(self) -> int: 

283 return len(self._state) 

284 

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

286 return iter(self._state) 

287 

288 ######## 

289 

290 @reify 

291 def secure(self) -> bool: 

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

293 return self.scheme == "https" 

294 

295 @reify 

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

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

298 

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

300 

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

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

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

304 subsequent item corresponds to those added by later proxies. 

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

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

307 - It un-escapes found escape sequences. 

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

309 6. 

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

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

312 

313 Returns a tuple containing one or more immutable dicts 

314 """ 

315 elems = [] 

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

317 length = len(field_value) 

318 pos = 0 

319 need_separator = False 

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

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

322 while 0 <= pos < length: 

323 match = _FORWARDED_PAIR_RE.match(field_value, pos) 

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

325 if need_separator: 

326 # bad syntax here, skip to next comma 

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

328 else: 

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

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

331 # quoted string: remove quotes and unescape 

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

333 if port: 

334 value += port 

335 elem[name.lower()] = value 

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

337 need_separator = True 

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

339 need_separator = False 

340 elem = {} 

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

342 pos += 1 

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

344 need_separator = False 

345 pos += 1 

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

347 # Allow whitespace even between forwarded-pairs, though 

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

349 # with Postel's law. 

350 pos += 1 

351 else: 

352 # bad syntax here, skip to next comma 

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

354 return tuple(elems) 

355 

356 @reify 

357 def scheme(self) -> str: 

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

359 

360 Hostname is resolved in this order: 

361 

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

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

364 

365 'http' or 'https'. 

366 """ 

367 if self._transport_sslcontext: 

368 return "https" 

369 else: 

370 return "http" 

371 

372 @reify 

373 def method(self) -> str: 

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

375 

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

377 """ 

378 return self._method 

379 

380 @reify 

381 def version(self) -> HttpVersion: 

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

383 

384 Returns aiohttp.protocol.HttpVersion instance. 

385 """ 

386 return self._version 

387 

388 @reify 

389 def host(self) -> str: 

390 """Hostname of the request. 

391 

392 Hostname is resolved in this order: 

393 

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

395 - HOST HTTP header 

396 - socket.getfqdn() value 

397 

398 For example, 'example.com' or 'localhost:8080'. 

399 

400 For historical reasons, the port number may be included. 

401 """ 

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

403 if host is not None: 

404 return host 

405 return socket.getfqdn() 

406 

407 @reify 

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

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

410 

411 The IP is resolved in this order: 

412 

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

414 - peername of opened socket 

415 """ 

416 if self._transport_peername is None: 

417 return None 

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

419 return str(self._transport_peername[0]) 

420 return str(self._transport_peername) 

421 

422 @reify 

423 def url(self) -> URL: 

424 """The full URL of the request.""" 

425 # authority is used here because it may include the port number 

426 # and we want yarl to parse it correctly 

427 return URL.build(scheme=self.scheme, authority=self.host).join(self._rel_url) 

428 

429 @reify 

430 def path(self) -> str: 

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

432 

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

434 """ 

435 return self._rel_url.path 

436 

437 @reify 

438 def path_qs(self) -> str: 

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

440 

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

442 """ 

443 return str(self._rel_url) 

444 

445 @reify 

446 def raw_path(self) -> str: 

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

448 

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

450 

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

452 """ 

453 return self._message.path 

454 

455 @reify 

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

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

458 return self._rel_url.query 

459 

460 @reify 

461 def query_string(self) -> str: 

462 """The query string in the URL. 

463 

464 E.g., id=10 

465 """ 

466 return self._rel_url.query_string 

467 

468 @reify 

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

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

471 return self._headers 

472 

473 @reify 

474 def raw_headers(self) -> RawHeaders: 

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

476 return self._message.raw_headers 

477 

478 @reify 

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

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

481 

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

483 """ 

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

485 

486 @reify 

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

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

489 

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

491 """ 

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

493 

494 @staticmethod 

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

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

497 if etag_header == ETAG_ANY: 

498 yield ETag( 

499 is_weak=False, 

500 value=ETAG_ANY, 

501 ) 

502 else: 

503 for match in LIST_QUOTED_ETAG_RE.finditer(etag_header): 

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

505 # Any symbol captured by 4th group means 

506 # that the following sequence is invalid. 

507 if garbage: 

508 break 

509 

510 yield ETag( 

511 is_weak=bool(is_weak), 

512 value=value, 

513 ) 

514 

515 @classmethod 

516 def _if_match_or_none_impl( 

517 cls, header_value: Optional[str] 

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

519 if not header_value: 

520 return None 

521 

522 return tuple(cls._etag_values(header_value)) 

523 

524 @reify 

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

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

527 

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

529 """ 

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

531 

532 @reify 

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

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

535 

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

537 """ 

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

539 

540 @reify 

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

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

543 

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

545 """ 

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

547 

548 @reify 

549 def keep_alive(self) -> bool: 

550 """Is keepalive enabled by client?""" 

551 return not self._message.should_close 

552 

553 @reify 

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

555 """Return request cookies. 

556 

557 A read-only dictionary-like object. 

558 """ 

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

560 parsed = SimpleCookie(raw) 

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

562 

563 @reify 

564 def http_range(self) -> "slice[int, int, int]": 

565 """The content of Range HTTP header. 

566 

567 Return a slice instance. 

568 

569 """ 

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

571 start, end = None, None 

572 if rng is not None: 

573 try: 

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

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

576 except IndexError: # pattern was not found in header 

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

578 

579 end = int(end) if end else None 

580 start = int(start) if start else None 

581 

582 if start is None and end is not None: 

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

584 start = -end 

585 end = None 

586 

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

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

589 end += 1 

590 

591 if start >= end: 

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

593 

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

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

596 

597 return slice(start, end, 1) 

598 

599 @reify 

600 def content(self) -> StreamReader: 

601 """Return raw payload stream.""" 

602 return self._payload 

603 

604 @property 

605 def can_read_body(self) -> bool: 

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

607 return not self._payload.at_eof() 

608 

609 @reify 

610 def body_exists(self) -> bool: 

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

612 return type(self._payload) is not EmptyStreamReader 

613 

614 async def release(self) -> None: 

615 """Release request. 

616 

617 Eat unread part of HTTP BODY if present. 

618 """ 

619 while not self._payload.at_eof(): 

620 await self._payload.readany() 

621 

622 async def read(self) -> bytes: 

623 """Read request body if present. 

624 

625 Returns bytes object with full request content. 

626 """ 

627 if self._read_bytes is None: 

628 body = bytearray() 

629 while True: 

630 chunk = await self._payload.readany() 

631 body.extend(chunk) 

632 if self._client_max_size: 

633 body_size = len(body) 

634 if body_size > self._client_max_size: 

635 raise HTTPRequestEntityTooLarge( 

636 max_size=self._client_max_size, actual_size=body_size 

637 ) 

638 if not chunk: 

639 break 

640 self._read_bytes = bytes(body) 

641 return self._read_bytes 

642 

643 async def text(self) -> str: 

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

645 bytes_body = await self.read() 

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

647 try: 

648 return bytes_body.decode(encoding) 

649 except LookupError: 

650 raise HTTPUnsupportedMediaType() 

651 

652 async def json( 

653 self, 

654 *, 

655 loads: JSONDecoder = DEFAULT_JSON_DECODER, 

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

657 ) -> Any: 

658 """Return BODY as JSON.""" 

659 body = await self.text() 

660 if content_type: 

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

662 raise HTTPBadRequest( 

663 text=( 

664 "Attempt to decode JSON with " 

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

666 ) 

667 ) 

668 

669 return loads(body) 

670 

671 async def multipart(self) -> MultipartReader: 

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

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

674 

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

676 """Return POST parameters.""" 

677 if self._post is not None: 

678 return self._post 

679 if self._method not in self.POST_METHODS: 

680 self._post = MultiDictProxy(MultiDict()) 

681 return self._post 

682 

683 content_type = self.content_type 

684 if content_type not in ( 

685 "", 

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

687 "multipart/form-data", 

688 ): 

689 self._post = MultiDictProxy(MultiDict()) 

690 return self._post 

691 

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

693 

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

695 multipart = await self.multipart() 

696 max_size = self._client_max_size 

697 

698 field = await multipart.next() 

699 while field is not None: 

700 size = 0 

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

702 

703 if isinstance(field, BodyPartReader): 

704 assert field.name is not None 

705 

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

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

708 # present. 

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

710 if field.filename: 

711 # store file in temp file 

712 tmp = await self._loop.run_in_executor( 

713 None, tempfile.TemporaryFile 

714 ) 

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

716 while chunk: 

717 chunk = field.decode(chunk) 

718 await self._loop.run_in_executor(None, tmp.write, chunk) 

719 size += len(chunk) 

720 if 0 < max_size < size: 

721 await self._loop.run_in_executor(None, tmp.close) 

722 raise HTTPRequestEntityTooLarge( 

723 max_size=max_size, actual_size=size 

724 ) 

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

726 await self._loop.run_in_executor(None, tmp.seek, 0) 

727 

728 if field_ct is None: 

729 field_ct = "application/octet-stream" 

730 

731 ff = FileField( 

732 field.name, 

733 field.filename, 

734 cast(io.BufferedReader, tmp), 

735 field_ct, 

736 field.headers, 

737 ) 

738 out.add(field.name, ff) 

739 else: 

740 # deal with ordinary data 

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

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

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

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

745 else: 

746 out.add(field.name, value) 

747 size += len(value) 

748 if 0 < max_size < size: 

749 raise HTTPRequestEntityTooLarge( 

750 max_size=max_size, actual_size=size 

751 ) 

752 else: 

753 raise ValueError( 

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

755 ) 

756 

757 field = await multipart.next() 

758 else: 

759 data = await self.read() 

760 if data: 

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

762 bytes_query = data.rstrip() 

763 try: 

764 query = bytes_query.decode(charset) 

765 except LookupError: 

766 raise HTTPUnsupportedMediaType() 

767 out.extend( 

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

769 ) 

770 

771 self._post = MultiDictProxy(out) 

772 return self._post 

773 

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

775 """Extra info from protocol transport""" 

776 transport = self._protocol.transport 

777 if transport is None: 

778 return default 

779 

780 return transport.get_extra_info(name, default) 

781 

782 def __repr__(self) -> str: 

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

784 "ascii" 

785 ) 

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

787 self.__class__.__name__, self._method, ascii_encodable_path 

788 ) 

789 

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

791 return id(self) == id(other) 

792 

793 def __bool__(self) -> bool: 

794 return True 

795 

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

797 return 

798 

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

800 set_exception(self._payload, exc) 

801 

802 def _finish(self) -> None: 

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

804 return 

805 

806 # NOTE: Release file descriptors for the 

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

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

809 # NOTE: via HTTP POST request. 

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

811 if isinstance(file_field_object, FileField): 

812 file_field_object.file.close() 

813 

814 

815class Request(BaseRequest): 

816 

817 _match_info: Optional["UrlMappingMatchInfo"] = None 

818 

819 def clone( 

820 self, 

821 *, 

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

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

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

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

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

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

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

829 ) -> "Request": 

830 ret = super().clone( 

831 method=method, 

832 rel_url=rel_url, 

833 headers=headers, 

834 scheme=scheme, 

835 host=host, 

836 remote=remote, 

837 client_max_size=client_max_size, 

838 ) 

839 new_ret = cast(Request, ret) 

840 new_ret._match_info = self._match_info 

841 return new_ret 

842 

843 @reify 

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

845 """Result of route resolving.""" 

846 match_info = self._match_info 

847 assert match_info is not None 

848 return match_info 

849 

850 @property 

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

852 """Application instance.""" 

853 match_info = self._match_info 

854 assert match_info is not None 

855 return match_info.current_app 

856 

857 @property 

858 def config_dict(self) -> ChainMapProxy: 

859 match_info = self._match_info 

860 assert match_info is not None 

861 lst = match_info.apps 

862 app = self.app 

863 idx = lst.index(app) 

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

865 return ChainMapProxy(sublist) 

866 

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

868 match_info = self._match_info 

869 if match_info is None: 

870 return 

871 for app in match_info._apps: 

872 if on_response_prepare := app.on_response_prepare: 

873 await on_response_prepare.send(self, response)