Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
http2.py593 linesDownload Raw Back to _sync
1from __future__ import annotations2 3import enum4import logging5import time6import types7import typing8 9import h2.config10import h2.connection11import h2.events12import h2.exceptions13import h2.settings14 15from .._backends.base import NetworkStream16from .._exceptions import (17    ConnectionNotAvailable,18    LocalProtocolError,19    RemoteProtocolError,20)21from .._models import Origin, Request, Response22from .._synchronization import Lock, Semaphore, ShieldCancellation23from .._trace import Trace24from .interfaces import ConnectionInterface25 26logger = logging.getLogger("httpcore.http2")27 28 29def has_body_headers(request: Request) -> bool:30    return any(31        k.lower() == b"content-length" or k.lower() == b"transfer-encoding"32        for k, v in request.headers33    )34 35 36class HTTPConnectionState(enum.IntEnum):37    ACTIVE = 138    IDLE = 239    CLOSED = 340 41 42class HTTP2Connection(ConnectionInterface):43    READ_NUM_BYTES = 64 * 102444    CONFIG = h2.config.H2Configuration(validate_inbound_headers=False)45 46    def __init__(47        self,48        origin: Origin,49        stream: NetworkStream,50        keepalive_expiry: float | None = None,51    ):52        self._origin = origin53        self._network_stream = stream54        self._keepalive_expiry: float | None = keepalive_expiry55        self._h2_state = h2.connection.H2Connection(config=self.CONFIG)56        self._state = HTTPConnectionState.IDLE57        self._expire_at: float | None = None58        self._request_count = 059        self._init_lock = Lock()60        self._state_lock = Lock()61        self._read_lock = Lock()62        self._write_lock = Lock()63        self._sent_connection_init = False64        self._used_all_stream_ids = False65        self._connection_error = False66 67        # Mapping from stream ID to response stream events.68        self._events: dict[69            int,70            list[71                h2.events.ResponseReceived72                | h2.events.DataReceived73                | h2.events.StreamEnded74                | h2.events.StreamReset,75            ],76        ] = {}77 78        # Connection terminated events are stored as state since79        # we need to handle them for all streams.80        self._connection_terminated: h2.events.ConnectionTerminated | None = None81 82        self._read_exception: Exception | None = None83        self._write_exception: Exception | None = None84 85    def handle_request(self, request: Request) -> Response:86        if not self.can_handle_request(request.url.origin):87            # This cannot occur in normal operation, since the connection pool88            # will only send requests on connections that handle them.89            # It's in place simply for resilience as a guard against incorrect90            # usage, for anyone working directly with httpcore connections.91            raise RuntimeError(92                f"Attempted to send request to {request.url.origin} on connection "93                f"to {self._origin}"94            )95 96        with self._state_lock:97            if self._state in (HTTPConnectionState.ACTIVE, HTTPConnectionState.IDLE):98                self._request_count += 199                self._expire_at = None100                self._state = HTTPConnectionState.ACTIVE101            else:102                raise ConnectionNotAvailable()103 104        with self._init_lock:105            if not self._sent_connection_init:106                try:107                    sci_kwargs = {"request": request}108                    with Trace(109                        "send_connection_init", logger, request, sci_kwargs110                    ):111                        self._send_connection_init(**sci_kwargs)112                except BaseException as exc:113                    with ShieldCancellation():114                        self.close()115                    raise exc116 117                self._sent_connection_init = True118 119                # Initially start with just 1 until the remote server provides120                # its max_concurrent_streams value121                self._max_streams = 1122 123                local_settings_max_streams = (124                    self._h2_state.local_settings.max_concurrent_streams125                )126                self._max_streams_semaphore = Semaphore(local_settings_max_streams)127 128                for _ in range(local_settings_max_streams - self._max_streams):129                    self._max_streams_semaphore.acquire()130 131        self._max_streams_semaphore.acquire()132 133        try:134            stream_id = self._h2_state.get_next_available_stream_id()135            self._events[stream_id] = []136        except h2.exceptions.NoAvailableStreamIDError:  # pragma: nocover137            self._used_all_stream_ids = True138            self._request_count -= 1139            raise ConnectionNotAvailable()140 141        try:142            kwargs = {"request": request, "stream_id": stream_id}143            with Trace("send_request_headers", logger, request, kwargs):144                self._send_request_headers(request=request, stream_id=stream_id)145            with Trace("send_request_body", logger, request, kwargs):146                self._send_request_body(request=request, stream_id=stream_id)147            with Trace(148                "receive_response_headers", logger, request, kwargs149            ) as trace:150                status, headers = self._receive_response(151                    request=request, stream_id=stream_id152                )153                trace.return_value = (status, headers)154 155            return Response(156                status=status,157                headers=headers,158                content=HTTP2ConnectionByteStream(self, request, stream_id=stream_id),159                extensions={160                    "http_version": b"HTTP/2",161                    "network_stream": self._network_stream,162                    "stream_id": stream_id,163                },164            )165        except BaseException as exc:  # noqa: PIE786166            with ShieldCancellation():167                kwargs = {"stream_id": stream_id}168                with Trace("response_closed", logger, request, kwargs):169                    self._response_closed(stream_id=stream_id)170 171            if isinstance(exc, h2.exceptions.ProtocolError):172                # One case where h2 can raise a protocol error is when a173                # closed frame has been seen by the state machine.174                #175                # This happens when one stream is reading, and encounters176                # a GOAWAY event. Other flows of control may then raise177                # a protocol error at any point they interact with the 'h2_state'.178                #179                # In this case we'll have stored the event, and should raise180                # it as a RemoteProtocolError.181                if self._connection_terminated:  # pragma: nocover182                    raise RemoteProtocolError(self._connection_terminated)183                # If h2 raises a protocol error in some other state then we184                # must somehow have made a protocol violation.185                raise LocalProtocolError(exc)  # pragma: nocover186 187            raise exc188 189    def _send_connection_init(self, request: Request) -> None:190        """191        The HTTP/2 connection requires some initial setup before we can start192        using individual request/response streams on it.193        """194        # Need to set these manually here instead of manipulating via195        # __setitem__() otherwise the H2Connection will emit SettingsUpdate196        # frames in addition to sending the undesired defaults.197        self._h2_state.local_settings = h2.settings.Settings(198            client=True,199            initial_values={200                # Disable PUSH_PROMISE frames from the server since we don't do anything201                # with them for now.  Maybe when we support caching?202                h2.settings.SettingCodes.ENABLE_PUSH: 0,203                # These two are taken from h2 for safe defaults204                h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS: 100,205                h2.settings.SettingCodes.MAX_HEADER_LIST_SIZE: 65536,206            },207        )208 209        # Some websites (*cough* Yahoo *cough*) balk at this setting being210        # present in the initial handshake since it's not defined in the original211        # RFC despite the RFC mandating ignoring settings you don't know about.212        del self._h2_state.local_settings[213            h2.settings.SettingCodes.ENABLE_CONNECT_PROTOCOL214        ]215 216        self._h2_state.initiate_connection()217        self._h2_state.increment_flow_control_window(2**24)218        self._write_outgoing_data(request)219 220    # Sending the request...221 222    def _send_request_headers(self, request: Request, stream_id: int) -> None:223        """224        Send the request headers to a given stream ID.225        """226        end_stream = not has_body_headers(request)227 228        # In HTTP/2 the ':authority' pseudo-header is used instead of 'Host'.229        # In order to gracefully handle HTTP/1.1 and HTTP/2 we always require230        # HTTP/1.1 style headers, and map them appropriately if we end up on231        # an HTTP/2 connection.232        authority = [v for k, v in request.headers if k.lower() == b"host"][0]233 234        headers = [235            (b":method", request.method),236            (b":authority", authority),237            (b":scheme", request.url.scheme),238            (b":path", request.url.target),239        ] + [240            (k.lower(), v)241            for k, v in request.headers242            if k.lower()243            not in (244                b"host",245                b"transfer-encoding",246            )247        ]248 249        self._h2_state.send_headers(stream_id, headers, end_stream=end_stream)250        self._h2_state.increment_flow_control_window(2**24, stream_id=stream_id)251        self._write_outgoing_data(request)252 253    def _send_request_body(self, request: Request, stream_id: int) -> None:254        """255        Iterate over the request body sending it to a given stream ID.256        """257        if not has_body_headers(request):258            return259 260        assert isinstance(request.stream, typing.Iterable)261        for data in request.stream:262            self._send_stream_data(request, stream_id, data)263        self._send_end_stream(request, stream_id)264 265    def _send_stream_data(266        self, request: Request, stream_id: int, data: bytes267    ) -> None:268        """269        Send a single chunk of data in one or more data frames.270        """271        while data:272            max_flow = self._wait_for_outgoing_flow(request, stream_id)273            chunk_size = min(len(data), max_flow)274            chunk, data = data[:chunk_size], data[chunk_size:]275            self._h2_state.send_data(stream_id, chunk)276            self._write_outgoing_data(request)277 278    def _send_end_stream(self, request: Request, stream_id: int) -> None:279        """280        Send an empty data frame on on a given stream ID with the END_STREAM flag set.281        """282        self._h2_state.end_stream(stream_id)283        self._write_outgoing_data(request)284 285    # Receiving the response...286 287    def _receive_response(288        self, request: Request, stream_id: int289    ) -> tuple[int, list[tuple[bytes, bytes]]]:290        """291        Return the response status code and headers for a given stream ID.292        """293        while True:294            event = self._receive_stream_event(request, stream_id)295            if isinstance(event, h2.events.ResponseReceived):296                break297 298        status_code = 200299        headers = []300        assert event.headers is not None301        for k, v in event.headers:302            if k == b":status":303                status_code = int(v.decode("ascii", errors="ignore"))304            elif not k.startswith(b":"):305                headers.append((k, v))306 307        return (status_code, headers)308 309    def _receive_response_body(310        self, request: Request, stream_id: int311    ) -> typing.Iterator[bytes]:312        """313        Iterator that returns the bytes of the response body for a given stream ID.314        """315        while True:316            event = self._receive_stream_event(request, stream_id)317            if isinstance(event, h2.events.DataReceived):318                assert event.flow_controlled_length is not None319                assert event.data is not None320                amount = event.flow_controlled_length321                self._h2_state.acknowledge_received_data(amount, stream_id)322                self._write_outgoing_data(request)323                yield event.data324            elif isinstance(event, h2.events.StreamEnded):325                break326 327    def _receive_stream_event(328        self, request: Request, stream_id: int329    ) -> h2.events.ResponseReceived | h2.events.DataReceived | h2.events.StreamEnded:330        """331        Return the next available event for a given stream ID.332 333        Will read more data from the network if required.334        """335        while not self._events.get(stream_id):336            self._receive_events(request, stream_id)337        event = self._events[stream_id].pop(0)338        if isinstance(event, h2.events.StreamReset):339            raise RemoteProtocolError(event)340        return event341 342    def _receive_events(343        self, request: Request, stream_id: int | None = None344    ) -> None:345        """346        Read some data from the network until we see one or more events347        for a given stream ID.348        """349        with self._read_lock:350            if self._connection_terminated is not None:351                last_stream_id = self._connection_terminated.last_stream_id352                if stream_id and last_stream_id and stream_id > last_stream_id:353                    self._request_count -= 1354                    raise ConnectionNotAvailable()355                raise RemoteProtocolError(self._connection_terminated)356 357            # This conditional is a bit icky. We don't want to block reading if we've358            # actually got an event to return for a given stream. We need to do that359            # check *within* the atomic read lock. Though it also need to be optional,360            # because when we call it from `_wait_for_outgoing_flow` we *do* want to361            # block until we've available flow control, event when we have events362            # pending for the stream ID we're attempting to send on.363            if stream_id is None or not self._events.get(stream_id):364                events = self._read_incoming_data(request)365                for event in events:366                    if isinstance(event, h2.events.RemoteSettingsChanged):367                        with Trace(368                            "receive_remote_settings", logger, request369                        ) as trace:370                            self._receive_remote_settings_change(event)371                            trace.return_value = event372 373                    elif isinstance(374                        event,375                        (376                            h2.events.ResponseReceived,377                            h2.events.DataReceived,378                            h2.events.StreamEnded,379                            h2.events.StreamReset,380                        ),381                    ):382                        if event.stream_id in self._events:383                            self._events[event.stream_id].append(event)384 385                    elif isinstance(event, h2.events.ConnectionTerminated):386                        self._connection_terminated = event387 388        self._write_outgoing_data(request)389 390    def _receive_remote_settings_change(391        self, event: h2.events.RemoteSettingsChanged392    ) -> None:393        max_concurrent_streams = event.changed_settings.get(394            h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS395        )396        if max_concurrent_streams:397            new_max_streams = min(398                max_concurrent_streams.new_value,399                self._h2_state.local_settings.max_concurrent_streams,400            )401            if new_max_streams and new_max_streams != self._max_streams:402                while new_max_streams > self._max_streams:403                    self._max_streams_semaphore.release()404                    self._max_streams += 1405                while new_max_streams < self._max_streams:406                    self._max_streams_semaphore.acquire()407                    self._max_streams -= 1408 409    def _response_closed(self, stream_id: int) -> None:410        self._max_streams_semaphore.release()411        del self._events[stream_id]412        with self._state_lock:413            if self._connection_terminated and not self._events:414                self.close()415 416            elif self._state == HTTPConnectionState.ACTIVE and not self._events:417                self._state = HTTPConnectionState.IDLE418                if self._keepalive_expiry is not None:419                    now = time.monotonic()420                    self._expire_at = now + self._keepalive_expiry421                if self._used_all_stream_ids:  # pragma: nocover422                    self.close()423 424    def close(self) -> None:425        # Note that this method unilaterally closes the connection, and does426        # not have any kind of locking in place around it.427        self._h2_state.close_connection()428        self._state = HTTPConnectionState.CLOSED429        self._network_stream.close()430 431    # Wrappers around network read/write operations...432 433    def _read_incoming_data(self, request: Request) -> list[h2.events.Event]:434        timeouts = request.extensions.get("timeout", {})435        timeout = timeouts.get("read", None)436 437        if self._read_exception is not None:438            raise self._read_exception  # pragma: nocover439 440        try:441            data = self._network_stream.read(self.READ_NUM_BYTES, timeout)442            if data == b"":443                raise RemoteProtocolError("Server disconnected")444        except Exception as exc:445            # If we get a network error we should:446            #447            # 1. Save the exception and just raise it immediately on any future reads.448            #    (For example, this means that a single read timeout or disconnect will449            #    immediately close all pending streams. Without requiring multiple450            #    sequential timeouts.)451            # 2. Mark the connection as errored, so that we don't accept any other452            #    incoming requests.453            self._read_exception = exc454            self._connection_error = True455            raise exc456 457        events: list[h2.events.Event] = self._h2_state.receive_data(data)458 459        return events460 461    def _write_outgoing_data(self, request: Request) -> None:462        timeouts = request.extensions.get("timeout", {})463        timeout = timeouts.get("write", None)464 465        with self._write_lock:466            data_to_send = self._h2_state.data_to_send()467 468            if self._write_exception is not None:469                raise self._write_exception  # pragma: nocover470 471            try:472                self._network_stream.write(data_to_send, timeout)473            except Exception as exc:  # pragma: nocover474                # If we get a network error we should:475                #476                # 1. Save the exception and just raise it immediately on any future write.477                #    (For example, this means that a single write timeout or disconnect will478                #    immediately close all pending streams. Without requiring multiple479                #    sequential timeouts.)480                # 2. Mark the connection as errored, so that we don't accept any other481                #    incoming requests.482                self._write_exception = exc483                self._connection_error = True484                raise exc485 486    # Flow control...487 488    def _wait_for_outgoing_flow(self, request: Request, stream_id: int) -> int:489        """490        Returns the maximum allowable outgoing flow for a given stream.491 492        If the allowable flow is zero, then waits on the network until493        WindowUpdated frames have increased the flow rate.494        https://tools.ietf.org/html/rfc7540#section-6.9495        """496        local_flow: int = self._h2_state.local_flow_control_window(stream_id)497        max_frame_size: int = self._h2_state.max_outbound_frame_size498        flow = min(local_flow, max_frame_size)499        while flow == 0:500            self._receive_events(request)501            local_flow = self._h2_state.local_flow_control_window(stream_id)502            max_frame_size = self._h2_state.max_outbound_frame_size503            flow = min(local_flow, max_frame_size)504        return flow505 506    # Interface for connection pooling...507 508    def can_handle_request(self, origin: Origin) -> bool:509        return origin == self._origin510 511    def is_available(self) -> bool:512        return (513            self._state != HTTPConnectionState.CLOSED514            and not self._connection_error515            and not self._used_all_stream_ids516            and not (517                self._h2_state.state_machine.state518                == h2.connection.ConnectionState.CLOSED519            )520        )521 522    def has_expired(self) -> bool:523        now = time.monotonic()524        return self._expire_at is not None and now > self._expire_at525 526    def is_idle(self) -> bool:527        return self._state == HTTPConnectionState.IDLE528 529    def is_closed(self) -> bool:530        return self._state == HTTPConnectionState.CLOSED531 532    def info(self) -> str:533        origin = str(self._origin)534        return (535            f"{origin!r}, HTTP/2, {self._state.name}, "536            f"Request Count: {self._request_count}"537        )538 539    def __repr__(self) -> str:540        class_name = self.__class__.__name__541        origin = str(self._origin)542        return (543            f"<{class_name} [{origin!r}, {self._state.name}, "544            f"Request Count: {self._request_count}]>"545        )546 547    # These context managers are not used in the standard flow, but are548    # useful for testing or working with connection instances directly.549 550    def __enter__(self) -> HTTP2Connection:551        return self552 553    def __exit__(554        self,555        exc_type: type[BaseException] | None = None,556        exc_value: BaseException | None = None,557        traceback: types.TracebackType | None = None,558    ) -> None:559        self.close()560 561 562class HTTP2ConnectionByteStream:563    def __init__(564        self, connection: HTTP2Connection, request: Request, stream_id: int565    ) -> None:566        self._connection = connection567        self._request = request568        self._stream_id = stream_id569        self._closed = False570 571    def __iter__(self) -> typing.Iterator[bytes]:572        kwargs = {"request": self._request, "stream_id": self._stream_id}573        try:574            with Trace("receive_response_body", logger, self._request, kwargs):575                for chunk in self._connection._receive_response_body(576                    request=self._request, stream_id=self._stream_id577                ):578                    yield chunk579        except BaseException as exc:580            # If we get an exception while streaming the response,581            # we want to close the response (and possibly the connection)582            # before raising that exception.583            with ShieldCancellation():584                self.close()585            raise exc586 587    def close(self) -> None:588        if not self._closed:589            self._closed = True590            kwargs = {"stream_id": self._stream_id}591            with Trace("response_closed", logger, self._request, kwargs):592                self._connection._response_closed(stream_id=self._stream_id)593 
codekingpro/portable-devtools · Team Ai