Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/httpcore/_sync/connection.py: 26%
113 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-26 06:12 +0000
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-26 06:12 +0000
1import itertools
2import ssl
3from types import TracebackType
4from typing import Iterator, Optional, Type
6from .._exceptions import ConnectError, ConnectionNotAvailable, ConnectTimeout
7from .._models import Origin, Request, Response
8from .._ssl import default_ssl_context
9from .._synchronization import Lock
10from .._trace import Trace
11from ..backends.sync import SyncBackend
12from ..backends.base import NetworkBackend, NetworkStream
13from .http11 import HTTP11Connection
14from .interfaces import ConnectionInterface
16RETRIES_BACKOFF_FACTOR = 0.5 # 0s, 0.5s, 1s, 2s, 4s, etc.
19def exponential_backoff(factor: float) -> Iterator[float]:
20 yield 0
21 for n in itertools.count(2):
22 yield factor * (2 ** (n - 2))
25class HTTPConnection(ConnectionInterface):
26 def __init__(
27 self,
28 origin: Origin,
29 ssl_context: Optional[ssl.SSLContext] = None,
30 keepalive_expiry: Optional[float] = None,
31 http1: bool = True,
32 http2: bool = False,
33 retries: int = 0,
34 local_address: Optional[str] = None,
35 uds: Optional[str] = None,
36 network_backend: Optional[NetworkBackend] = None,
37 ) -> None:
38 self._origin = origin
39 self._ssl_context = ssl_context
40 self._keepalive_expiry = keepalive_expiry
41 self._http1 = http1
42 self._http2 = http2
43 self._retries = retries
44 self._local_address = local_address
45 self._uds = uds
47 self._network_backend: NetworkBackend = (
48 SyncBackend() if network_backend is None else network_backend
49 )
50 self._connection: Optional[ConnectionInterface] = None
51 self._connect_failed: bool = False
52 self._request_lock = Lock()
54 def handle_request(self, request: Request) -> Response:
55 if not self.can_handle_request(request.url.origin):
56 raise RuntimeError(
57 f"Attempted to send request to {request.url.origin} on connection to {self._origin}"
58 )
60 with self._request_lock:
61 if self._connection is None:
62 try:
63 stream = self._connect(request)
65 ssl_object = stream.get_extra_info("ssl_object")
66 http2_negotiated = (
67 ssl_object is not None
68 and ssl_object.selected_alpn_protocol() == "h2"
69 )
70 if http2_negotiated or (self._http2 and not self._http1):
71 from .http2 import HTTP2Connection
73 self._connection = HTTP2Connection(
74 origin=self._origin,
75 stream=stream,
76 keepalive_expiry=self._keepalive_expiry,
77 )
78 else:
79 self._connection = HTTP11Connection(
80 origin=self._origin,
81 stream=stream,
82 keepalive_expiry=self._keepalive_expiry,
83 )
84 except Exception as exc:
85 self._connect_failed = True
86 raise exc
87 elif not self._connection.is_available():
88 raise ConnectionNotAvailable()
90 return self._connection.handle_request(request)
92 def _connect(self, request: Request) -> NetworkStream:
93 timeouts = request.extensions.get("timeout", {})
94 timeout = timeouts.get("connect", None)
96 retries_left = self._retries
97 delays = exponential_backoff(factor=RETRIES_BACKOFF_FACTOR)
99 while True:
100 try:
101 if self._uds is None:
102 kwargs = {
103 "host": self._origin.host.decode("ascii"),
104 "port": self._origin.port,
105 "local_address": self._local_address,
106 "timeout": timeout,
107 }
108 with Trace(
109 "connection.connect_tcp", request, kwargs
110 ) as trace:
111 stream = self._network_backend.connect_tcp(**kwargs)
112 trace.return_value = stream
113 else:
114 kwargs = {
115 "path": self._uds,
116 "timeout": timeout,
117 }
118 with Trace(
119 "connection.connect_unix_socket", request, kwargs
120 ) as trace:
121 stream = self._network_backend.connect_unix_socket(
122 **kwargs
123 )
124 trace.return_value = stream
125 except (ConnectError, ConnectTimeout):
126 if retries_left <= 0:
127 raise
128 retries_left -= 1
129 delay = next(delays)
130 # TRACE 'retry'
131 self._network_backend.sleep(delay)
132 else:
133 break
135 if self._origin.scheme == b"https":
136 ssl_context = (
137 default_ssl_context()
138 if self._ssl_context is None
139 else self._ssl_context
140 )
141 alpn_protocols = ["http/1.1", "h2"] if self._http2 else ["http/1.1"]
142 ssl_context.set_alpn_protocols(alpn_protocols)
144 kwargs = {
145 "ssl_context": ssl_context,
146 "server_hostname": self._origin.host.decode("ascii"),
147 "timeout": timeout,
148 }
149 with Trace("connection.start_tls", request, kwargs) as trace:
150 stream = stream.start_tls(**kwargs)
151 trace.return_value = stream
152 return stream
154 def can_handle_request(self, origin: Origin) -> bool:
155 return origin == self._origin
157 def close(self) -> None:
158 if self._connection is not None:
159 self._connection.close()
161 def is_available(self) -> bool:
162 if self._connection is None:
163 # If HTTP/2 support is enabled, and the resulting connection could
164 # end up as HTTP/2 then we should indicate the connection as being
165 # available to service multiple requests.
166 return (
167 self._http2
168 and (self._origin.scheme == b"https" or not self._http1)
169 and not self._connect_failed
170 )
171 return self._connection.is_available()
173 def has_expired(self) -> bool:
174 if self._connection is None:
175 return self._connect_failed
176 return self._connection.has_expired()
178 def is_idle(self) -> bool:
179 if self._connection is None:
180 return self._connect_failed
181 return self._connection.is_idle()
183 def is_closed(self) -> bool:
184 if self._connection is None:
185 return self._connect_failed
186 return self._connection.is_closed()
188 def info(self) -> str:
189 if self._connection is None:
190 return "CONNECTION FAILED" if self._connect_failed else "CONNECTING"
191 return self._connection.info()
193 def __repr__(self) -> str:
194 return f"<{self.__class__.__name__} [{self.info()}]>"
196 # These context managers are not used in the standard flow, but are
197 # useful for testing or working with connection instances directly.
199 def __enter__(self) -> "HTTPConnection":
200 return self
202 def __exit__(
203 self,
204 exc_type: Optional[Type[BaseException]] = None,
205 exc_value: Optional[BaseException] = None,
206 traceback: Optional[TracebackType] = None,
207 ) -> None:
208 self.close()