
管理员
站长
- UID
- 1
- 积分
- 224400
- 余额
- 13 R
- Moe币
- 84881
- 在线时间
- 279 小时
- 注册时间
- 2025-12-28
- 最后登录
- 2026-11-9
|
from __future__ import annotations
import enum
import logging
import time
import types
import typing
import h2.config
import h2.connection
import h2.events
import h2.exceptions
import h2.settings
from .._backends.base import AsyncNetworkStream
from .._exceptions import (
ConnectionNotAvailable,
LocalProtocolError,
RemoteProtocolError,
)
from .._models import Origin, Request, Response
from .._synchronization import AsyncLock, AsyncSemaphore, AsyncShieldCancellation
from .._trace import Trace
from .interfaces import AsyncConnectionInterface
logger = logging.getLogger("httpcore.http2")
def has_body_headers(request: Request) -> bool:
return any(
k.lower() == b"content-length" or k.lower() == b"transfer-encoding"
for k, v in request.headers
)
class HTTPConnectionState(enum.IntEnum):
ACTIVE = 1
IDLE = 2
CLOSED = 3
class AsyncHTTP2Connection(AsyncConnectionInterface):
READ_NUM_BYTES = 64 * 1024
CONFIG = h2.config.H2Configuration(validate_inbound_headers=False)
def __init__(
self,
origin: Origin,
stream: AsyncNetworkStream,
keepalive_expiry: float | None = None,
):
self._origin = origin
self._network_stream = stream
self._keepalive_expiry: float | None = keepalive_expiry
self._h2_state = h2.connection.H2Connection(config=self.CONFIG)
self._state = HTTPConnectionState.IDLE
self._expire_at: float | None = None
self._request_count = 0
self._init_lock = AsyncLock()
self._state_lock = AsyncLock()
self._read_lock = AsyncLock()
self._write_lock = AsyncLock()
self._sent_connection_init = False
self._used_all_stream_ids = False
self._connection_error = False
# Mapping from stream ID to response stream events.
self._events: dict[
int,
list[
h2.events.ResponseReceived
| h2.events.DataReceived
| h2.events.StreamEnded
| h2.events.StreamReset,
],
] = {}
# Connection terminated events are stored as state since
# we need to handle them for all streams.
self._connection_terminated: h2.events.ConnectionTerminated | None = None
self._read_exception: Exception | None = None
self._write_exception: Exception | None = None
async def handle_async_request(self, request: Request) -> Response:
if not self.can_handle_request(request.url.origin):
# This cannot occur in normal operation, since the connection pool
# will only send requests on connections that handle them.
# It's in place simply for resilience as a guard against incorrect
# usage, for anyone working directly with httpcore connections.
raise RuntimeError(
f"Attempted to send request to {request.url.origin} on connection "
f"to {self._origin}"
)
async with self._state_lock:
if self._state in (HTTPConnectionState.ACTIVE, HTTPConnectionState.IDLE):
self._request_count += 1
self._expire_at = None
self._state = HTTPConnectionState.ACTIVE
else:
raise ConnectionNotAvailable()
async with self._init_lock:
if not self._sent_connection_init:
try:
sci_kwargs = {"request": request}
async with Trace(
"send_connection_init", logger, request, sci_kwargs
):
await self._send_connection_init(**sci_kwargs)
except BaseException as exc:
with AsyncShieldCancellation():
await self.aclose()
raise exc
self._sent_connection_init = True
# Initially start with just 1 until the remote server provides
# its max_concurrent_streams value
self._max_streams = 1
local_settings_max_streams = (
self._h2_state.local_settings.max_concurrent_streams
)
self._max_streams_semaphore = AsyncSemaphore(local_settings_max_streams)
for _ in range(local_settings_max_streams - self._max_streams):
await self._max_streams_semaphore.acquire()
await self._max_streams_semaphore.acquire()
try:
stream_id = self._h2_state.get_next_available_stream_id()
self._events[stream_id] = []
except h2.exceptions.NoAvailableStreamIDError: # pragma: nocover
self._used_all_stream_ids = True
self._request_count -= 1
raise ConnectionNotAvailable()
try:
kwargs = {"request": request, "stream_id": stream_id}
async with Trace("send_request_headers", logger, request, kwargs):
await self._send_request_headers(request=request, stream_id=stream_id)
async with Trace("send_request_body", logger, request, kwargs):
await self._send_request_body(request=request, stream_id=stream_id)
async with Trace(
"receive_response_headers", logger, request, kwargs
) as trace:
status, headers = await self._receive_response(
request=request, stream_id=stream_id
)
trace.return_value = (status, headers)
return Response(
status=status,
headers=headers,
content=HTTP2ConnectionByteStream(self, request, stream_id=stream_id),
extensions={
"http_version": b"HTTP/2",
"network_stream": self._network_stream,
"stream_id": stream_id,
},
)
except BaseException as exc: # noqa: PIE786
with AsyncShieldCancellation():
kwargs = {"stream_id": stream_id}
async with Trace("response_closed", logger, request, kwargs):
await self._response_closed(stream_id=stream_id)
if isinstance(exc, h2.exceptions.ProtocolError):
# One case where h2 can raise a protocol error is when a
# closed frame has been seen by the state machine.
#
# This happens when one stream is reading, and encounters
# a GOAWAY event. Other flows of control may then raise
# a protocol error at any point they interact with the 'h2_state'.
#
# In this case we'll have stored the event, and should raise
# it as a RemoteProtocolError.
if self._connection_terminated: # pragma: nocover
raise RemoteProtocolError(self._connection_terminated)
# If h2 raises a protocol error in some other state then we
# must somehow have made a protocol violation.
raise LocalProtocolError(exc) # pragma: nocover
raise exc
async def _send_connection_init(self, request: Request) -> None:
"""
The HTTP/2 connection requires some initial setup before we can start
using individual request/response streams on it.
"""
# Need to set these manually here instead of manipulating via
# __setitem__() otherwise the H2Connection will emit SettingsUpdate
# frames in addition to sending the undesired defaults.
self._h2_state.local_settings = h2.settings.Settings(
client=True,
initial_values={
# Disable PUSH_PROMISE frames from the server since we don't do anything
# with them for now. Maybe when we support caching?
h2.settings.SettingCodes.ENABLE_PUSH: 0,
# These two are taken from h2 for safe defaults
h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS: 100,
h2.settings.SettingCodes.MAX_HEADER_LIST_SIZE: 65536,
},
)
# Some websites (*cough* Yahoo *cough*) balk at this setting being
# present in the initial handshake since it's not defined in the original
# RFC despite the RFC mandating ignoring settings you don't know about.
del self._h2_state.local_settings[
h2.settings.SettingCodes.ENABLE_CONNECT_PROTOCOL
]
self._h2_state.initiate_connection()
self._h2_state.increment_flow_control_window(2**24)
await self._write_outgoing_data(request)
# Sending the request...
async def _send_request_headers(self, request: Request, stream_id: int) -> None:
"""
Send the request headers to a given stream ID.
"""
end_stream = not has_body_headers(request)
# In HTTP/2 the ':authority' pseudo-header is used instead of 'Host'.
# In order to gracefully handle HTTP/1.1 and HTTP/2 we always require
# HTTP/1.1 style headers, and map them appropriately if we end up on
# an HTTP/2 connection.
authority = [v for k, v in request.headers if k.lower() == b"host"][0]
headers = [
(b":method", request.method),
(b":authority", authority),
(b":scheme", request.url.scheme),
(b":path", request.url.target),
] + [
(k.lower(), v)
for k, v in request.headers
if k.lower()
not in (
b"host",
b"transfer-encoding",
)
]
self._h2_state.send_headers(stream_id, headers, end_stream=end_stream)
self._h2_state.increment_flow_control_window(2**24, stream_id=stream_id)
await self._write_outgoing_data(request)
async def _send_request_body(self, request: Request, stream_id: int) -> None:
"""
Iterate over the request body sending it to a given stream ID.
"""
if not has_body_headers(request):
return
assert isinstance(request.stream, typing.AsyncIterable)
async for data in request.stream:
await self._send_stream_data(request, stream_id, data)
await self._send_end_stream(request, stream_id)
async def _send_stream_data(
self, request: Request, stream_id: int, data: bytes
) -> None:
"""
Send a single chunk of d |
|