asgi.py 12 KB

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