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

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

446 statements  

1import asyncio 

2import datetime 

3import io 

4import re 

5import socket 

6import string 

7import sys 

8import tempfile 

9import types 

10from collections.abc import Iterator, Mapping, MutableMapping 

11from re import Pattern 

12from types import MappingProxyType 

13from typing import TYPE_CHECKING, Any, Final, Optional, TypeVar, cast, overload 

14from urllib.parse import parse_qsl 

15 

16from multidict import CIMultiDict, CIMultiDictProxy, MultiDict, MultiDictProxy 

17from yarl import URL 

18 

19from . import hdrs 

20from ._cookie_helpers import parse_cookie_header 

21from .abc import AbstractStreamWriter 

22from .helpers import ( 

23 _SENTINEL, 

24 ETAG_ANY, 

25 LIST_QUOTED_ETAG_RE, 

26 ChainMapProxy, 

27 ETag, 

28 HeadersMixin, 

29 RequestKey, 

30 frozen_dataclass_decorator, 

31 is_expected_content_type, 

32 parse_http_date, 

33 reify, 

34 sentinel, 

35 set_exception, 

36) 

37from .http_parser import RawRequestMessage 

38from .http_writer import HttpVersion 

39from .multipart import BodyPartReader, MultipartReader 

40from .streams import EmptyStreamReader, StreamReader 

41from .typedefs import ( 

42 DEFAULT_JSON_DECODER, 

43 JSONDecoder, 

44 LooseHeaders, 

45 RawHeaders, 

46 StrOrURL, 

47) 

48from .web_exceptions import ( 

49 HTTPBadRequest, 

50 HTTPRequestEntityTooLarge, 

51 HTTPUnsupportedMediaType, 

52) 

53from .web_response import StreamResponse 

54 

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

56 from typing import Self 

57else: 

58 Self = Any 

59 

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

61 

62 

63if TYPE_CHECKING: 

64 from .web_app import Application 

65 from .web_protocol import RequestHandler 

66 from .web_urldispatcher import UrlMappingMatchInfo 

67 

68 

69_T = TypeVar("_T") 

70 

71 

72@frozen_dataclass_decorator 

73class FileField: 

74 name: str 

75 filename: str 

76 file: io.BufferedReader 

77 content_type: str 

78 headers: CIMultiDictProxy[str] 

79 

80 

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

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

83 

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

85 

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

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

88) 

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

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

91 

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

93 

94_QUOTED_STRING: Final[str] = rf'"(?:{_QUOTED_PAIR}|{_QDTEXT})*"' 

95 

96# This does not have a ReDOS/performance concern as long as it used with re.match(). 

97_FORWARDED_PAIR: Final[str] = rf"({_TOKEN})=({_TOKEN}|{_QUOTED_STRING})(:\d{{1,4}})?" 

98 

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

100# same pattern as _QUOTED_PAIR but contains a capture group 

101 

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

103 

104############################################################ 

105# HTTP Request 

106############################################################ 

107 

108 

109class BaseRequest(MutableMapping[str | RequestKey[Any], Any], HeadersMixin): 

110 POST_METHODS = { 

111 hdrs.METH_PATCH, 

112 hdrs.METH_POST, 

113 hdrs.METH_PUT, 

114 hdrs.METH_TRACE, 

115 hdrs.METH_DELETE, 

116 } 

117 

118 _post: MultiDictProxy[str | bytes | FileField] | None = None 

119 _read_bytes: bytes | None = None 

120 

121 def __init__( 

122 self, 

123 message: RawRequestMessage, 

124 payload: StreamReader, 

125 protocol: "RequestHandler[Self]", 

126 payload_writer: AbstractStreamWriter, 

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

128 loop: asyncio.AbstractEventLoop, 

129 *, 

130 client_max_size: int = 1024**2, 

131 state: dict[RequestKey[Any] | str, Any] | None = None, 

132 scheme: str | None = None, 

133 host: str | None = None, 

134 remote: str | None = None, 

135 ) -> None: 

136 self._message = message 

137 self._protocol = protocol 

138 self._payload_writer = payload_writer 

139 

140 self._payload = payload 

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

142 self._method = message.method 

143 self._version = message.version 

144 self._cache: dict[str, Any] = {} 

145 url = message.url 

146 if url.absolute: 

147 if scheme is not None: 

148 url = url.with_scheme(scheme) 

149 if host is not None: 

150 url = url.with_host(host) 

151 # absolute URL is given, 

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

153 # all other properties should be good 

154 self._cache["url"] = url 

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

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

157 self._rel_url = url.relative() 

158 else: 

159 self._rel_url = url 

160 if scheme is not None: 

161 self._cache["scheme"] = scheme 

162 if host is not None: 

163 self._cache["host"] = host 

164 

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

166 self._task = task 

167 self._client_max_size = client_max_size 

168 self._loop = loop 

169 

170 self._transport_sslcontext = protocol.ssl_context 

171 self._transport_peername = protocol.peername 

172 

173 if remote is not None: 

174 self._cache["remote"] = remote 

175 

176 def clone( 

177 self, 

178 *, 

179 method: str | _SENTINEL = sentinel, 

180 rel_url: StrOrURL | _SENTINEL = sentinel, 

181 headers: LooseHeaders | _SENTINEL = sentinel, 

182 scheme: str | _SENTINEL = sentinel, 

183 host: str | _SENTINEL = sentinel, 

184 remote: str | _SENTINEL = sentinel, 

185 client_max_size: int | _SENTINEL = sentinel, 

186 ) -> "BaseRequest": 

187 """Clone itself with replacement some attributes. 

188 

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

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

191 will reuse the one from the current request object. 

192 """ 

193 if self._read_bytes: 

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

195 

196 dct: dict[str, Any] = {} 

197 if method is not sentinel: 

198 dct["method"] = method 

199 if rel_url is not sentinel: 

200 new_url: URL = URL(rel_url) 

201 dct["url"] = new_url 

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

203 if headers is not sentinel: 

204 # a copy semantic 

205 new_headers = CIMultiDictProxy(CIMultiDict(headers)) 

206 dct["headers"] = new_headers 

207 dct["raw_headers"] = tuple( 

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

209 ) 

210 

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

212 

213 kwargs: dict[str, str] = {} 

214 if scheme is not sentinel: 

215 kwargs["scheme"] = scheme 

216 if host is not sentinel: 

217 kwargs["host"] = host 

218 if remote is not sentinel: 

219 kwargs["remote"] = remote 

220 if client_max_size is sentinel: 

221 client_max_size = self._client_max_size 

222 

223 return self.__class__( 

224 message, 

225 self._payload, 

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

227 self._payload_writer, 

228 self._task, 

229 self._loop, 

230 client_max_size=client_max_size, 

231 state=self._state.copy(), 

232 **kwargs, 

233 ) 

234 

235 @property 

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

237 return self._task 

238 

239 @property 

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

241 return self._protocol 

242 

243 @property 

244 def transport(self) -> asyncio.Transport | None: 

245 return self._protocol.transport 

246 

247 @property 

248 def writer(self) -> AbstractStreamWriter: 

249 return self._payload_writer 

250 

251 @property 

252 def client_max_size(self) -> int: 

253 return self._client_max_size 

254 

255 @reify 

256 def rel_url(self) -> URL: 

257 return self._rel_url 

258 

259 # MutableMapping API 

260 

261 @overload # type: ignore[override] 

262 def __getitem__(self, key: RequestKey[_T]) -> _T: ... 

263 

264 @overload 

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

266 

267 def __getitem__(self, key: str | RequestKey[_T]) -> Any: 

268 return self._state[key] 

269 

270 @overload # type: ignore[override] 

271 def __setitem__(self, key: RequestKey[_T], value: _T) -> None: ... 

272 

273 @overload 

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

275 

276 def __setitem__(self, key: str | RequestKey[_T], value: Any) -> None: 

277 self._state[key] = value 

278 

279 def __delitem__(self, key: str | RequestKey[_T]) -> None: 

280 del self._state[key] 

281 

282 def __len__(self) -> int: 

283 return len(self._state) 

284 

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

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) -> str | None: 

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) -> datetime.datetime | None: 

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) -> datetime.datetime | None: 

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: str | None 

518 ) -> tuple[ETag, ...] | None: 

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) -> tuple[ETag, ...] | None: 

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) -> tuple[ETag, ...] | None: 

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) -> datetime.datetime | None: 

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 # Use parse_cookie_header for RFC 6265 compliant Cookie header parsing 

560 # that accepts special characters in cookie names (fixes #2683) 

561 parsed = parse_cookie_header(self.headers.get(hdrs.COOKIE, "")) 

562 # Extract values from Morsel objects 

563 return MappingProxyType({name: morsel.value for name, morsel in parsed}) 

564 

565 @reify 

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

567 """The content of Range HTTP header. 

568 

569 Return a slice instance. 

570 

571 """ 

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

573 start, end = None, None 

574 if rng is not None: 

575 try: 

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

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

578 except IndexError: # pattern was not found in header 

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

580 

581 end = int(end) if end else None 

582 start = int(start) if start else None 

583 

584 if start is None and end is not None: 

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

586 start = -end 

587 end = None 

588 

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

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

591 end += 1 

592 

593 if start >= end: 

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

595 

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

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

598 

599 return slice(start, end, 1) 

600 

601 @reify 

602 def content(self) -> StreamReader: 

603 """Return raw payload stream.""" 

604 return self._payload 

605 

606 @property 

607 def can_read_body(self) -> bool: 

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

609 return not self._payload.at_eof() 

610 

611 @reify 

612 def body_exists(self) -> bool: 

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

614 return type(self._payload) is not EmptyStreamReader 

615 

616 async def release(self) -> None: 

617 """Release request. 

618 

619 Eat unread part of HTTP BODY if present. 

620 """ 

621 while not self._payload.at_eof(): 

622 await self._payload.readany() 

623 

624 async def read(self) -> bytes: 

625 """Read request body if present. 

626 

627 Returns bytes object with full request content. 

628 """ 

629 if self._read_bytes is None: 

630 body = bytearray() 

631 while True: 

632 chunk = await self._payload.readany() 

633 body.extend(chunk) 

634 if self._client_max_size: 

635 body_size = len(body) 

636 if body_size > self._client_max_size: 

637 raise HTTPRequestEntityTooLarge( 

638 max_size=self._client_max_size, actual_size=body_size 

639 ) 

640 if not chunk: 

641 break 

642 self._read_bytes = bytes(body) 

643 return self._read_bytes 

644 

645 async def text(self) -> str: 

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

647 bytes_body = await self.read() 

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

649 try: 

650 return bytes_body.decode(encoding) 

651 except LookupError: 

652 raise HTTPUnsupportedMediaType() 

653 

654 async def json( 

655 self, 

656 *, 

657 loads: JSONDecoder = DEFAULT_JSON_DECODER, 

658 content_type: str | None = "application/json", 

659 ) -> Any: 

660 """Return BODY as JSON.""" 

661 body = await self.text() 

662 if content_type: 

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

664 raise HTTPBadRequest( 

665 text=( 

666 "Attempt to decode JSON with " 

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

668 ) 

669 ) 

670 

671 return loads(body) 

672 

673 async def multipart(self) -> MultipartReader: 

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

675 return MultipartReader( 

676 self._headers, 

677 self._payload, 

678 max_field_size=self._protocol.max_field_size, 

679 max_headers=self._protocol.max_headers, 

680 ) 

681 

682 async def post(self) -> "MultiDictProxy[str | bytes | FileField]": 

683 """Return POST parameters.""" 

684 if self._post is not None: 

685 return self._post 

686 if self._method not in self.POST_METHODS: 

687 self._post = MultiDictProxy(MultiDict()) 

688 return self._post 

689 

690 content_type = self.content_type 

691 if content_type not in ( 

692 "", 

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

694 "multipart/form-data", 

695 ): 

696 self._post = MultiDictProxy(MultiDict()) 

697 return self._post 

698 

699 out: MultiDict[str | bytes | FileField] = MultiDict() 

700 

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

702 multipart = await self.multipart() 

703 max_size = self._client_max_size 

704 

705 size = 0 

706 while (field := await multipart.next()) is not None: 

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

708 

709 if isinstance(field, BodyPartReader): 

710 if field.name is None: 

711 raise ValueError("Multipart field missing name.") 

712 

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

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

715 # present. 

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

717 if field.filename: 

718 # store file in temp file 

719 tmp = await self._loop.run_in_executor( 

720 None, tempfile.TemporaryFile 

721 ) 

722 while chunk := await field.read_chunk(size=2**18): 

723 async for decoded_chunk in field.decode_iter(chunk): 

724 await self._loop.run_in_executor( 

725 None, tmp.write, decoded_chunk 

726 ) 

727 size += len(decoded_chunk) 

728 if 0 < max_size < size: 

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

730 raise HTTPRequestEntityTooLarge( 

731 max_size=max_size, actual_size=size 

732 ) 

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

734 

735 if field_ct is None: 

736 field_ct = "application/octet-stream" 

737 

738 ff = FileField( 

739 field.name, 

740 field.filename, 

741 cast(io.BufferedReader, tmp), 

742 field_ct, 

743 field.headers, 

744 ) 

745 out.add(field.name, ff) 

746 else: 

747 # deal with ordinary data 

748 raw_data = bytearray() 

749 while chunk := await field.read_chunk(): 

750 size += len(chunk) 

751 if 0 < max_size < size: 

752 raise HTTPRequestEntityTooLarge( 

753 max_size=max_size, actual_size=size 

754 ) 

755 raw_data.extend(chunk) 

756 

757 value = bytearray() 

758 # form-data doesn't support compression, so don't need to check size again. 

759 async for d in field.decode_iter(raw_data): # type: ignore[arg-type] 

760 value.extend(d) 

761 

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

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

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

765 else: 

766 out.add(field.name, value) # type: ignore[arg-type] 

767 else: 

768 raise ValueError( 

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

770 ) 

771 else: 

772 data = await self.read() 

773 if data: 

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

775 bytes_query = data.rstrip() 

776 try: 

777 query = bytes_query.decode(charset) 

778 except LookupError: 

779 raise HTTPUnsupportedMediaType() 

780 out.extend( 

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

782 ) 

783 

784 self._post = MultiDictProxy(out) 

785 return self._post 

786 

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

788 """Extra info from protocol transport""" 

789 transport = self._protocol.transport 

790 if transport is None: 

791 return default 

792 

793 return transport.get_extra_info(name, default) 

794 

795 def __repr__(self) -> str: 

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

797 "ascii" 

798 ) 

799 return f"<{self.__class__.__name__} {self._method} {ascii_encodable_path} >" 

800 

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

802 return id(self) == id(other) 

803 

804 def __bool__(self) -> bool: 

805 return True 

806 

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

808 return 

809 

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

811 set_exception(self._payload, exc) 

812 

813 def _finish(self) -> None: 

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

815 return 

816 

817 # NOTE: Release file descriptors for the 

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

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

820 # NOTE: via HTTP POST request. 

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

822 if isinstance(file_field_object, FileField): 

823 file_field_object.file.close() 

824 

825 

826class Request(BaseRequest): 

827 

828 _match_info: Optional["UrlMappingMatchInfo"] = None 

829 

830 def clone( 

831 self, 

832 *, 

833 method: str | _SENTINEL = sentinel, 

834 rel_url: StrOrURL | _SENTINEL = sentinel, 

835 headers: LooseHeaders | _SENTINEL = sentinel, 

836 scheme: str | _SENTINEL = sentinel, 

837 host: str | _SENTINEL = sentinel, 

838 remote: str | _SENTINEL = sentinel, 

839 client_max_size: int | _SENTINEL = sentinel, 

840 ) -> "Request": 

841 ret = super().clone( 

842 method=method, 

843 rel_url=rel_url, 

844 headers=headers, 

845 scheme=scheme, 

846 host=host, 

847 remote=remote, 

848 client_max_size=client_max_size, 

849 ) 

850 new_ret = cast(Request, ret) 

851 new_ret._match_info = self._match_info 

852 return new_ret 

853 

854 @reify 

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

856 """Result of route resolving.""" 

857 match_info = self._match_info 

858 assert match_info is not None 

859 return match_info 

860 

861 @property 

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

863 """Application instance.""" 

864 match_info = self._match_info 

865 assert match_info is not None 

866 return match_info.current_app 

867 

868 @property 

869 def config_dict(self) -> ChainMapProxy: 

870 match_info = self._match_info 

871 assert match_info is not None 

872 lst = match_info.apps 

873 app = self.app 

874 idx = lst.index(app) 

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

876 return ChainMapProxy(sublist) 

877 

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

879 match_info = self._match_info 

880 if match_info is None: 

881 return 

882 for app in match_info._apps: 

883 if on_response_prepare := app.on_response_prepare: 

884 await on_response_prepare.send(self, response)