Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/django/core/handlers/asgi.py: 20%
158 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
1import logging
2import sys
3import tempfile
4import traceback
6from asgiref.sync import ThreadSensitiveContext, sync_to_async
8from django.conf import settings
9from django.core import signals
10from django.core.exceptions import RequestAborted, RequestDataTooBig
11from django.core.handlers import base
12from django.http import (
13 FileResponse,
14 HttpRequest,
15 HttpResponse,
16 HttpResponseBadRequest,
17 HttpResponseServerError,
18 QueryDict,
19 parse_cookie,
20)
21from django.urls import set_script_prefix
22from django.utils.functional import cached_property
24logger = logging.getLogger("django.request")
27class ASGIRequest(HttpRequest):
28 """
29 Custom request subclass that decodes from an ASGI-standard request dict
30 and wraps request body handling.
31 """
33 # Number of seconds until a Request gives up on trying to read a request
34 # body and aborts.
35 body_receive_timeout = 60
37 def __init__(self, scope, body_file):
38 self.scope = scope
39 self._post_parse_error = False
40 self._read_started = False
41 self.resolver_match = None
42 self.script_name = self.scope.get("root_path", "")
43 if self.script_name and scope["path"].startswith(self.script_name):
44 # TODO: Better is-prefix checking, slash handling?
45 self.path_info = scope["path"][len(self.script_name) :]
46 else:
47 self.path_info = scope["path"]
48 # The Django path is different from ASGI scope path args, it should
49 # combine with script name.
50 if self.script_name:
51 self.path = "%s/%s" % (
52 self.script_name.rstrip("/"),
53 self.path_info.replace("/", "", 1),
54 )
55 else:
56 self.path = scope["path"]
57 # HTTP basics.
58 self.method = self.scope["method"].upper()
59 # Ensure query string is encoded correctly.
60 query_string = self.scope.get("query_string", "")
61 if isinstance(query_string, bytes):
62 query_string = query_string.decode()
63 self.META = {
64 "REQUEST_METHOD": self.method,
65 "QUERY_STRING": query_string,
66 "SCRIPT_NAME": self.script_name,
67 "PATH_INFO": self.path_info,
68 # WSGI-expecting code will need these for a while
69 "wsgi.multithread": True,
70 "wsgi.multiprocess": True,
71 }
72 if self.scope.get("client"):
73 self.META["REMOTE_ADDR"] = self.scope["client"][0]
74 self.META["REMOTE_HOST"] = self.META["REMOTE_ADDR"]
75 self.META["REMOTE_PORT"] = self.scope["client"][1]
76 if self.scope.get("server"):
77 self.META["SERVER_NAME"] = self.scope["server"][0]
78 self.META["SERVER_PORT"] = str(self.scope["server"][1])
79 else:
80 self.META["SERVER_NAME"] = "unknown"
81 self.META["SERVER_PORT"] = "0"
82 # Headers go into META.
83 for name, value in self.scope.get("headers", []):
84 name = name.decode("latin1")
85 if name == "content-length":
86 corrected_name = "CONTENT_LENGTH"
87 elif name == "content-type":
88 corrected_name = "CONTENT_TYPE"
89 else:
90 corrected_name = "HTTP_%s" % name.upper().replace("-", "_")
91 # HTTP/2 say only ASCII chars are allowed in headers, but decode
92 # latin1 just in case.
93 value = value.decode("latin1")
94 if corrected_name in self.META:
95 value = self.META[corrected_name] + "," + value
96 self.META[corrected_name] = value
97 # Pull out request encoding, if provided.
98 self._set_content_type_params(self.META)
99 # Directly assign the body file to be our stream.
100 self._stream = body_file
101 # Other bits.
102 self.resolver_match = None
104 @cached_property
105 def GET(self):
106 return QueryDict(self.META["QUERY_STRING"])
108 def _get_scheme(self):
109 return self.scope.get("scheme") or super()._get_scheme()
111 def _get_post(self):
112 if not hasattr(self, "_post"):
113 self._load_post_and_files()
114 return self._post
116 def _set_post(self, post):
117 self._post = post
119 def _get_files(self):
120 if not hasattr(self, "_files"):
121 self._load_post_and_files()
122 return self._files
124 POST = property(_get_post, _set_post)
125 FILES = property(_get_files)
127 @cached_property
128 def COOKIES(self):
129 return parse_cookie(self.META.get("HTTP_COOKIE", ""))
132class ASGIHandler(base.BaseHandler):
133 """Handler for ASGI requests."""
135 request_class = ASGIRequest
136 # Size to chunk response bodies into for multiple response messages.
137 chunk_size = 2**16
139 def __init__(self):
140 super().__init__()
141 self.load_middleware(is_async=True)
143 async def __call__(self, scope, receive, send):
144 """
145 Async entrypoint - parses the request and hands off to get_response.
146 """
147 # Serve only HTTP connections.
148 # FIXME: Allow to override this.
149 if scope["type"] != "http":
150 raise ValueError(
151 "Django can only handle ASGI/HTTP connections, not %s." % scope["type"]
152 )
154 async with ThreadSensitiveContext():
155 await self.handle(scope, receive, send)
157 async def handle(self, scope, receive, send):
158 """
159 Handles the ASGI request. Called via the __call__ method.
160 """
161 # Receive the HTTP request body as a stream object.
162 try:
163 body_file = await self.read_body(receive)
164 except RequestAborted:
165 return
166 # Request is complete and can be served.
167 set_script_prefix(self.get_script_prefix(scope))
168 await sync_to_async(signals.request_started.send, thread_sensitive=True)(
169 sender=self.__class__, scope=scope
170 )
171 # Get the request and check for basic issues.
172 request, error_response = self.create_request(scope, body_file)
173 if request is None:
174 await self.send_response(error_response, send)
175 return
176 # Get the response, using the async mode of BaseHandler.
177 response = await self.get_response_async(request)
178 response._handler_class = self.__class__
179 # Increase chunk size on file responses (ASGI servers handles low-level
180 # chunking).
181 if isinstance(response, FileResponse):
182 response.block_size = self.chunk_size
183 # Send the response.
184 await self.send_response(response, send)
186 async def read_body(self, receive):
187 """Reads an HTTP body from an ASGI connection."""
188 # Use the tempfile that auto rolls-over to a disk file as it fills up.
189 body_file = tempfile.SpooledTemporaryFile(
190 max_size=settings.FILE_UPLOAD_MAX_MEMORY_SIZE, mode="w+b"
191 )
192 while True:
193 message = await receive()
194 if message["type"] == "http.disconnect":
195 # Early client disconnect.
196 raise RequestAborted()
197 # Add a body chunk from the message, if provided.
198 if "body" in message:
199 body_file.write(message["body"])
200 # Quit out if that's the end.
201 if not message.get("more_body", False):
202 break
203 body_file.seek(0)
204 return body_file
206 def create_request(self, scope, body_file):
207 """
208 Create the Request object and returns either (request, None) or
209 (None, response) if there is an error response.
210 """
211 try:
212 return self.request_class(scope, body_file), None
213 except UnicodeDecodeError:
214 logger.warning(
215 "Bad Request (UnicodeDecodeError)",
216 exc_info=sys.exc_info(),
217 extra={"status_code": 400},
218 )
219 return None, HttpResponseBadRequest()
220 except RequestDataTooBig:
221 return None, HttpResponse("413 Payload too large", status=413)
223 def handle_uncaught_exception(self, request, resolver, exc_info):
224 """Last-chance handler for exceptions."""
225 # There's no WSGI server to catch the exception further up
226 # if this fails, so translate it into a plain text response.
227 try:
228 return super().handle_uncaught_exception(request, resolver, exc_info)
229 except Exception:
230 return HttpResponseServerError(
231 traceback.format_exc() if settings.DEBUG else "Internal Server Error",
232 content_type="text/plain",
233 )
235 async def send_response(self, response, send):
236 """Encode and send a response out over ASGI."""
237 # Collect cookies into headers. Have to preserve header case as there
238 # are some non-RFC compliant clients that require e.g. Content-Type.
239 response_headers = []
240 for header, value in response.items():
241 if isinstance(header, str):
242 header = header.encode("ascii")
243 if isinstance(value, str):
244 value = value.encode("latin1")
245 response_headers.append((bytes(header), bytes(value)))
246 for c in response.cookies.values():
247 response_headers.append(
248 (b"Set-Cookie", c.output(header="").encode("ascii").strip())
249 )
250 # Initial response message.
251 await send(
252 {
253 "type": "http.response.start",
254 "status": response.status_code,
255 "headers": response_headers,
256 }
257 )
258 # Streaming responses need to be pinned to their iterator.
259 if response.streaming:
260 # Access `__iter__` and not `streaming_content` directly in case
261 # it has been overridden in a subclass.
262 for part in response:
263 for chunk, _ in self.chunk_bytes(part):
264 await send(
265 {
266 "type": "http.response.body",
267 "body": chunk,
268 # Ignore "more" as there may be more parts; instead,
269 # use an empty final closing message with False.
270 "more_body": True,
271 }
272 )
273 # Final closing message.
274 await send({"type": "http.response.body"})
275 # Other responses just need chunking.
276 else:
277 # Yield chunks of response.
278 for chunk, last in self.chunk_bytes(response.content):
279 await send(
280 {
281 "type": "http.response.body",
282 "body": chunk,
283 "more_body": not last,
284 }
285 )
286 await sync_to_async(response.close, thread_sensitive=True)()
288 @classmethod
289 def chunk_bytes(cls, data):
290 """
291 Chunks some data up so it can be sent in reasonable size messages.
292 Yields (chunk, last_chunk) tuples.
293 """
294 position = 0
295 if not data:
296 yield data, True
297 return
298 while position < len(data):
299 yield (
300 data[position : position + cls.chunk_size],
301 (position + cls.chunk_size) >= len(data),
302 )
303 position += cls.chunk_size
305 def get_script_prefix(self, scope):
306 """
307 Return the script prefix to use from either the scope or a setting.
308 """
309 if settings.FORCE_SCRIPT_NAME:
310 return settings.FORCE_SCRIPT_NAME
311 return scope.get("root_path", "") or ""