Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
49 changes: 40 additions & 9 deletions aiohttp/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
import json
import os
import sys
import time
import traceback
import warnings
from collections import deque
from collections.abc import (
Awaitable,
Callable,
Expand Down Expand Up @@ -232,22 +234,51 @@ async def _connect_and_send_request(req: ClientRequest) -> ClientResponse:
connector = req._session._connector
assert connector is not None
try:
conn = await connector.connect(req, traces=req._traces, timeout=req._timeout)
async with connector.sem:
# at most just one
conn = await connector.connect(
req, traces=req._traces, timeout=req._timeout
)
except asyncio.TimeoutError as exc:
raise ConnectionTimeoutError(f"Connection timeout to host {req.url}") from exc

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

ssl_object = conn.protocol.transport.get_extra_info("ssl_object")
alpn_protocol = ssl_object.selected_alpn_protocol() if ssl_object else "http/1.1"

resp = None
started = False

if alpn_protocol == "h2" and not connector._conns[conn._key]:
connector._conns[conn._key] = deque([(conn.protocol, time.monotonic())])
try:
resp = await req._send(conn)
try:
# backwards compatibility
if alpn_protocol == "h2":
body = await req.body.as_bytes()
resp = await conn.protocol.send( # type: ignore[attr-defined]
req.method,
req.url,
list(req.headers.items()),
body,
)

# we are done with the connection
conn._protocol = None
else:
conn.protocol.set_response_params(**req._response_params)
resp = await req._send(conn)
await resp.start(conn)
except BaseException:
# h3 not implemented

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
22 changes: 17 additions & 5 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 @@ -51,6 +52,7 @@
set_exception,
set_result,
)
from .http_protocol import HttpDispatcherProtocol
from .log import client_logger
from .resolver import DefaultResolver

Expand Down Expand Up @@ -292,7 +294,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 @@ -319,6 +321,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.sem = asyncio.Semaphore(1)

def __del__(self, _warnings: Any = warnings) -> None:
if self._closed:
return
Expand Down Expand Up @@ -856,7 +864,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))
return sslcontext


Expand Down Expand Up @@ -1403,7 +1415,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 @@ -1495,7 +1508,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 @@ -1627,7 +1639,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