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
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 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
15from multidict import CIMultiDict, MultiDict, MultiDictProxy
16from yarl import URL
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
56if sys.version_info >= (3, 11):
57 from typing import Self
58else:
59 Self = Any
61__all__ = ("BaseRequest", "FileField", "Request")
64if TYPE_CHECKING:
65 from .web_app import Application
66 from .web_protocol import RequestHandler
67 from .web_urldispatcher import UrlMappingMatchInfo
70_T = TypeVar("_T")
73@frozen_dataclass_decorator
74class FileField:
75 name: str
76 filename: str
77 file: io.BufferedReader
78 content_type: str
79 headers: HeadersDictProxy
82_TCHAR: Final[str] = string.digits + string.ascii_letters + r"!#$%&'*+.^_`|~-"
83# '-' at the end to prevent interpretation as range in a char class
85_TOKEN: Final[str] = rf"[{_TCHAR}]+"
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)
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)
99############################################################
100# HTTP Request
101############################################################
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 }
113 _post: MultiDictProxy[str | bytes | FileField] | None = None
114 _read_bytes: bytes | None = None
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
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
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
165 self._transport_sslcontext = protocol.ssl_context
166 self._transport_peername = protocol.peername
167 self._transport_sockname = protocol.sockname
169 if remote is not None:
170 self._cache["remote"] = remote
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.
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")
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 )
208 message = self._message._replace(**dct)
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
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 )
232 @property
233 def task(self) -> "asyncio.Task[None]":
234 return self._task
236 @property
237 def protocol(self) -> "RequestHandler[Self]":
238 return self._protocol
240 @property
241 def transport(self) -> asyncio.Transport | None:
242 return self._protocol.transport
244 @property
245 def writer(self) -> AbstractStreamWriter:
246 return self._payload_writer
248 @property
249 def client_max_size(self) -> int:
250 return self._client_max_size
252 @reify
253 def rel_url(self) -> URL:
254 return self._rel_url
256 # MutableMapping API
258 @overload # type: ignore[override]
259 def __getitem__(self, key: RequestKey[_T]) -> _T: ...
261 @overload
262 def __getitem__(self, key: str) -> Any: ...
264 def __getitem__(self, key: str | RequestKey[_T]) -> Any:
265 return self._state[key]
267 @overload # type: ignore[override]
268 def __setitem__(self, key: RequestKey[_T], value: _T) -> None: ...
270 @overload
271 def __setitem__(self, key: str, value: Any) -> None: ...
273 def __setitem__(self, key: str | RequestKey[_T], value: Any) -> None:
274 self._state[key] = value
276 def __delitem__(self, key: str | RequestKey[_T]) -> None:
277 del self._state[key]
279 def __len__(self) -> int:
280 return len(self._state)
282 def __iter__(self) -> Iterator[str | RequestKey[Any]]:
283 return iter(self._state)
285 ########
287 @reify
288 def secure(self) -> bool:
289 """A bool indicating if the request is handled with SSL."""
290 return self.scheme == "https"
292 @reify
293 def forwarded(self) -> tuple[Mapping[str, str], ...]:
294 """A tuple containing all parsed Forwarded header(s).
296 Makes an effort to parse Forwarded headers as specified by RFC 7239:
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.
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)
335 @reify
336 def scheme(self) -> str:
337 """A string representing the scheme of the request.
339 Hostname is resolved in this order:
341 - overridden value by .clone(scheme=new_scheme) call.
342 - type of connection to peer: HTTPS if socket is SSL, HTTP otherwise.
344 'http' or 'https'.
345 """
346 if self._transport_sslcontext:
347 return "https"
348 else:
349 return "http"
351 @reify
352 def method(self) -> str:
353 """Read only property for getting HTTP method.
355 The value is upper-cased str like 'GET', 'POST', 'PUT' etc.
356 """
357 return self._method
359 @reify
360 def version(self) -> HttpVersion:
361 """Read only property for getting HTTP version of request.
363 Returns aiohttp.protocol.HttpVersion instance.
364 """
365 return self._version
367 @reify
368 def host(self) -> str:
369 """Hostname of the request.
371 Hostname is resolved in this order:
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
379 For example, 'example.com' or 'localhost:8080'.
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)
398 @reify
399 def remote(self) -> str | None:
400 """Remote IP of client initiated HTTP request.
402 The IP is resolved in this order:
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)
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)
420 @reify
421 def path(self) -> str:
422 """The URL including *PATH INFO* without the host or scheme.
424 E.g., ``/app/blog``
425 """
426 return self._rel_url.path
428 @reify
429 def path_qs(self) -> str:
430 """The URL including PATH_INFO and the query string.
432 E.g, /app/blog?id=10
433 """
434 return str(self._rel_url)
436 @reify
437 def raw_path(self) -> str:
438 """The URL including raw *PATH INFO* without the host or scheme.
440 Warning, the path is unquoted and may contains non valid URL characters
442 E.g., ``/my%2Fpath%7Cwith%21some%25strange%24characters``
443 """
444 return self._message.path
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
451 @reify
452 def query_string(self) -> str:
453 """The query string in the URL.
455 E.g., id=10
456 """
457 return self._rel_url.query_string
459 @reify
460 def headers(self) -> HeadersDictProxy:
461 """A case-insensitive multidict proxy with all headers."""
462 return self._headers
464 @reify
465 def raw_headers(self) -> RawHeaders:
466 """A sequence of pairs for all headers."""
467 return self._message.raw_headers
469 @reify
470 def if_modified_since(self) -> datetime.datetime | None:
471 """The value of If-Modified-Since HTTP header, or None.
473 This header is represented as a `datetime` object.
474 """
475 return parse_http_date(self.headers.get(hdrs.IF_MODIFIED_SINCE))
477 @reify
478 def if_unmodified_since(self) -> datetime.datetime | None:
479 """The value of If-Unmodified-Since HTTP header, or None.
481 This header is represented as a `datetime` object.
482 """
483 return parse_http_date(self.headers.get(hdrs.IF_UNMODIFIED_SINCE))
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
501 yield ETag(
502 is_weak=bool(is_weak),
503 value=value,
504 )
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
513 return tuple(cls._etag_values(header_value))
515 @reify
516 def if_match(self) -> tuple[ETag, ...] | None:
517 """The value of If-Match HTTP header, or None.
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))
523 @reify
524 def if_none_match(self) -> tuple[ETag, ...] | None:
525 """The value of If-None-Match HTTP header, or None.
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))
531 @reify
532 def if_range(self) -> datetime.datetime | None:
533 """The value of If-Range HTTP header, or None.
535 This header is represented as a `datetime` object.
536 """
537 return parse_http_date(self.headers.get(hdrs.IF_RANGE))
539 @reify
540 def keep_alive(self) -> bool:
541 """Is keepalive enabled by client?"""
542 return not self._message.should_close
544 @reify
545 def cookies(self) -> Mapping[str, str]:
546 """Return request cookies.
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})
556 @reify
557 def http_range(self) -> "slice[int, int, int]":
558 """The content of Range HTTP header.
560 Return a slice instance.
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")
572 end = int(end) if end else None
573 start = int(start) if start else None
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
580 if start is not None and end is not None:
581 # end is inclusive in range header, exclusive for slice
582 end += 1
584 if start >= end:
585 raise ValueError("start cannot be after end")
587 if start is end is None: # No valid range supplied
588 raise ValueError("No start or end of range specified")
590 return slice(start, end, 1)
592 @reify
593 def content(self) -> StreamReader:
594 """Return raw payload stream."""
595 return self._payload
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()
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
607 async def release(self) -> None:
608 """Release request.
610 Eat unread part of HTTP BODY if present.
611 """
612 while not self._payload.at_eof():
613 await self._payload.readany()
615 async def read(self) -> bytes:
616 """Read request body if present.
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
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()
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 )
664 return loads(body)
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 )
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
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
694 out: MultiDict[str | bytes | FileField] = MultiDict()
696 if content_type == "multipart/form-data":
697 multipart = await self.multipart()
698 max_size = self._client_max_size
700 size = 0
701 while (field := await multipart.next()) is not None:
702 field_ct = field.headers.get(hdrs.CONTENT_TYPE)
704 if isinstance(field, BodyPartReader):
705 if field.name is None:
706 raise ValueError("Multipart field missing name.")
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)
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 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)
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)
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 )
775 self._post = MultiDictProxy(out)
776 return self._post
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
784 return transport.get_extra_info(name, default)
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} >"
792 def __eq__(self, other: object) -> bool:
793 return id(self) == id(other)
795 def __bool__(self) -> bool:
796 return True
798 async def _prepare_hook(self, response: StreamResponse) -> None:
799 return
801 def _cancel(self, exc: BaseException) -> None:
802 set_exception(self._payload, exc)
804 def _finish(self) -> None:
805 if self._post is None or self.content_type != "multipart/form-data":
806 return
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()
817class Request(BaseRequest):
819 _match_info: Optional["UrlMappingMatchInfo"] = None
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
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
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
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)
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)