base.py 8.6 KB

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