base.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. from __future__ import annotations
  2. import typing
  3. import anyio
  4. from starlette._utils import collapse_excgroups
  5. from starlette.requests import ClientDisconnect, Request
  6. from starlette.responses import AsyncContentStream, Response
  7. from starlette.types import ASGIApp, Message, Receive, Scope, Send
  8. RequestResponseEndpoint = typing.Callable[[Request], typing.Awaitable[Response]]
  9. DispatchFunction = typing.Callable[[Request, RequestResponseEndpoint], typing.Awaitable[Response]]
  10. T = typing.TypeVar("T")
  11. class _CachedRequest(Request):
  12. """
  13. If the user calls Request.body() from their dispatch function
  14. we cache the entire request body in memory and pass that to downstream middlewares,
  15. but if they call Request.stream() then all we do is send an
  16. empty body so that downstream things don't hang forever.
  17. """
  18. def __init__(self, scope: Scope, receive: Receive):
  19. super().__init__(scope, receive)
  20. self._wrapped_rcv_disconnected = False
  21. self._wrapped_rcv_consumed = False
  22. self._wrapped_rc_stream = self.stream()
  23. async def wrapped_receive(self) -> Message:
  24. # wrapped_rcv state 1: disconnected
  25. if self._wrapped_rcv_disconnected:
  26. # we've already sent a disconnect to the downstream app
  27. # we don't need to wait to get another one
  28. # (although most ASGI servers will just keep sending it)
  29. return {"type": "http.disconnect"}
  30. # wrapped_rcv state 1: consumed but not yet disconnected
  31. if self._wrapped_rcv_consumed:
  32. # since the downstream app has consumed us all that is left
  33. # is to send it a disconnect
  34. if self._is_disconnected:
  35. # the middleware has already seen the disconnect
  36. # since we know the client is disconnected no need to wait
  37. # for the message
  38. self._wrapped_rcv_disconnected = True
  39. return {"type": "http.disconnect"}
  40. # we don't know yet if the client is disconnected or not
  41. # so we'll wait until we get that message
  42. msg = await self.receive()
  43. if msg["type"] != "http.disconnect": # pragma: no cover
  44. # at this point a disconnect is all that we should be receiving
  45. # if we get something else, things went wrong somewhere
  46. raise RuntimeError(f"Unexpected message received: {msg['type']}")
  47. self._wrapped_rcv_disconnected = True
  48. return msg
  49. # wrapped_rcv state 3: not yet consumed
  50. if getattr(self, "_body", None) is not None:
  51. # body() was called, we return it even if the client disconnected
  52. self._wrapped_rcv_consumed = True
  53. return {
  54. "type": "http.request",
  55. "body": self._body,
  56. "more_body": False,
  57. }
  58. elif self._stream_consumed:
  59. # stream() was called to completion
  60. # return an empty body so that downstream apps don't hang
  61. # waiting for a disconnect
  62. self._wrapped_rcv_consumed = True
  63. return {
  64. "type": "http.request",
  65. "body": b"",
  66. "more_body": False,
  67. }
  68. else:
  69. # body() was never called and stream() wasn't consumed
  70. try:
  71. stream = self.stream()
  72. chunk = await stream.__anext__()
  73. self._wrapped_rcv_consumed = self._stream_consumed
  74. return {
  75. "type": "http.request",
  76. "body": chunk,
  77. "more_body": not self._stream_consumed,
  78. }
  79. except ClientDisconnect:
  80. self._wrapped_rcv_disconnected = True
  81. return {"type": "http.disconnect"}
  82. class BaseHTTPMiddleware:
  83. def __init__(self, app: ASGIApp, dispatch: DispatchFunction | None = None) -> None:
  84. self.app = app
  85. self.dispatch_func = self.dispatch if dispatch is None else dispatch
  86. async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
  87. if scope["type"] != "http":
  88. await self.app(scope, receive, send)
  89. return
  90. request = _CachedRequest(scope, receive)
  91. wrapped_receive = request.wrapped_receive
  92. response_sent = anyio.Event()
  93. app_exc: Exception | None = None
  94. exception_already_raised = False
  95. async def call_next(request: Request) -> Response:
  96. async def receive_or_disconnect() -> Message:
  97. if response_sent.is_set():
  98. return {"type": "http.disconnect"}
  99. async with anyio.create_task_group() as task_group:
  100. async def wrap(func: typing.Callable[[], typing.Awaitable[T]]) -> T:
  101. result = await func()
  102. task_group.cancel_scope.cancel()
  103. return result
  104. task_group.start_soon(wrap, response_sent.wait)
  105. message = await wrap(wrapped_receive)
  106. if response_sent.is_set():
  107. return {"type": "http.disconnect"}
  108. return message
  109. async def send_no_error(message: Message) -> None:
  110. try:
  111. await send_stream.send(message)
  112. except anyio.BrokenResourceError:
  113. # recv_stream has been closed, i.e. response_sent has been set.
  114. return
  115. async def coro() -> None:
  116. nonlocal app_exc
  117. with send_stream:
  118. try:
  119. await self.app(scope, receive_or_disconnect, send_no_error)
  120. except Exception as exc:
  121. app_exc = exc
  122. task_group.start_soon(coro)
  123. try:
  124. message = await recv_stream.receive()
  125. info = message.get("info", None)
  126. if message["type"] == "http.response.debug" and info is not None:
  127. message = await recv_stream.receive()
  128. except anyio.EndOfStream:
  129. if app_exc is not None:
  130. nonlocal exception_already_raised
  131. exception_already_raised = True
  132. raise app_exc
  133. raise RuntimeError("No response returned.")
  134. assert message["type"] == "http.response.start"
  135. async def body_stream() -> typing.AsyncGenerator[bytes, None]:
  136. async for message in recv_stream:
  137. assert message["type"] == "http.response.body"
  138. body = message.get("body", b"")
  139. if body:
  140. yield body
  141. if not message.get("more_body", False):
  142. break
  143. response = _StreamingResponse(status_code=message["status"], content=body_stream(), info=info)
  144. response.raw_headers = message["headers"]
  145. return response
  146. streams: anyio.create_memory_object_stream[Message] = anyio.create_memory_object_stream()
  147. send_stream, recv_stream = streams
  148. with recv_stream, send_stream, collapse_excgroups():
  149. async with anyio.create_task_group() as task_group:
  150. response = await self.dispatch_func(request, call_next)
  151. await response(scope, wrapped_receive, send)
  152. response_sent.set()
  153. recv_stream.close()
  154. if app_exc is not None and not exception_already_raised:
  155. raise app_exc
  156. async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
  157. raise NotImplementedError() # pragma: no cover
  158. class _StreamingResponse(Response):
  159. def __init__(
  160. self,
  161. content: AsyncContentStream,
  162. status_code: int = 200,
  163. headers: typing.Mapping[str, str] | None = None,
  164. media_type: str | None = None,
  165. info: typing.Mapping[str, typing.Any] | None = None,
  166. ) -> None:
  167. self.info = info
  168. self.body_iterator = content
  169. self.status_code = status_code
  170. self.media_type = media_type
  171. self.init_headers(headers)
  172. self.background = None
  173. async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
  174. if self.info is not None:
  175. await send({"type": "http.response.debug", "info": self.info})
  176. await send(
  177. {
  178. "type": "http.response.start",
  179. "status": self.status_code,
  180. "headers": self.raw_headers,
  181. }
  182. )
  183. async for chunk in self.body_iterator:
  184. await send({"type": "http.response.body", "body": chunk, "more_body": True})
  185. await send({"type": "http.response.body", "body": b"", "more_body": False})
  186. if self.background:
  187. await self.background()