Skip to content

Commit 876e722

Browse files
Update to httpcore 0.10 (#1126)
* Keep HTTPError as a base class for .request() and .raise_for_status() * Updates for httpcore 0.10 * Update httpx/_exceptions.py Co-authored-by: Stephen Brown II <Stephen.Brown2@gmail.com> * Use httpcore.SimpleByteStream/httpcore.IteratorByteStream * Use httpcore.PlainByteStream * Merge master * Update to httpcore 0.10.x Co-authored-by: Stephen Brown II <Stephen.Brown2@gmail.com>
1 parent 0a38695 commit 876e722

19 files changed

Lines changed: 108 additions & 115 deletions

httpx/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@
1212
HTTPError,
1313
HTTPStatusError,
1414
InvalidURL,
15+
LocalProtocolError,
1516
NetworkError,
1617
NotRedirectResponse,
1718
PoolTimeout,
1819
ProtocolError,
1920
ProxyError,
2021
ReadError,
2122
ReadTimeout,
23+
RemoteProtocolError,
2224
RequestBodyUnavailable,
2325
RequestError,
2426
RequestNotRead,
@@ -29,6 +31,7 @@
2931
TimeoutException,
3032
TooManyRedirects,
3133
TransportError,
34+
UnsupportedProtocol,
3235
WriteError,
3336
WriteTimeout,
3437
)
@@ -72,6 +75,9 @@
7275
"HTTPError",
7376
"HTTPStatusError",
7477
"InvalidURL",
78+
"UnsupportedProtocol",
79+
"LocalProtocolError",
80+
"RemoteProtocolError",
7581
"NetworkError",
7682
"NotRedirectResponse",
7783
"PoolTimeout",

httpx/_client.py

Lines changed: 8 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
from ._content_streams import ContentStream
2020
from ._exceptions import (
2121
HTTPCORE_EXC_MAP,
22-
InvalidURL,
2322
RequestBodyUnavailable,
2423
TooManyRedirects,
2524
map_exceptions,
@@ -44,7 +43,6 @@
4443
from ._utils import (
4544
NetRCInfo,
4645
URLPattern,
47-
enforce_http_url,
4846
get_environment_proxies,
4947
get_logger,
5048
same_origin,
@@ -344,11 +342,6 @@ def _redirect_url(self, request: Request, response: Response) -> URL:
344342

345343
url = URL(location)
346344

347-
# Check that we can handle the scheme
348-
if url.scheme and url.scheme not in ("http", "https"):
349-
message = f'Scheme "{url.scheme}" not supported.'
350-
raise InvalidURL(message, request=request)
351-
352345
# Handle malformed 'Location' headers that are "absolute" form, have no host.
353346
# See: https://github.com/encode/httpx/issues/771
354347
if url.scheme and not url.host:
@@ -540,8 +533,8 @@ def _init_transport(
540533

541534
return httpcore.SyncConnectionPool(
542535
ssl_context=ssl_context,
543-
max_keepalive=limits.max_keepalive,
544536
max_connections=limits.max_connections,
537+
max_keepalive_connections=limits.max_keepalive_connections,
545538
keepalive_expiry=KEEPALIVE_EXPIRY,
546539
http2=http2,
547540
)
@@ -562,20 +555,17 @@ def _init_proxy_transport(
562555
proxy_headers=proxy.headers.raw,
563556
proxy_mode=proxy.mode,
564557
ssl_context=ssl_context,
565-
max_keepalive=limits.max_keepalive,
566558
max_connections=limits.max_connections,
559+
max_keepalive_connections=limits.max_keepalive_connections,
567560
keepalive_expiry=KEEPALIVE_EXPIRY,
568561
http2=http2,
569562
)
570563

571-
def _transport_for_url(self, request: Request) -> httpcore.SyncHTTPTransport:
564+
def _transport_for_url(self, url: URL) -> httpcore.SyncHTTPTransport:
572565
"""
573566
Returns the transport instance that should be used for a given URL.
574567
This will either be the standard connection pool, or a proxy.
575568
"""
576-
url = request.url
577-
enforce_http_url(request)
578-
579569
for pattern, transport in self._proxies.items():
580570
if pattern.matches(url):
581571
return self._transport if transport is None else transport
@@ -620,10 +610,6 @@ def send(
620610
allow_redirects: bool = True,
621611
timeout: typing.Union[TimeoutTypes, UnsetType] = UNSET,
622612
) -> Response:
623-
if request.url.scheme not in ("http", "https"):
624-
message = 'URL scheme must be "http" or "https".'
625-
raise InvalidURL(message, request=request)
626-
627613
timeout = self.timeout if isinstance(timeout, UnsetType) else Timeout(timeout)
628614

629615
auth = self._build_auth(request, auth)
@@ -714,7 +700,7 @@ def _send_single_request(self, request: Request, timeout: Timeout) -> Response:
714700
"""
715701
Sends a single request, without handling any redirections.
716702
"""
717-
transport = self._transport_for_url(request)
703+
transport = self._transport_for_url(request.url)
718704

719705
with map_exceptions(HTTPCORE_EXC_MAP, request=request):
720706
(
@@ -1072,8 +1058,8 @@ def _init_transport(
10721058

10731059
return httpcore.AsyncConnectionPool(
10741060
ssl_context=ssl_context,
1075-
max_keepalive=limits.max_keepalive,
10761061
max_connections=limits.max_connections,
1062+
max_keepalive_connections=limits.max_keepalive_connections,
10771063
keepalive_expiry=KEEPALIVE_EXPIRY,
10781064
http2=http2,
10791065
)
@@ -1094,20 +1080,17 @@ def _init_proxy_transport(
10941080
proxy_headers=proxy.headers.raw,
10951081
proxy_mode=proxy.mode,
10961082
ssl_context=ssl_context,
1097-
max_keepalive=limits.max_keepalive,
10981083
max_connections=limits.max_connections,
1084+
max_keepalive_connections=limits.max_keepalive_connections,
10991085
keepalive_expiry=KEEPALIVE_EXPIRY,
11001086
http2=http2,
11011087
)
11021088

1103-
def _transport_for_url(self, request: Request) -> httpcore.AsyncHTTPTransport:
1089+
def _transport_for_url(self, url: URL) -> httpcore.AsyncHTTPTransport:
11041090
"""
11051091
Returns the transport instance that should be used for a given URL.
11061092
This will either be the standard connection pool, or a proxy.
11071093
"""
1108-
url = request.url
1109-
enforce_http_url(request)
1110-
11111094
for pattern, transport in self._proxies.items():
11121095
if pattern.matches(url):
11131096
return self._transport if transport is None else transport
@@ -1245,7 +1228,7 @@ async def _send_single_request(
12451228
"""
12461229
Sends a single request, without handling any redirections.
12471230
"""
1248-
transport = self._transport_for_url(request)
1231+
transport = self._transport_for_url(request.url)
12491232

12501233
with map_exceptions(HTTPCORE_EXC_MAP, request=request):
12511234
(

httpx/_config.py

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -323,42 +323,53 @@ class Limits:
323323
324324
**Parameters:**
325325
326-
* **max_keepalive** - Allow the connection pool to maintain keep-alive connections
327-
below this point.
328326
* **max_connections** - The maximum number of concurrent connections that may be
329-
established.
327+
established.
328+
* **max_keepalive_connections** - Allow the connection pool to maintain
329+
keep-alive connections below this point. Should be less than or equal
330+
to `max_connections`.
330331
"""
331332

332333
def __init__(
333-
self, *, max_keepalive: int = None, max_connections: int = None,
334+
self,
335+
*,
336+
max_connections: int = None,
337+
max_keepalive_connections: int = None,
338+
# Deprecated parameter naming, in favour of more explicit version:
339+
max_keepalive: int = None,
334340
):
335-
self.max_keepalive = max_keepalive
341+
if max_keepalive is not None:
342+
warnings.warn(
343+
"'max_keepalive' is deprecated. Use 'max_keepalive_connections'.",
344+
DeprecationWarning,
345+
)
346+
max_keepalive_connections = max_keepalive
347+
336348
self.max_connections = max_connections
349+
self.max_keepalive_connections = max_keepalive_connections
337350

338351
def __eq__(self, other: typing.Any) -> bool:
339352
return (
340353
isinstance(other, self.__class__)
341-
and self.max_keepalive == other.max_keepalive
342354
and self.max_connections == other.max_connections
355+
and self.max_keepalive_connections == other.max_keepalive_connections
343356
)
344357

345358
def __repr__(self) -> str:
346359
class_name = self.__class__.__name__
347360
return (
348-
f"{class_name}(max_keepalive={self.max_keepalive}, "
349-
f"max_connections={self.max_connections})"
361+
f"{class_name}(max_connections={self.max_connections}, "
362+
f"max_keepalive_connections={self.max_keepalive_connections})"
350363
)
351364

352365

353366
class PoolLimits(Limits):
354-
def __init__(
355-
self, *, max_keepalive: int = None, max_connections: int = None,
356-
) -> None:
367+
def __init__(self, **kwargs: typing.Any) -> None:
357368
warn_deprecated(
358369
"httpx.PoolLimits(...) is deprecated and will raise errors in the future. "
359370
"Use httpx.Limits(...) instead."
360371
)
361-
super().__init__(max_keepalive=max_keepalive, max_connections=max_connections)
372+
super().__init__(**kwargs)
362373

363374

364375
class Proxy:

httpx/_exceptions.py

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@
1515
· WriteError
1616
· CloseError
1717
- ProtocolError
18+
· LocalProtocolError
19+
· RemoteProtocolError
1820
- ProxyError
21+
- UnsupportedProtocol
1922
+ DecodingError
2023
+ TooManyRedirects
2124
+ RequestBodyUnavailable
22-
+ InvalidURL
2325
x HTTPStatusError
2426
* NotRedirectResponse
2527
* CookieConflict
@@ -153,9 +155,35 @@ class ProxyError(TransportError):
153155
"""
154156

155157

158+
class UnsupportedProtocol(TransportError):
159+
"""
160+
Attempted to make a request to an unsupported protocol.
161+
162+
For example issuing a request to `ftp://www.example.com`.
163+
"""
164+
165+
156166
class ProtocolError(TransportError):
157167
"""
158-
A protocol was violated by the server.
168+
The protocol was violated.
169+
"""
170+
171+
172+
class LocalProtocolError(ProtocolError):
173+
"""
174+
A protocol was violated by the client.
175+
176+
For example if the user instantiated a `Request` instance explicitly,
177+
failed to include the mandatory `Host:` header, and then issued it directly
178+
using `client.send()`.
179+
"""
180+
181+
182+
class RemoteProtocolError(ProtocolError):
183+
"""
184+
The protocol was violated by the server.
185+
186+
For exaample, returning malformed HTTP.
159187
"""
160188

161189

@@ -181,12 +209,6 @@ class RequestBodyUnavailable(RequestError):
181209
"""
182210

183211

184-
class InvalidURL(RequestError):
185-
"""
186-
URL was missing a hostname, or was not one of HTTP/HTTPS.
187-
"""
188-
189-
190212
# Client errors
191213

192214

@@ -297,6 +319,14 @@ def __init__(self) -> None:
297319
super().__init__(message)
298320

299321

322+
# The `InvalidURL` class is no longer required. It was being used to enforce only
323+
# 'http'/'https' URLs being requested, but is now treated instead at the
324+
# transport layer using `UnsupportedProtocol()`.`
325+
326+
# We are currently still exposing this class, but it will be removed in 1.0.
327+
InvalidURL = UnsupportedProtocol
328+
329+
300330
@contextlib.contextmanager
301331
def map_exceptions(
302332
mapping: typing.Mapping[typing.Type[Exception], typing.Type[Exception]],
@@ -335,5 +365,8 @@ def map_exceptions(
335365
httpcore.WriteError: WriteError,
336366
httpcore.CloseError: CloseError,
337367
httpcore.ProxyError: ProxyError,
368+
httpcore.UnsupportedProtocol: UnsupportedProtocol,
338369
httpcore.ProtocolError: ProtocolError,
370+
httpcore.LocalProtocolError: LocalProtocolError,
371+
httpcore.RemoteProtocolError: RemoteProtocolError,
339372
}

httpx/_transports/asgi.py

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,4 @@
1-
from typing import (
2-
TYPE_CHECKING,
3-
AsyncIterator,
4-
Callable,
5-
Dict,
6-
List,
7-
Optional,
8-
Tuple,
9-
Union,
10-
)
1+
from typing import TYPE_CHECKING, Callable, List, Mapping, Optional, Tuple, Union
112

123
import httpcore
134
import sniffio
@@ -31,10 +22,6 @@ def create_event() -> "Event":
3122
return asyncio.Event()
3223

3324

34-
async def async_byte_iterator(bytestring: bytes) -> AsyncIterator[bytes]:
35-
yield bytestring
36-
37-
3825
class ASGITransport(httpcore.AsyncHTTPTransport):
3926
"""
4027
A custom AsyncTransport that handles sending requests directly to an ASGI app.
@@ -86,14 +73,10 @@ async def request(
8673
url: Tuple[bytes, bytes, Optional[int], bytes],
8774
headers: List[Tuple[bytes, bytes]] = None,
8875
stream: httpcore.AsyncByteStream = None,
89-
timeout: Dict[str, Optional[float]] = None,
76+
timeout: Mapping[str, Optional[float]] = None,
9077
) -> Tuple[bytes, int, bytes, List[Tuple[bytes, bytes]], httpcore.AsyncByteStream]:
9178
headers = [] if headers is None else headers
92-
stream = (
93-
httpcore.AsyncByteStream(async_byte_iterator(b""))
94-
if stream is None
95-
else stream
96-
)
79+
stream = httpcore.PlainByteStream(content=b"") if stream is None else stream
9780

9881
# ASGI scope.
9982
scheme, host, port, full_path = url
@@ -170,8 +153,6 @@ async def send(message: dict) -> None:
170153
assert status_code is not None
171154
assert response_headers is not None
172155

173-
response_body = b"".join(body_parts)
174-
175-
stream = httpcore.AsyncByteStream(async_byte_iterator(response_body))
156+
stream = httpcore.PlainByteStream(content=b"".join(body_parts))
176157

177158
return (b"HTTP/1.1", status_code, b"", response_headers, stream)

httpx/_transports/urllib3.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import socket
2-
from typing import Dict, Iterator, List, Optional, Tuple
2+
from typing import Iterator, List, Mapping, Optional, Tuple
33

44
import httpcore
55

@@ -45,7 +45,7 @@ def request(
4545
url: Tuple[bytes, bytes, Optional[int], bytes],
4646
headers: List[Tuple[bytes, bytes]] = None,
4747
stream: httpcore.SyncByteStream = None,
48-
timeout: Dict[str, Optional[float]] = None,
48+
timeout: Mapping[str, Optional[float]] = None,
4949
) -> Tuple[bytes, int, bytes, List[Tuple[bytes, bytes]], httpcore.SyncByteStream]:
5050
headers = [] if headers is None else headers
5151
stream = ByteStream(b"") if stream is None else stream

0 commit comments

Comments
 (0)