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
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
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
28from multidict import CIMultiDict, CIMultiDictProxy, MultiDict, MultiDictProxy
29from yarl import URL
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
65if sys.version_info >= (3, 11):
66 from typing import Self
67else:
68 Self = Any
70__all__ = ("BaseRequest", "FileField", "Request")
73if TYPE_CHECKING:
74 from .web_app import Application
75 from .web_protocol import RequestHandler
76 from .web_urldispatcher import UrlMappingMatchInfo
79@frozen_dataclass_decorator
80class FileField:
81 name: str
82 filename: str
83 file: io.BufferedReader
84 content_type: str
85 headers: CIMultiDictProxy[str]
88_TCHAR: Final[str] = string.digits + string.ascii_letters + r"!#$%&'*+.^_`|~-"
89# '-' at the end to prevent interpretation as range in a char class
91_TOKEN: Final[str] = rf"[{_TCHAR}]+"
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)
99_QUOTED_PAIR: Final[str] = r"\\[\t !-~]"
101_QUOTED_STRING: Final[str] = r'"(?:{quoted_pair}|{qdtext})*"'.format(
102 qdtext=_QDTEXT, quoted_pair=_QUOTED_PAIR
103)
105_FORWARDED_PAIR: Final[str] = (
106 r"({token})=({token}|{quoted_string})(:\d{{1,4}})?".format(
107 token=_TOKEN, quoted_string=_QUOTED_STRING
108 )
109)
111_QUOTED_PAIR_REPLACE_RE: Final[Pattern[str]] = re.compile(r"\\([\t !-~])")
112# same pattern as _QUOTED_PAIR but contains a capture group
114_FORWARDED_PAIR_RE: Final[Pattern[str]] = re.compile(_FORWARDED_PAIR)
116############################################################
117# HTTP Request
118############################################################
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 }
130 _post: Optional[MultiDictProxy[Union[str, bytes, FileField]]] = None
131 _read_bytes: Optional[bytes] = None
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
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
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
182 self._transport_sslcontext = protocol.ssl_context
183 self._transport_peername = protocol.peername
185 if remote is not None:
186 self._cache["remote"] = remote
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.
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")
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 )
223 message = self._message._replace(**dct)
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
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 )
247 @property
248 def task(self) -> "asyncio.Task[None]":
249 return self._task
251 @property
252 def protocol(self) -> "RequestHandler[Self]":
253 return self._protocol
255 @property
256 def transport(self) -> Optional[asyncio.Transport]:
257 return self._protocol.transport
259 @property
260 def writer(self) -> AbstractStreamWriter:
261 return self._payload_writer
263 @property
264 def client_max_size(self) -> int:
265 return self._client_max_size
267 @reify
268 def rel_url(self) -> URL:
269 return self._rel_url
271 # MutableMapping API
273 def __getitem__(self, key: str) -> Any:
274 return self._state[key]
276 def __setitem__(self, key: str, value: Any) -> None:
277 self._state[key] = value
279 def __delitem__(self, key: str) -> None:
280 del self._state[key]
282 def __len__(self) -> int:
283 return len(self._state)
285 def __iter__(self) -> Iterator[str]:
286 return iter(self._state)
288 ########
290 @reify
291 def secure(self) -> bool:
292 """A bool indicating if the request is handled with SSL."""
293 return self.scheme == "https"
295 @reify
296 def forwarded(self) -> Tuple[Mapping[str, str], ...]:
297 """A tuple containing all parsed Forwarded header(s).
299 Makes an effort to parse Forwarded headers as specified by RFC 7239:
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.
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)
356 @reify
357 def scheme(self) -> str:
358 """A string representing the scheme of the request.
360 Hostname is resolved in this order:
362 - overridden value by .clone(scheme=new_scheme) call.
363 - type of connection to peer: HTTPS if socket is SSL, HTTP otherwise.
365 'http' or 'https'.
366 """
367 if self._transport_sslcontext:
368 return "https"
369 else:
370 return "http"
372 @reify
373 def method(self) -> str:
374 """Read only property for getting HTTP method.
376 The value is upper-cased str like 'GET', 'POST', 'PUT' etc.
377 """
378 return self._method
380 @reify
381 def version(self) -> HttpVersion:
382 """Read only property for getting HTTP version of request.
384 Returns aiohttp.protocol.HttpVersion instance.
385 """
386 return self._version
388 @reify
389 def host(self) -> str:
390 """Hostname of the request.
392 Hostname is resolved in this order:
394 - overridden value by .clone(host=new_host) call.
395 - HOST HTTP header
396 - socket.getfqdn() value
398 For example, 'example.com' or 'localhost:8080'.
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()
407 @reify
408 def remote(self) -> Optional[str]:
409 """Remote IP of client initiated HTTP request.
411 The IP is resolved in this order:
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)
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)
429 @reify
430 def path(self) -> str:
431 """The URL including *PATH INFO* without the host or scheme.
433 E.g., ``/app/blog``
434 """
435 return self._rel_url.path
437 @reify
438 def path_qs(self) -> str:
439 """The URL including PATH_INFO and the query string.
441 E.g, /app/blog?id=10
442 """
443 return str(self._rel_url)
445 @reify
446 def raw_path(self) -> str:
447 """The URL including raw *PATH INFO* without the host or scheme.
449 Warning, the path is unquoted and may contains non valid URL characters
451 E.g., ``/my%2Fpath%7Cwith%21some%25strange%24characters``
452 """
453 return self._message.path
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
460 @reify
461 def query_string(self) -> str:
462 """The query string in the URL.
464 E.g., id=10
465 """
466 return self._rel_url.query_string
468 @reify
469 def headers(self) -> CIMultiDictProxy[str]:
470 """A case-insensitive multidict proxy with all headers."""
471 return self._headers
473 @reify
474 def raw_headers(self) -> RawHeaders:
475 """A sequence of pairs for all headers."""
476 return self._message.raw_headers
478 @reify
479 def if_modified_since(self) -> Optional[datetime.datetime]:
480 """The value of If-Modified-Since HTTP header, or None.
482 This header is represented as a `datetime` object.
483 """
484 return parse_http_date(self.headers.get(hdrs.IF_MODIFIED_SINCE))
486 @reify
487 def if_unmodified_since(self) -> Optional[datetime.datetime]:
488 """The value of If-Unmodified-Since HTTP header, or None.
490 This header is represented as a `datetime` object.
491 """
492 return parse_http_date(self.headers.get(hdrs.IF_UNMODIFIED_SINCE))
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
510 yield ETag(
511 is_weak=bool(is_weak),
512 value=value,
513 )
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
522 return tuple(cls._etag_values(header_value))
524 @reify
525 def if_match(self) -> Optional[Tuple[ETag, ...]]:
526 """The value of If-Match HTTP header, or None.
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))
532 @reify
533 def if_none_match(self) -> Optional[Tuple[ETag, ...]]:
534 """The value of If-None-Match HTTP header, or None.
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))
540 @reify
541 def if_range(self) -> Optional[datetime.datetime]:
542 """The value of If-Range HTTP header, or None.
544 This header is represented as a `datetime` object.
545 """
546 return parse_http_date(self.headers.get(hdrs.IF_RANGE))
548 @reify
549 def keep_alive(self) -> bool:
550 """Is keepalive enabled by client?"""
551 return not self._message.should_close
553 @reify
554 def cookies(self) -> Mapping[str, str]:
555 """Return request cookies.
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()})
563 @reify
564 def http_range(self) -> "slice[int, int, int]":
565 """The content of Range HTTP header.
567 Return a slice instance.
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")
579 end = int(end) if end else None
580 start = int(start) if start else None
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
587 if start is not None and end is not None:
588 # end is inclusive in range header, exclusive for slice
589 end += 1
591 if start >= end:
592 raise ValueError("start cannot be after end")
594 if start is end is None: # No valid range supplied
595 raise ValueError("No start or end of range specified")
597 return slice(start, end, 1)
599 @reify
600 def content(self) -> StreamReader:
601 """Return raw payload stream."""
602 return self._payload
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()
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
614 async def release(self) -> None:
615 """Release request.
617 Eat unread part of HTTP BODY if present.
618 """
619 while not self._payload.at_eof():
620 await self._payload.readany()
622 async def read(self) -> bytes:
623 """Read request body if present.
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
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()
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 )
669 return loads(body)
671 async def multipart(self) -> MultipartReader:
672 """Return async iterator to process BODY as multipart."""
673 return MultipartReader(self._headers, self._payload)
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
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
692 out: MultiDict[Union[str, bytes, FileField]] = MultiDict()
694 if content_type == "multipart/form-data":
695 multipart = await self.multipart()
696 max_size = self._client_max_size
698 field = await multipart.next()
699 while field is not None:
700 size = 0
701 field_ct = field.headers.get(hdrs.CONTENT_TYPE)
703 if isinstance(field, BodyPartReader):
704 assert field.name is not None
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)
728 if field_ct is None:
729 field_ct = "application/octet-stream"
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 )
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 )
771 self._post = MultiDictProxy(out)
772 return self._post
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
780 return transport.get_extra_info(name, default)
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 )
790 def __eq__(self, other: object) -> bool:
791 return id(self) == id(other)
793 def __bool__(self) -> bool:
794 return True
796 async def _prepare_hook(self, response: StreamResponse) -> None:
797 return
799 def _cancel(self, exc: BaseException) -> None:
800 set_exception(self._payload, exc)
802 def _finish(self) -> None:
803 if self._post is None or self.content_type != "multipart/form-data":
804 return
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()
815class Request(BaseRequest):
817 _match_info: Optional["UrlMappingMatchInfo"] = None
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
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
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
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)
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)