Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
4badcd8
Add support for HTTP/2
Moist-Cat Jul 2, 2026
8d6ffe2
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 2, 2026
1b21c5b
Merge branch 'master' into master
Moist-Cat Jul 4, 2026
982ad3a
Lint code and add test
Moist-Cat Jul 4, 2026
2ecae5a
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 4, 2026
73bfa16
Fix lint errors
Moist-Cat Jul 4, 2026
9bd6ff9
Add more tests.
Moist-Cat Jul 5, 2026
91b46d8
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 5, 2026
0b361d3
Fix type issues
Moist-Cat Jul 5, 2026
9ab172f
Fix protocol violations and perform functional tests
Moist-Cat Jul 5, 2026
38b9d3d
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 5, 2026
538164e
More tests
Moist-Cat Jul 5, 2026
2ce9b41
Misc changes
Moist-Cat Jul 6, 2026
fdb10b7
Merge branch 'aio-libs:master' into master
Moist-Cat Jul 8, 2026
e312859
Merge branch 'aio-libs:master' into master
Moist-Cat Jul 14, 2026
93aef89
Release connection in the HTTP/2 path
Moist-Cat Jul 8, 2026
23997c4
Merge branch 'master' into master
Moist-Cat Jul 27, 2026
d78d47a
Merge branch 'aio-libs:master' into master
Moist-Cat Jul 28, 2026
35eb119
Merge branch 'aio-libs:master' into master
Moist-Cat Aug 3, 2026
742899f
Merge branch 'aio-libs:master' into master
Moist-Cat Aug 4, 2026
44de092
Merge branch 'master' into master
Moist-Cat Aug 6, 2026
0f9f4d8
Merge branch 'master' into master
Moist-Cat Aug 10, 2026
979bb4a
Merge branch 'aio-libs:master' into master
Moist-Cat Aug 11, 2026
51a5ee2
Update response with base class
Moist-Cat Jul 15, 2026
1d10b19
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 11, 2026
519c830
Merge branch 'aio-libs:master' into master
Moist-Cat Aug 12, 2026
44811d3
Merge branch 'aio-libs:master' into master
Moist-Cat Aug 13, 2026
d242ad7
Merge branch 'master' into master
Moist-Cat Aug 15, 2026
5f57683
Merge branch 'aio-libs:master' into master
Moist-Cat Aug 20, 2026
d14a1c7
First working version after changes
Moist-Cat Aug 20, 2026
314a6e5
Remove unneeded response file.
Moist-Cat Aug 20, 2026
38c11ff
Fix type errors
Moist-Cat Aug 20, 2026
899742e
Merge branch 'aio-libs:master' into master
Moist-Cat Aug 21, 2026
2c0493a
Fix type errors and compatibility issues with py < 3.14
Moist-Cat Aug 21, 2026
088eb38
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 21, 2026
d40dc83
Merge branch 'master' into master
Moist-Cat Aug 25, 2026
1028739
Merge branch 'master' into master
Moist-Cat Aug 27, 2026
bc9eb29
Add tests for new code paths
Moist-Cat Aug 27, 2026
d2941d2
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 27, 2026
d8dc63a
Merge branch 'master' into master
Moist-Cat Aug 28, 2026
8fdf2e4
Merge branch 'master' into master
Moist-Cat Aug 29, 2026
af86d82
Merge branch 'master' into master
Moist-Cat Aug 31, 2026
e70d672
Merge branch 'master' into master
Moist-Cat Aug 31, 2026
5f1dc50
Merge branch 'master' into master
Moist-Cat Sep 1, 2026
1f1b8db
Add new synchronization mechanism and extra tests
Moist-Cat Sep 1, 2026
9f9d127
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 1, 2026
a5124c0
flake8
Moist-Cat Sep 2, 2026
7e8daa3
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 49 additions & 9 deletions aiohttp/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
strip_auth_from_url,
)
from .http import WS_KEY, HttpVersion, WebSocketReader, WebSocketWriter
from .http2.adapter import get_version
from .http_websocket import WSHandshakeError, ws_ext_gen, ws_ext_parse
from .tracing import Trace, TraceConfig
from .typedefs import (
Expand Down Expand Up @@ -234,23 +235,62 @@ class _WSConnectOptions(TypedDict, total=False):
async def _connect_and_send_request(req: ClientRequest) -> ClientResponse:
connector = req._session._connector
assert connector is not None
key = req.connection_key
try:
# only the first connection to a host blocks
# the rest of the connection requests are done
# concurrently
await connector.semaphore.acquire(key)

conn = await connector.connect(req, traces=req._traces, timeout=req._timeout)

connector.semaphore.release(key)
except asyncio.TimeoutError as exc:
raise ConnectionTimeoutError(f"Connection timeout to host {req.url}") from exc
finally:
connector.semaphore.release(key)

assert conn.protocol is not None
conn.protocol.set_response_params(**req._response_params)
assert conn.protocol.transport is not None

alpn_protocol = get_version(conn.protocol)

resp = None
started = False

if alpn_protocol == "h2":
# release immediately to allow reuse
connector._release(conn._key, conn.protocol, should_close=False)
# the protocol corresponding to the connection
# remains (i.e., the count per host is always 1 for h2)
# This is the number of TCP connections not the number of
# streams
connector._acquired.add(conn.protocol)
try:
resp = await req._send(conn)
try:
await resp.start(conn)
except BaseException:
# backwards compatibility
if alpn_protocol == "h2":
stream = await conn.protocol.create_stream() # type: ignore[attr-defined]
req.stream_id = stream.stream_id
# release again to clear the protocol from _acquired if required
connector._release(conn._key, conn.protocol, should_close=False)
resp = await req._send(conn)
resp.stream_id = stream.stream_id
else:
conn.protocol.set_response_params(**req._response_params)
resp = await req._send(conn)
await resp.start(conn)

if alpn_protocol == "h2":
# we still have to null the protocol since we didn't close the connection
conn._protocol = None

started = True
finally:
if resp is not None and not started:
resp.close()
raise
except BaseException:
conn.close()
raise
conn.close()
if resp is None:
conn.close()
return resp


Expand Down
25 changes: 22 additions & 3 deletions aiohttp/client_reqrep.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
HttpVersion11,
StreamWriter,
)
from .http2.adapter import Http2StreamWriter, get_version
from .streams import EMPTY_PAYLOAD, StreamReader
from .typedefs import DEFAULT_JSON_DECODER, JSONDecoder, RawHeaders

Expand Down Expand Up @@ -280,6 +281,9 @@ class ClientResponse(HeadersMixin):
_output_size: int = 0
_upload_complete: asyncio.Future[None] | None = None

# HTTP/2 stream id
stream_id: int | None = None

def __init__(
self,
method: str,
Expand Down Expand Up @@ -525,7 +529,13 @@ async def start(self, connection: "Connection") -> "ClientResponse":
# read response
try:
protocol = self._protocol
message, payload = await protocol.read() # type: ignore[union-attr]
# conditional branching to pass the stream id
# to the protocol
assert protocol is not None
if get_version(protocol) == "h2":
message, payload = await protocol.read_stream(self.stream_id) # type: ignore[attr-defined]
else:
message, payload = await protocol.read()
except HttpProcessingError as exc:
raise ClientResponseError(
self.request_info,
Expand Down Expand Up @@ -810,6 +820,9 @@ class ClientRequestBase:

_skip_auto_headers: "CIMultiDict[None] | None" = None

# HTTP/2 stream id
stream_id: int | None = None

# N.B.
# Adding __del__ method with self._writer closing doesn't make sense
# because _writer is instance method, thus it keeps a reference to self.
Expand Down Expand Up @@ -932,7 +945,9 @@ def _create_response(
stream_writer=stream_writer,
)

def _create_writer(self, protocol: BaseProtocol) -> StreamWriter:
def _create_writer(
self, protocol: BaseProtocol
) -> StreamWriter | Http2StreamWriter:
return StreamWriter(protocol, self.loop)

def _should_write(self, protocol: BaseProtocol) -> bool:
Expand Down Expand Up @@ -1428,7 +1443,11 @@ def _create_response(
stream_writer=stream_writer,
)

def _create_writer(self, protocol: BaseProtocol) -> StreamWriter:
def _create_writer(
self, protocol: BaseProtocol
) -> StreamWriter | Http2StreamWriter:
if get_version(protocol) == "h2":
return Http2StreamWriter(protocol, self.loop, self)
writer = StreamWriter(
protocol,
self.loop,
Expand Down
25 changes: 19 additions & 6 deletions aiohttp/connector.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import asyncio
import functools
import os
import random
import socket
import sys
Expand Down Expand Up @@ -52,6 +53,8 @@
set_exception,
set_result,
)
from .http2.synchro import HostProbeSynchronizer
from .http_protocol import HttpDispatcherProtocol
from .log import client_logger
from .resolver import DefaultResolver

Expand Down Expand Up @@ -138,7 +141,7 @@ async def create_connection(
async def start_tls(
loop: asyncio.AbstractEventLoop,
transport: asyncio.Transport,
protocol: ResponseHandler,
protocol: HttpDispatcherProtocol | ResponseHandler,
sslcontext: SSLContext,
*,
server_hostname: str | None,
Expand Down Expand Up @@ -375,7 +378,7 @@ def __init__(
] = defaultdict(OrderedDict)

self._loop = loop
self._factory = functools.partial(ResponseHandler, loop=loop)
self._factory = functools.partial(HttpDispatcherProtocol, loop=loop)

# start keep-alive connection cleanup task
self._cleanup_handle: asyncio.TimerHandle | None = None
Expand All @@ -402,6 +405,12 @@ def __init__(
self._placeholder_future.set_result(None)
self._cleanup_closed()

# Semaphore for HTTP/2 connections
# avoids duplicate connections to the
# same host
# (HTTP/2 doesn't need connection pooling to send multiple requests)
self.semaphore = HostProbeSynchronizer()

def __del__(self, _warnings: Any = warnings) -> None:
if self._closed:
return
Expand Down Expand Up @@ -939,7 +948,11 @@ def _make_ssl_context(verified: bool) -> SSLContext:
sslcontext.verify_mode = ssl.CERT_NONE
sslcontext.options |= ssl.OP_NO_COMPRESSION
sslcontext.set_default_verify_paths()
sslcontext.set_alpn_protocols(("http/1.1",))

protocols = ["http/1.1"]
if os.getenv("AIOHTTP_ENABLE_EXPERIMENTAL_PROTOCOLS", False):
protocols += ["h2"]
sslcontext.set_alpn_protocols(tuple(protocols))
Comment on lines 948 to +955

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 HTTP/2 feature lacks documentation

The new environment-variable opt-in changes ALPN negotiation and introduces user-visible HTTP/2 behavior and limitations, but the PR adds no client reference or narrative documentation, leaving users without shipped guidance for enabling or evaluating the feature.

Context Used: AGENTS.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

return sslcontext


Expand Down Expand Up @@ -1499,7 +1512,8 @@ async def _start_tls_connection(
tls_transport
) # Kick the state machine of the new TLS protocol

return tls_transport, tls_proto
# HACK use the correct type
return tls_transport, tls_proto # type: ignore[return-value]

def _convert_hosts_to_addr_infos(
self, hosts: list[ResolveResult]
Expand Down Expand Up @@ -1591,7 +1605,6 @@ async def _create_direct_connection(
bad_peer = sock.getpeername()
aiohappyeyeballs.remove_addr_infos(addr_infos, bad_peer)
continue

return transp, proto
assert last_exc is not None
raise last_exc
Expand Down Expand Up @@ -1723,7 +1736,7 @@ async def _create_connection(
raise
raise UnixClientConnectorError(self.path, req.connection_key, exc) from exc

return proto
return proto # type: ignore[return-value]


class NamedPipeConnector(BaseConnector):
Expand Down
1 change: 0 additions & 1 deletion aiohttp/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@

from . import hdrs
from .log import client_logger
from .typedefs import PathLike # noqa

if sys.version_info >= (3, 11):
import asyncio as async_timeout
Expand Down
Loading
Loading