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 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
16from multidict import CIMultiDict, CIMultiDictProxy, MultiDict, MultiDictProxy
17from yarl import URL
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
55if sys.version_info >= (3, 11):
56 from typing import Self
57else:
58 Self = Any
60__all__ = ("BaseRequest", "FileField", "Request")
63if TYPE_CHECKING:
64 from .web_app import Application
65 from .web_protocol import RequestHandler
66 from .web_urldispatcher import UrlMappingMatchInfo
69_T = TypeVar("_T")
72@frozen_dataclass_decorator
73class FileField:
74 name: str
75 filename: str
76 file: io.BufferedReader
77 content_type: str
78 headers: CIMultiDictProxy[str]
81_TCHAR: Final[str] = string.digits + string.ascii_letters + r"!#$%&'*+.^_`|~-"
82# '-' at the end to prevent interpretation as range in a char class
84_TOKEN: Final[str] = rf"[{_TCHAR}]+"
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)
92_QUOTED_PAIR: Final[str] = r"\\[\t !-~]"
94_QUOTED_STRING: Final[str] = rf'"(?:{_QUOTED_PAIR}|{_QDTEXT})*"'
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}})?"
99_QUOTED_PAIR_REPLACE_RE: Final[Pattern[str]] = re.compile(r"\\([\t !-~])")
100# same pattern as _QUOTED_PAIR but contains a capture group
102_FORWARDED_PAIR_RE: Final[Pattern[str]] = re.compile(_FORWARDED_PAIR)
104############################################################
105# HTTP Request
106############################################################
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 }
118 _post: MultiDictProxy[str | bytes | FileField] | None = None
119 _read_bytes: bytes | None = None
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
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
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
170 self._transport_sslcontext = protocol.ssl_context
171 self._transport_peername = protocol.peername
173 if remote is not None:
174 self._cache["remote"] = remote
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.
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")
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 )
211 message = self._message._replace(**dct)
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
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 )
235 @property
236 def task(self) -> "asyncio.Task[None]":
237 return self._task
239 @property
240 def protocol(self) -> "RequestHandler[Self]":
241 return self._protocol
243 @property
244 def transport(self) -> asyncio.Transport | None:
245 return self._protocol.transport
247 @property
248 def writer(self) -> AbstractStreamWriter:
249 return self._payload_writer
251 @property
252 def client_max_size(self) -> int:
253 return self._client_max_size
255 @reify
256 def rel_url(self) -> URL:
257 return self._rel_url
259 # MutableMapping API
261 @overload # type: ignore[override]
262 def __getitem__(self, key: RequestKey[_T]) -> _T: ...
264 @overload
265 def __getitem__(self, key: str) -> Any: ...
267 def __getitem__(self, key: str | RequestKey[_T]) -> Any:
268 return self._state[key]
270 @overload # type: ignore[override]
271 def __setitem__(self, key: RequestKey[_T], value: _T) -> None: ...
273 @overload
274 def __setitem__(self, key: str, value: Any) -> None: ...
276 def __setitem__(self, key: str | RequestKey[_T], value: Any) -> None:
277 self._state[key] = value
279 def __delitem__(self, key: str | RequestKey[_T]) -> None:
280 del self._state[key]
282 def __len__(self) -> int:
283 return len(self._state)
285 def __iter__(self) -> Iterator[str | RequestKey[Any]]:
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) -> str | None:
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) -> datetime.datetime | None:
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) -> datetime.datetime | None:
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: str | None
518 ) -> tuple[ETag, ...] | None:
519 if not header_value:
520 return None
522 return tuple(cls._etag_values(header_value))
524 @reify
525 def if_match(self) -> tuple[ETag, ...] | None:
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) -> tuple[ETag, ...] | None:
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) -> datetime.datetime | None:
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 # 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})
565 @reify
566 def http_range(self) -> "slice[int, int, int]":
567 """The content of Range HTTP header.
569 Return a slice instance.
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")
581 end = int(end) if end else None
582 start = int(start) if start else None
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
589 if start is not None and end is not None:
590 # end is inclusive in range header, exclusive for slice
591 end += 1
593 if start >= end:
594 raise ValueError("start cannot be after end")
596 if start is end is None: # No valid range supplied
597 raise ValueError("No start or end of range specified")
599 return slice(start, end, 1)
601 @reify
602 def content(self) -> StreamReader:
603 """Return raw payload stream."""
604 return self._payload
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()
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
616 async def release(self) -> None:
617 """Release request.
619 Eat unread part of HTTP BODY if present.
620 """
621 while not self._payload.at_eof():
622 await self._payload.readany()
624 async def read(self) -> bytes:
625 """Read request body if present.
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
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()
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 )
671 return loads(body)
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 )
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
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
699 out: MultiDict[str | bytes | FileField] = MultiDict()
701 if content_type == "multipart/form-data":
702 multipart = await self.multipart()
703 max_size = self._client_max_size
705 size = 0
706 while (field := await multipart.next()) is not None:
707 field_ct = field.headers.get(hdrs.CONTENT_TYPE)
709 if isinstance(field, BodyPartReader):
710 if field.name is None:
711 raise ValueError("Multipart field missing name.")
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)
735 if field_ct is None:
736 field_ct = "application/octet-stream"
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)
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)
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 )
784 self._post = MultiDictProxy(out)
785 return self._post
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
793 return transport.get_extra_info(name, default)
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} >"
801 def __eq__(self, other: object) -> bool:
802 return id(self) == id(other)
804 def __bool__(self) -> bool:
805 return True
807 async def _prepare_hook(self, response: StreamResponse) -> None:
808 return
810 def _cancel(self, exc: BaseException) -> None:
811 set_exception(self._payload, exc)
813 def _finish(self) -> None:
814 if self._post is None or self.content_type != "multipart/form-data":
815 return
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()
826class Request(BaseRequest):
828 _match_info: Optional["UrlMappingMatchInfo"] = None
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
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
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
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)
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)