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

439 statements  

1import asyncio 

2import datetime 

3import io 

4import re 

5import string 

6import sys 

7import tempfile 

8import types 

9from collections.abc import Iterator, Mapping, MutableMapping 

10from re import Pattern 

11from types import MappingProxyType 

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

13from urllib.parse import parse_qsl 

14 

15from multidict import CIMultiDict, MultiDict, MultiDictProxy 

16from yarl import URL 

17 

18from . import hdrs 

19from ._cookie_helpers import parse_cookie_header 

20from .abc import AbstractStreamWriter 

21from .helpers import ( 

22 _SENTINEL, 

23 DEFAULT_CHUNK_SIZE, 

24 ETAG_ANY, 

25 LIST_QUOTED_ETAG_RE, 

26 ChainMapProxy, 

27 ETag, 

28 HeadersDictProxy, 

29 HeadersMixin, 

30 RequestKey, 

31 frozen_dataclass_decorator, 

32 is_expected_content_type, 

33 parse_http_date, 

34 reify, 

35 sentinel, 

36 set_exception, 

37) 

38from .http_parser import RawRequestMessage 

39from .http_writer import HttpVersion 

40from .multipart import BodyPartReader, MultipartReader 

41from .streams import EmptyStreamReader, StreamReader 

42from .typedefs import ( 

43 DEFAULT_JSON_DECODER, 

44 JSONDecoder, 

45 LooseHeaders, 

46 RawHeaders, 

47 StrOrURL, 

48) 

49from .web_exceptions import ( 

50 HTTPBadRequest, 

51 HTTPRequestEntityTooLarge, 

52 HTTPUnsupportedMediaType, 

53) 

54from .web_response import StreamResponse 

55 

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

57 from typing import Self 

58else: 

59 Self = Any 

60 

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

62 

63 

64if TYPE_CHECKING: 

65 from .web_app import Application 

66 from .web_protocol import RequestHandler 

67 from .web_urldispatcher import UrlMappingMatchInfo 

68 

69 

70_T = TypeVar("_T") 

71 

72 

73@frozen_dataclass_decorator 

74class FileField: 

75 name: str 

76 filename: str 

77 file: io.BufferedReader 

78 content_type: str 

79 headers: HeadersDictProxy 

80 

81 

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

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

84 

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

86 

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

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

89) 

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

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

92 

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

94_FORWARDED_PAIR: Final[str] = ( 

95 rf'[ \t]*({_TOKEN})=({_TOKEN}|".*")(:\d{{1,4}})?[ \t]*(?:\Z|;)' 

96) 

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

98 

99############################################################ 

100# HTTP Request 

101############################################################ 

102 

103 

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

105 POST_METHODS = { 

106 hdrs.METH_PATCH, 

107 hdrs.METH_POST, 

108 hdrs.METH_PUT, 

109 hdrs.METH_TRACE, 

110 hdrs.METH_DELETE, 

111 } 

112 

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

114 _read_bytes: bytes | None = None 

115 

116 def __init__( 

117 self, 

118 message: RawRequestMessage, 

119 payload: StreamReader, 

120 protocol: "RequestHandler[Self]", 

121 payload_writer: AbstractStreamWriter, 

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

123 loop: asyncio.AbstractEventLoop, 

124 *, 

125 client_max_size: int = 1024**2, 

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

127 scheme: str | None = None, 

128 host: str | None = None, 

129 remote: str | None = None, 

130 ) -> None: 

131 self._message = message 

132 self._protocol = protocol 

133 self._payload_writer = payload_writer 

134 

135 self._payload = payload 

136 self._headers: HeadersDictProxy = message.headers 

137 self._method = message.method 

138 self._version = message.version 

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

140 url = message.url 

141 if url.absolute: 

142 if scheme is not None: 

143 url = url.with_scheme(scheme) 

144 if host is not None: 

145 url = url.with_host(host) 

146 # absolute URL is given, 

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

148 # all other properties should be good 

149 self._cache["url"] = url 

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

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

152 self._rel_url = url.relative() 

153 else: 

154 self._rel_url = url 

155 if scheme is not None: 

156 self._cache["scheme"] = scheme 

157 if host is not None: 

158 self._cache["host"] = host 

159 

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

161 self._task = task 

162 self._client_max_size = client_max_size 

163 self._loop = loop 

164 

165 self._transport_sslcontext = protocol.ssl_context 

166 self._transport_peername = protocol.peername 

167 self._transport_sockname = protocol.sockname 

168 

169 if remote is not None: 

170 self._cache["remote"] = remote 

171 

172 def clone( 

173 self, 

174 *, 

175 method: str | _SENTINEL = sentinel, 

176 rel_url: StrOrURL | _SENTINEL = sentinel, 

177 headers: LooseHeaders | _SENTINEL = sentinel, 

178 scheme: str | _SENTINEL = sentinel, 

179 host: str | _SENTINEL = sentinel, 

180 remote: str | _SENTINEL = sentinel, 

181 client_max_size: int | _SENTINEL = sentinel, 

182 ) -> "BaseRequest": 

183 """Clone itself with replacement some attributes. 

184 

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

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

187 will reuse the one from the current request object. 

188 """ 

189 if self._read_bytes: 

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

191 

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

193 if method is not sentinel: 

194 dct["method"] = method 

195 if rel_url is not sentinel: 

196 new_url: URL = URL(rel_url) 

197 dct["url"] = new_url 

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

199 if headers is not sentinel: 

200 # a copy semantic 

201 new_headers = HeadersDictProxy(CIMultiDict(headers)) 

202 dct["headers"] = new_headers 

203 dct["raw_headers"] = tuple( 

204 (k.encode("utf-8"), v.encode("utf-8")) 

205 for k, v in new_headers._md.items() 

206 ) 

207 

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

209 

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

211 if scheme is not sentinel: 

212 kwargs["scheme"] = scheme 

213 if host is not sentinel: 

214 kwargs["host"] = host 

215 if remote is not sentinel: 

216 kwargs["remote"] = remote 

217 if client_max_size is sentinel: 

218 client_max_size = self._client_max_size 

219 

220 return self.__class__( 

221 message, 

222 self._payload, 

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

224 self._payload_writer, 

225 self._task, 

226 self._loop, 

227 client_max_size=client_max_size, 

228 state=self._state.copy(), 

229 **kwargs, 

230 ) 

231 

232 @property 

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

234 return self._task 

235 

236 @property 

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

238 return self._protocol 

239 

240 @property 

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

242 return self._protocol.transport 

243 

244 @property 

245 def writer(self) -> AbstractStreamWriter: 

246 return self._payload_writer 

247 

248 @property 

249 def client_max_size(self) -> int: 

250 return self._client_max_size 

251 

252 @reify 

253 def rel_url(self) -> URL: 

254 return self._rel_url 

255 

256 # MutableMapping API 

257 

258 @overload # type: ignore[override] 

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

260 

261 @overload 

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

263 

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

265 return self._state[key] 

266 

267 @overload # type: ignore[override] 

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

269 

270 @overload 

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

272 

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

274 self._state[key] = value 

275 

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

277 del self._state[key] 

278 

279 def __len__(self) -> int: 

280 return len(self._state) 

281 

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

283 return iter(self._state) 

284 

285 ######## 

286 

287 @reify 

288 def secure(self) -> bool: 

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

290 return self.scheme == "https" 

291 

292 @reify 

293 def forwarded(self) -> tuple[Mapping[str, str], ...]: 

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

295 

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

297 

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

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

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

301 subsequent item corresponds to those added by later proxies. 

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

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

304 - It un-escapes found escape sequences. 

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

306 6. 

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

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

309 

310 Returns a tuple containing one or more immutable dicts 

311 """ 

312 elems = [] 

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

314 pos = 0 

315 elem: dict[str, str] = {} 

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

317 while 0 <= pos < len(field_value): 

318 match = _FORWARDED_PAIR_RE.match(field_value, pos) 

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

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

321 if value[0] == value[-1] == '"': 

322 value = value[1:-1] 

323 if port: 

324 value += port 

325 elem[name.lower()] = value 

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

327 elif not field_value[pos : field_value.find(";", pos)].strip(" \t"): 

328 # Empty value 

329 pos = field_value.find(";", pos) + 1 

330 else: 

331 # bad syntax here, skip to next field value 

332 break 

333 return tuple(elems) 

334 

335 @reify 

336 def scheme(self) -> str: 

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

338 

339 Hostname is resolved in this order: 

340 

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

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

343 

344 'http' or 'https'. 

345 """ 

346 if self._transport_sslcontext: 

347 return "https" 

348 else: 

349 return "http" 

350 

351 @reify 

352 def method(self) -> str: 

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

354 

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

356 """ 

357 return self._method 

358 

359 @reify 

360 def version(self) -> HttpVersion: 

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

362 

363 Returns aiohttp.protocol.HttpVersion instance. 

364 """ 

365 return self._version 

366 

367 @reify 

368 def host(self) -> str: 

369 """Hostname of the request. 

370 

371 Hostname is resolved in this order: 

372 

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

374 - HOST HTTP header 

375 - local socket address the request arrived on 

376 (transport ``sockname``) 

377 - empty string if no transport information is available 

378 

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

380 

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

382 """ 

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

384 if host is not None: 

385 return host 

386 sockname = self._transport_sockname 

387 if sockname is None: 

388 return "" 

389 if isinstance(sockname, tuple): 

390 # AF_INET6 returns a 4-tuple (host, port, flowinfo, scopeid); 

391 # bracket the bare address so it matches the Host-header shape 

392 # and is a valid URL authority component. 

393 if len(sockname) == 4: 

394 return f"[{sockname[0]}]" 

395 return str(sockname[0]) 

396 return str(sockname) 

397 

398 @reify 

399 def remote(self) -> str | None: 

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

401 

402 The IP is resolved in this order: 

403 

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

405 - peername of opened socket 

406 """ 

407 if self._transport_peername is None: 

408 return None 

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

410 return str(self._transport_peername[0]) 

411 return str(self._transport_peername) 

412 

413 @reify 

414 def url(self) -> URL: 

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

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

417 # and we want yarl to parse it correctly 

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

419 

420 @reify 

421 def path(self) -> str: 

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

423 

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

425 """ 

426 return self._rel_url.path 

427 

428 @reify 

429 def path_qs(self) -> str: 

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

431 

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

433 """ 

434 return str(self._rel_url) 

435 

436 @reify 

437 def raw_path(self) -> str: 

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

439 

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

441 

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

443 """ 

444 return self._message.path 

445 

446 @reify 

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

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

449 return self._rel_url.query 

450 

451 @reify 

452 def query_string(self) -> str: 

453 """The query string in the URL. 

454 

455 E.g., id=10 

456 """ 

457 return self._rel_url.query_string 

458 

459 @reify 

460 def headers(self) -> HeadersDictProxy: 

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

462 return self._headers 

463 

464 @reify 

465 def raw_headers(self) -> RawHeaders: 

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

467 return self._message.raw_headers 

468 

469 @reify 

470 def if_modified_since(self) -> datetime.datetime | None: 

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

472 

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

474 """ 

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

476 

477 @reify 

478 def if_unmodified_since(self) -> datetime.datetime | None: 

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

480 

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

482 """ 

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

484 

485 @staticmethod 

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

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

488 if etag_header == ETAG_ANY: 

489 yield ETag( 

490 is_weak=False, 

491 value=ETAG_ANY, 

492 ) 

493 else: 

494 for match in LIST_QUOTED_ETAG_RE.finditer(etag_header): 

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

496 # Any symbol captured by 4th group means 

497 # that the following sequence is invalid. 

498 if garbage: 

499 break 

500 

501 yield ETag( 

502 is_weak=bool(is_weak), 

503 value=value, 

504 ) 

505 

506 @classmethod 

507 def _if_match_or_none_impl( 

508 cls, header_value: str | None 

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

510 if not header_value: 

511 return None 

512 

513 return tuple(cls._etag_values(header_value)) 

514 

515 @reify 

516 def if_match(self) -> tuple[ETag, ...] | None: 

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

518 

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

520 """ 

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

522 

523 @reify 

524 def if_none_match(self) -> tuple[ETag, ...] | None: 

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

526 

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

528 """ 

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

530 

531 @reify 

532 def if_range(self) -> datetime.datetime | None: 

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

534 

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

536 """ 

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

538 

539 @reify 

540 def keep_alive(self) -> bool: 

541 """Is keepalive enabled by client?""" 

542 return not self._message.should_close 

543 

544 @reify 

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

546 """Return request cookies. 

547 

548 A read-only dictionary-like object. 

549 """ 

550 # Use parse_cookie_header for RFC 6265 compliant Cookie header parsing 

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

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

553 # Extract values from Morsel objects 

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

555 

556 @reify 

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

558 """The content of Range HTTP header. 

559 

560 Return a slice instance. 

561 

562 """ 

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

564 start, end = None, None 

565 if rng is not None: 

566 try: 

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

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

569 except IndexError: # pattern was not found in header 

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

571 

572 end = int(end) if end else None 

573 start = int(start) if start else None 

574 

575 if start is None and end is not None: 

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

577 start = -end 

578 end = None 

579 

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

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

582 end += 1 

583 

584 if start >= end: 

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

586 

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

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

589 

590 return slice(start, end, 1) 

591 

592 @reify 

593 def content(self) -> StreamReader: 

594 """Return raw payload stream.""" 

595 return self._payload 

596 

597 @property 

598 def can_read_body(self) -> bool: 

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

600 return not self._payload.at_eof() 

601 

602 @reify 

603 def body_exists(self) -> bool: 

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

605 return type(self._payload) is not EmptyStreamReader 

606 

607 async def release(self) -> None: 

608 """Release request. 

609 

610 Eat unread part of HTTP BODY if present. 

611 """ 

612 while not self._payload.at_eof(): 

613 await self._payload.readany() 

614 

615 async def read(self) -> bytes: 

616 """Read request body if present. 

617 

618 Returns bytes object with full request content. 

619 """ 

620 if self._read_bytes is None: 

621 # Raise the buffer limits so compressed payloads decompress in 

622 # larger chunks instead of many small pause/resume cycles. 

623 if self._client_max_size: 

624 self._payload.set_read_chunk_size(self._client_max_size) 

625 body = bytearray() 

626 while True: 

627 chunk = await self._payload.readany() 

628 body.extend(chunk) 

629 if self._client_max_size: 

630 body_size = len(body) 

631 if body_size > self._client_max_size: 

632 raise HTTPRequestEntityTooLarge(self._client_max_size) 

633 if not chunk: 

634 break 

635 self._read_bytes = bytes(body) 

636 return self._read_bytes 

637 

638 async def text(self) -> str: 

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

640 bytes_body = await self.read() 

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

642 try: 

643 return bytes_body.decode(encoding) 

644 except LookupError: 

645 raise HTTPUnsupportedMediaType() 

646 

647 async def json( 

648 self, 

649 *, 

650 loads: JSONDecoder = DEFAULT_JSON_DECODER, 

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

652 ) -> Any: 

653 """Return BODY as JSON.""" 

654 body = await self.text() 

655 if content_type: 

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

657 raise HTTPBadRequest( 

658 text=( 

659 "Attempt to decode JSON with " 

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

661 ) 

662 ) 

663 

664 return loads(body) 

665 

666 async def multipart(self) -> MultipartReader: 

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

668 return MultipartReader( 

669 self._headers, 

670 self._payload, 

671 client_max_size=self._client_max_size, 

672 max_field_size=self._protocol.max_field_size, 

673 max_headers=self._protocol.max_headers, 

674 max_size_error_cls=HTTPRequestEntityTooLarge, 

675 ) 

676 

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

678 """Return POST parameters.""" 

679 if self._post is not None: 

680 return self._post 

681 if self._method not in self.POST_METHODS: 

682 self._post = MultiDictProxy(MultiDict()) 

683 return self._post 

684 

685 content_type = self.content_type 

686 if content_type not in ( 

687 "", 

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

689 "multipart/form-data", 

690 ): 

691 self._post = MultiDictProxy(MultiDict()) 

692 return self._post 

693 

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

695 

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

697 multipart = await self.multipart() 

698 max_size = self._client_max_size 

699 

700 size = 0 

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

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

703 

704 if isinstance(field, BodyPartReader): 

705 if field.name is None: 

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

707 

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

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

710 # present. 

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

712 if field.filename: 

713 # store file in temp file 

714 tmp = await self._loop.run_in_executor( 

715 None, tempfile.TemporaryFile 

716 ) 

717 while chunk := await field.read_chunk(size=DEFAULT_CHUNK_SIZE): 

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

719 await self._loop.run_in_executor( 

720 None, tmp.write, decoded_chunk 

721 ) 

722 size += len(decoded_chunk) 

723 if 0 < max_size < size: 

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

725 raise HTTPRequestEntityTooLarge(max_size) 

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 raw_data = bytearray() 

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

743 size += len(chunk) 

744 if 0 < max_size < size: 

745 raise HTTPRequestEntityTooLarge(max_size) 

746 raw_data.extend(chunk) 

747 

748 value = bytearray() 

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

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

751 value.extend(d) 

752 

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

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

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

756 else: 

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

758 else: 

759 raise ValueError( 

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

761 ) 

762 else: 

763 data = await self.read() 

764 if data: 

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

766 bytes_query = data.rstrip() 

767 try: 

768 query = bytes_query.decode(charset) 

769 except LookupError: 

770 raise HTTPUnsupportedMediaType() 

771 out.extend( 

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

773 ) 

774 

775 self._post = MultiDictProxy(out) 

776 return self._post 

777 

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

779 """Extra info from protocol transport""" 

780 transport = self._protocol.transport 

781 if transport is None: 

782 return default 

783 

784 return transport.get_extra_info(name, default) 

785 

786 def __repr__(self) -> str: 

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

788 "ascii" 

789 ) 

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

791 

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

793 return id(self) == id(other) 

794 

795 def __bool__(self) -> bool: 

796 return True 

797 

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

799 return 

800 

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

802 set_exception(self._payload, exc) 

803 

804 def _finish(self) -> None: 

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

806 return 

807 

808 # NOTE: Release file descriptors for the 

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

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

811 # NOTE: via HTTP POST request. 

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

813 if isinstance(file_field_object, FileField): 

814 file_field_object.file.close() 

815 

816 

817class Request(BaseRequest): 

818 

819 _match_info: Optional["UrlMappingMatchInfo"] = None 

820 

821 def clone( 

822 self, 

823 *, 

824 method: str | _SENTINEL = sentinel, 

825 rel_url: StrOrURL | _SENTINEL = sentinel, 

826 headers: LooseHeaders | _SENTINEL = sentinel, 

827 scheme: str | _SENTINEL = sentinel, 

828 host: str | _SENTINEL = sentinel, 

829 remote: str | _SENTINEL = sentinel, 

830 client_max_size: int | _SENTINEL = sentinel, 

831 ) -> "Request": 

832 ret = super().clone( 

833 method=method, 

834 rel_url=rel_url, 

835 headers=headers, 

836 scheme=scheme, 

837 host=host, 

838 remote=remote, 

839 client_max_size=client_max_size, 

840 ) 

841 new_ret = cast(Request, ret) 

842 new_ret._match_info = self._match_info 

843 return new_ret 

844 

845 @reify 

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

847 """Result of route resolving.""" 

848 match_info = self._match_info 

849 assert match_info is not None 

850 return match_info 

851 

852 @property 

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

854 """Application instance.""" 

855 match_info = self._match_info 

856 assert match_info is not None 

857 return match_info.current_app 

858 

859 @property 

860 def config_dict(self) -> ChainMapProxy: 

861 match_info = self._match_info 

862 assert match_info is not None 

863 lst = match_info.apps 

864 app = self.app 

865 idx = lst.index(app) 

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

867 return ChainMapProxy(sublist) 

868 

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

870 match_info = self._match_info 

871 if match_info is None: 

872 return 

873 for app in match_info._apps: 

874 if on_response_prepare := app.on_response_prepare: 

875 await on_response_prepare.send(self, response)