Format: text/x-python
⬇ Download File (13.6 KB)
   1  from __future__ import annotations
   2  
   3  import enum
   4  import logging
   5  import ssl
   6  import time
   7  import types
   8  import typing
   9  
  10  import h11
  11  
  12  from .._backends.base import AsyncNetworkStream
  13  from .._exceptions import (
  14      ConnectionNotAvailable,
  15      LocalProtocolError,
  16      RemoteProtocolError,
  17      WriteError,
  18      map_exceptions,
  19  )
  20  from .._models import Origin, Request, Response
  21  from .._synchronization import AsyncLock, AsyncShieldCancellation
  22  from .._trace import Trace
  23  from .interfaces import AsyncConnectionInterface
  24  
  25  logger = logging.getLogger("httpcore.http11")
  26  
  27  
  28  # A subset of `h11.Event` types supported by `_send_event`
  29  H11SendEvent = typing.Union[
  30      h11.Request,
  31      h11.Data,
  32      h11.EndOfMessage,
  33  ]
  34  
  35  
  36  class HTTPConnectionState(enum.IntEnum):
  37      NEW = 0
  38      ACTIVE = 1
  39      IDLE = 2
  40      CLOSED = 3
  41  
  42  
  43  class AsyncHTTP11Connection(AsyncConnectionInterface):
  44      READ_NUM_BYTES = 64 * 1024
  45      MAX_INCOMPLETE_EVENT_SIZE = 100 * 1024
  46  
  47      def __init__(
  48          self,
  49          origin: Origin,
  50          stream: AsyncNetworkStream,
  51          keepalive_expiry: float | None = None,
  52      ) -> None:
  53          self._origin = origin
  54          self._network_stream = stream
  55          self._keepalive_expiry: float | None = keepalive_expiry
  56          self._expire_at: float | None = None
  57          self._state = HTTPConnectionState.NEW
  58          self._state_lock = AsyncLock()
  59          self._request_count = 0
  60          self._h11_state = h11.Connection(
  61              our_role=h11.CLIENT,
  62              max_incomplete_event_size=self.MAX_INCOMPLETE_EVENT_SIZE,
  63          )
  64  
  65      async def handle_async_request(self, request: Request) -> Response:
  66          if not self.can_handle_request(request.url.origin):
  67              raise RuntimeError(
  68                  f"Attempted to send request to {request.url.origin} on connection "
  69                  f"to {self._origin}"
  70              )
  71  
  72          async with self._state_lock:
  73              if self._state in (HTTPConnectionState.NEW, HTTPConnectionState.IDLE):
  74                  self._request_count += 1
  75                  self._state = HTTPConnectionState.ACTIVE
  76                  self._expire_at = None
  77              else:
  78                  raise ConnectionNotAvailable()
  79  
  80          try:
  81              kwargs = {"request": request}
  82              try:
  83                  async with Trace(
  84                      "send_request_headers", logger, request, kwargs
  85                  ) as trace:
  86                      await self._send_request_headers(**kwargs)
  87                  async with Trace("send_request_body", logger, request, kwargs) as trace:
  88                      await self._send_request_body(**kwargs)
  89              except WriteError:
  90                  # If we get a write error while we're writing the request,
  91                  # then we supress this error and move on to attempting to
  92                  # read the response. Servers can sometimes close the request
  93                  # pre-emptively and then respond with a well formed HTTP
  94                  # error response.
  95                  pass
  96  
  97              async with Trace(
  98                  "receive_response_headers", logger, request, kwargs
  99              ) as trace:
 100                  (
 101                      http_version,
 102                      status,
 103                      reason_phrase,
 104                      headers,
 105                      trailing_data,
 106                  ) = await self._receive_response_headers(**kwargs)
 107                  trace.return_value = (
 108                      http_version,
 109                      status,
 110                      reason_phrase,
 111                      headers,
 112                  )
 113  
 114              network_stream = self._network_stream
 115  
 116              # CONNECT or Upgrade request
 117              if (status == 101) or (
 118                  (request.method == b"CONNECT") and (200 <= status < 300)
 119              ):
 120                  network_stream = AsyncHTTP11UpgradeStream(network_stream, trailing_data)
 121  
 122              return Response(
 123                  status=status,
 124                  headers=headers,
 125                  content=HTTP11ConnectionByteStream(self, request),
 126                  extensions={
 127                      "http_version": http_version,
 128                      "reason_phrase": reason_phrase,
 129                      "network_stream": network_stream,
 130                  },
 131              )
 132          except BaseException as exc:
 133              with AsyncShieldCancellation():
 134                  async with Trace("response_closed", logger, request) as trace:
 135                      await self._response_closed()
 136              raise exc
 137  
 138      # Sending the request...
 139  
 140      async def _send_request_headers(self, request: Request) -> None:
 141          timeouts = request.extensions.get("timeout", {})
 142          timeout = timeouts.get("write", None)
 143  
 144          with map_exceptions({h11.LocalProtocolError: LocalProtocolError}):
 145              event = h11.Request(
 146                  method=request.method,
 147                  target=request.url.target,
 148                  headers=request.headers,
 149              )
 150          await self._send_event(event, timeout=timeout)
 151  
 152      async def _send_request_body(self, request: Request) -> None:
 153          timeouts = request.extensions.get("timeout", {})
 154          timeout = timeouts.get("write", None)
 155  
 156          assert isinstance(request.stream, typing.AsyncIterable)
 157          async for chunk in request.stream:
 158              event = h11.Data(data=chunk)
 159              await self._send_event(event, timeout=timeout)
 160  
 161          await self._send_event(h11.EndOfMessage(), timeout=timeout)
 162  
 163      async def _send_event(self, event: h11.Event, timeout: float | None = None) -> None:
 164          bytes_to_send = self._h11_state.send(event)
 165          if bytes_to_send is not None:
 166              await self._network_stream.write(bytes_to_send, timeout=timeout)
 167  
 168      # Receiving the response...
 169  
 170      async def _receive_response_headers(
 171          self, request: Request
 172      ) -> tuple[bytes, int, bytes, list[tuple[bytes, bytes]], bytes]:
 173          timeouts = request.extensions.get("timeout", {})
 174          timeout = timeouts.get("read", None)
 175  
 176          while True:
 177              event = await self._receive_event(timeout=timeout)
 178              if isinstance(event, h11.Response):
 179                  break
 180              if (
 181                  isinstance(event, h11.InformationalResponse)
 182                  and event.status_code == 101
 183              ):
 184                  break
 185  
 186          http_version = b"HTTP/" + event.http_version
 187  
 188          # h11 version 0.11+ supports a `raw_items` interface to get the
 189          # raw header casing, rather than the enforced lowercase headers.
 190          headers = event.headers.raw_items()
 191  
 192          trailing_data, _ = self._h11_state.trailing_data
 193  
 194          return http_version, event.status_code, event.reason, headers, trailing_data
 195  
 196      async def _receive_response_body(
 197          self, request: Request
 198      ) -> typing.AsyncIterator[bytes]:
 199          timeouts = request.extensions.get("timeout", {})
 200          timeout = timeouts.get("read", None)
 201  
 202          while True:
 203              event = await self._receive_event(timeout=timeout)
 204              if isinstance(event, h11.Data):
 205                  yield bytes(event.data)
 206              elif isinstance(event, (h11.EndOfMessage, h11.PAUSED)):
 207                  break
 208  
 209      async def _receive_event(
 210          self, timeout: float | None = None
 211      ) -> h11.Event | type[h11.PAUSED]:
 212          while True:
 213              with map_exceptions({h11.RemoteProtocolError: RemoteProtocolError}):
 214                  event = self._h11_state.next_event()
 215  
 216              if event is h11.NEED_DATA:
 217                  data = await self._network_stream.read(
 218                      self.READ_NUM_BYTES, timeout=timeout
 219                  )
 220  
 221                  # If we feed this case through h11 we'll raise an exception like:
 222                  #
 223                  #     httpcore.RemoteProtocolError: can't handle event type
 224                  #     ConnectionClosed when role=SERVER and state=SEND_RESPONSE
 225                  #
 226                  # Which is accurate, but not very informative from an end-user
 227                  # perspective. Instead we handle this case distinctly and treat
 228                  # it as a ConnectError.
 229                  if data == b"" and self._h11_state.their_state == h11.SEND_RESPONSE:
 230                      msg = "Server disconnected without sending a response."
 231                      raise RemoteProtocolError(msg)
 232  
 233                  self._h11_state.receive_data(data)
 234              else:
 235                  # mypy fails to narrow the type in the above if statement above
 236                  return event  # type: ignore[return-value]
 237  
 238      async def _response_closed(self) -> None:
 239          async with self._state_lock:
 240              if (
 241                  self._h11_state.our_state is h11.DONE
 242                  and self._h11_state.their_state is h11.DONE
 243              ):
 244                  self._state = HTTPConnectionState.IDLE
 245                  self._h11_state.start_next_cycle()
 246                  if self._keepalive_expiry is not None:
 247                      now = time.monotonic()
 248                      self._expire_at = now + self._keepalive_expiry
 249              else:
 250                  await self.aclose()
 251  
 252      # Once the connection is no longer required...
 253  
 254      async def aclose(self) -> None:
 255          # Note that this method unilaterally closes the connection, and does
 256          # not have any kind of locking in place around it.
 257          self._state = HTTPConnectionState.CLOSED
 258          await self._network_stream.aclose()
 259  
 260      # The AsyncConnectionInterface methods provide information about the state of
 261      # the connection, allowing for a connection pooling implementation to
 262      # determine when to reuse and when to close the connection...
 263  
 264      def can_handle_request(self, origin: Origin) -> bool:
 265          return origin == self._origin
 266  
 267      def is_available(self) -> bool:
 268          # Note that HTTP/1.1 connections in the "NEW" state are not treated as
 269          # being "available". The control flow which created the connection will
 270          # be able to send an outgoing request, but the connection will not be
 271          # acquired from the connection pool for any other request.
 272          return self._state == HTTPConnectionState.IDLE
 273  
 274      def has_expired(self) -> bool:
 275          now = time.monotonic()
 276          keepalive_expired = self._expire_at is not None and now > self._expire_at
 277  
 278          # If the HTTP connection is idle but the socket is readable, then the
 279          # only valid state is that the socket is about to return b"", indicating
 280          # a server-initiated disconnect.
 281          server_disconnected = (
 282              self._state == HTTPConnectionState.IDLE
 283              and self._network_stream.get_extra_info("is_readable")
 284          )
 285  
 286          return keepalive_expired or server_disconnected
 287  
 288      def is_idle(self) -> bool:
 289          return self._state == HTTPConnectionState.IDLE
 290  
 291      def is_closed(self) -> bool:
 292          return self._state == HTTPConnectionState.CLOSED
 293  
 294      def info(self) -> str:
 295          origin = str(self._origin)
 296          return (
 297              f"{origin!r}, HTTP/1.1, {self._state.name}, "
 298              f"Request Count: {self._request_count}"
 299          )
 300  
 301      def __repr__(self) -> str:
 302          class_name = self.__class__.__name__
 303          origin = str(self._origin)
 304          return (
 305              f"<{class_name} [{origin!r}, {self._state.name}, "
 306              f"Request Count: {self._request_count}]>"
 307          )
 308  
 309      # These context managers are not used in the standard flow, but are
 310      # useful for testing or working with connection instances directly.
 311  
 312      async def __aenter__(self) -> AsyncHTTP11Connection:
 313          return self
 314  
 315      async def __aexit__(
 316          self,
 317          exc_type: type[BaseException] | None = None,
 318          exc_value: BaseException | None = None,
 319          traceback: types.TracebackType | None = None,
 320      ) -> None:
 321          await self.aclose()
 322  
 323  
 324  class HTTP11ConnectionByteStream:
 325      def __init__(self, connection: AsyncHTTP11Connection, request: Request) -> None:
 326          self._connection = connection
 327          self._request = request
 328          self._closed = False
 329  
 330      async def __aiter__(self) -> typing.AsyncIterator[bytes]:
 331          kwargs = {"request": self._request}
 332          try:
 333              async with Trace("receive_response_body", logger, self._request, kwargs):
 334                  async for chunk in self._connection._receive_response_body(**kwargs):
 335                      yield chunk
 336          except BaseException as exc:
 337              # If we get an exception while streaming the response,
 338              # we want to close the response (and possibly the connection)
 339              # before raising that exception.
 340              with AsyncShieldCancellation():
 341                  await self.aclose()
 342              raise exc
 343  
 344      async def aclose(self) -> None:
 345          if not self._closed:
 346              self._closed = True
 347              async with Trace("response_closed", logger, self._request):
 348                  await self._connection._response_closed()
 349  
 350  
 351  class AsyncHTTP11UpgradeStream(AsyncNetworkStream):
 352      def __init__(self, stream: AsyncNetworkStream, leading_data: bytes) -> None:
 353          self._stream = stream
 354          self._leading_data = leading_data
 355  
 356      async def read(self, max_bytes: int, timeout: float | None = None) -> bytes:
 357          if self._leading_data:
 358              buffer = self._leading_data[:max_bytes]
 359              self._leading_data = self._leading_data[max_bytes:]
 360              return buffer
 361          else:
 362              return await self._stream.read(max_bytes, timeout)
 363  
 364      async def write(self, buffer: bytes, timeout: float | None = None) -> None:
 365          await self._stream.write(buffer, timeout)
 366  
 367      async def aclose(self) -> None:
 368          await self._stream.aclose()
 369  
 370      async def start_tls(
 371          self,
 372          ssl_context: ssl.SSLContext,
 373          server_hostname: str | None = None,
 374          timeout: float | None = None,
 375      ) -> AsyncNetworkStream:
 376          return await self._stream.start_tls(ssl_context, server_hostname, timeout)
 377  
 378      def get_extra_info(self, info: str) -> typing.Any:
 379          return self._stream.get_extra_info(info)
 380