|
from __future__ import annotations |
|
|
|
import errno |
|
import logging |
|
import queue |
|
import sys |
|
import typing |
|
import warnings |
|
import weakref |
|
from socket import timeout as SocketTimeout |
|
from types import TracebackType |
|
|
|
from ._base_connection import _TYPE_BODY |
|
from ._collections import HTTPHeaderDict |
|
from ._request_methods import RequestMethods |
|
from .connection import ( |
|
BaseSSLError, |
|
BrokenPipeError, |
|
DummyConnection, |
|
HTTPConnection, |
|
HTTPException, |
|
HTTPSConnection, |
|
ProxyConfig, |
|
_wrap_proxy_error, |
|
) |
|
from .connection import port_by_scheme as port_by_scheme |
|
from .exceptions import ( |
|
ClosedPoolError, |
|
EmptyPoolError, |
|
FullPoolError, |
|
HostChangedError, |
|
InsecureRequestWarning, |
|
LocationValueError, |
|
MaxRetryError, |
|
NewConnectionError, |
|
ProtocolError, |
|
ProxyError, |
|
ReadTimeoutError, |
|
SSLError, |
|
TimeoutError, |
|
) |
|
from .response import BaseHTTPResponse |
|
from .util.connection import is_connection_dropped |
|
from .util.proxy import connection_requires_http_tunnel |
|
from .util.request import _TYPE_BODY_POSITION, set_file_position |
|
from .util.retry import Retry |
|
from .util.ssl_match_hostname import CertificateError |
|
from .util.timeout import _DEFAULT_TIMEOUT, _TYPE_DEFAULT, Timeout |
|
from .util.url import Url, _encode_target |
|
from .util.url import _normalize_host as normalize_host |
|
from .util.url import parse_url |
|
from .util.util import to_str |
|
|
|
if typing.TYPE_CHECKING: |
|
import ssl |
|
from typing import Literal |
|
|
|
from ._base_connection import BaseHTTPConnection, BaseHTTPSConnection |
|
|
|
log = logging.getLogger(__name__) |
|
|
|
_TYPE_TIMEOUT = typing.Union[Timeout, float, _TYPE_DEFAULT, None] |
|
|
|
_SelfT = typing.TypeVar("_SelfT") |
|
|
|
|
|
|
|
class ConnectionPool: |
|
""" |
|
Base class for all connection pools, such as |
|
:class:`.HTTPConnectionPool` and :class:`.HTTPSConnectionPool`. |
|
|
|
.. note:: |
|
ConnectionPool.urlopen() does not normalize or percent-encode target URIs |
|
which is useful if your target server doesn't support percent-encoded |
|
target URIs. |
|
""" |
|
|
|
scheme: str | None = None |
|
QueueCls = queue.LifoQueue |
|
|
|
def __init__(self, host: str, port: int | None = None) -> None: |
|
if not host: |
|
raise LocationValueError("No host specified.") |
|
|
|
self.host = _normalize_host(host, scheme=self.scheme) |
|
self.port = port |
|
|
|
|
|
|
|
|
|
|
|
self._tunnel_host = normalize_host(host, scheme=self.scheme).lower() |
|
|
|
def __str__(self) -> str: |
|
return f"{type(self).__name__}(host={self.host!r}, port={self.port!r})" |
|
|
|
def __enter__(self: _SelfT) -> _SelfT: |
|
return self |
|
|
|
def __exit__( |
|
self, |
|
exc_type: type[BaseException] | None, |
|
exc_val: BaseException | None, |
|
exc_tb: TracebackType | None, |
|
) -> Literal[False]: |
|
self.close() |
|
|
|
return False |
|
|
|
def close(self) -> None: |
|
""" |
|
Close all pooled connections and disable the pool. |
|
""" |
|
|
|
|
|
|
|
_blocking_errnos = {errno.EAGAIN, errno.EWOULDBLOCK} |
|
|
|
|
|
class HTTPConnectionPool(ConnectionPool, RequestMethods): |
|
""" |
|
Thread-safe connection pool for one host. |
|
|
|
:param host: |
|
Host used for this HTTP Connection (e.g. "localhost"), passed into |
|
:class:`http.client.HTTPConnection`. |
|
|
|
:param port: |
|
Port used for this HTTP Connection (None is equivalent to 80), passed |
|
into :class:`http.client.HTTPConnection`. |
|
|
|
:param timeout: |
|
Socket timeout in seconds for each individual connection. This can |
|
be a float or integer, which sets the timeout for the HTTP request, |
|
or an instance of :class:`urllib3.util.Timeout` which gives you more |
|
fine-grained control over request timeouts. After the constructor has |
|
been parsed, this is always a `urllib3.util.Timeout` object. |
|
|
|
:param maxsize: |
|
Number of connections to save that can be reused. More than 1 is useful |
|
in multithreaded situations. If ``block`` is set to False, more |
|
connections will be created but they will not be saved once they've |
|
been used. |
|
|
|
:param block: |
|
If set to True, no more than ``maxsize`` connections will be used at |
|
a time. When no free connections are available, the call will block |
|
until a connection has been released. This is a useful side effect for |
|
particular multithreaded situations where one does not want to use more |
|
than maxsize connections per host to prevent flooding. |
|
|
|
:param headers: |
|
Headers to include with all requests, unless other headers are given |
|
explicitly. |
|
|
|
:param retries: |
|
Retry configuration to use by default with requests in this pool. |
|
|
|
:param _proxy: |
|
Parsed proxy URL, should not be used directly, instead, see |
|
:class:`urllib3.ProxyManager` |
|
|
|
:param _proxy_headers: |
|
A dictionary with proxy headers, should not be used directly, |
|
instead, see :class:`urllib3.ProxyManager` |
|
|
|
:param \\**conn_kw: |
|
Additional parameters are used to create fresh :class:`urllib3.connection.HTTPConnection`, |
|
:class:`urllib3.connection.HTTPSConnection` instances. |
|
""" |
|
|
|
scheme = "http" |
|
ConnectionCls: ( |
|
type[BaseHTTPConnection] | type[BaseHTTPSConnection] |
|
) = HTTPConnection |
|
|
|
def __init__( |
|
self, |
|
host: str, |
|
port: int | None = None, |
|
timeout: _TYPE_TIMEOUT | None = _DEFAULT_TIMEOUT, |
|
maxsize: int = 1, |
|
block: bool = False, |
|
headers: typing.Mapping[str, str] | None = None, |
|
retries: Retry | bool | int | None = None, |
|
_proxy: Url | None = None, |
|
_proxy_headers: typing.Mapping[str, str] | None = None, |
|
_proxy_config: ProxyConfig | None = None, |
|
**conn_kw: typing.Any, |
|
): |
|
ConnectionPool.__init__(self, host, port) |
|
RequestMethods.__init__(self, headers) |
|
|
|
if not isinstance(timeout, Timeout): |
|
timeout = Timeout.from_float(timeout) |
|
|
|
if retries is None: |
|
retries = Retry.DEFAULT |
|
|
|
self.timeout = timeout |
|
self.retries = retries |
|
|
|
self.pool: queue.LifoQueue[typing.Any] | None = self.QueueCls(maxsize) |
|
self.block = block |
|
|
|
self.proxy = _proxy |
|
self.proxy_headers = _proxy_headers or {} |
|
self.proxy_config = _proxy_config |
|
|
|
|
|
for _ in range(maxsize): |
|
self.pool.put(None) |
|
|
|
|
|
self.num_connections = 0 |
|
self.num_requests = 0 |
|
self.conn_kw = conn_kw |
|
|
|
if self.proxy: |
|
|
|
|
|
|
|
self.conn_kw.setdefault("socket_options", []) |
|
|
|
self.conn_kw["proxy"] = self.proxy |
|
self.conn_kw["proxy_config"] = self.proxy_config |
|
|
|
|
|
|
|
|
|
|
|
pool = self.pool |
|
|
|
|
|
|
|
weakref.finalize(self, _close_pool_connections, pool) |
|
|
|
def _new_conn(self) -> BaseHTTPConnection: |
|
""" |
|
Return a fresh :class:`HTTPConnection`. |
|
""" |
|
self.num_connections += 1 |
|
log.debug( |
|
"Starting new HTTP connection (%d): %s:%s", |
|
self.num_connections, |
|
self.host, |
|
self.port or "80", |
|
) |
|
|
|
conn = self.ConnectionCls( |
|
host=self.host, |
|
port=self.port, |
|
timeout=self.timeout.connect_timeout, |
|
**self.conn_kw, |
|
) |
|
return conn |
|
|
|
def _get_conn(self, timeout: float | None = None) -> BaseHTTPConnection: |
|
""" |
|
Get a connection. Will return a pooled connection if one is available. |
|
|
|
If no connections are available and :prop:`.block` is ``False``, then a |
|
fresh connection is returned. |
|
|
|
:param timeout: |
|
Seconds to wait before giving up and raising |
|
:class:`urllib3.exceptions.EmptyPoolError` if the pool is empty and |
|
:prop:`.block` is ``True``. |
|
""" |
|
conn = None |
|
|
|
if self.pool is None: |
|
raise ClosedPoolError(self, "Pool is closed.") |
|
|
|
try: |
|
conn = self.pool.get(block=self.block, timeout=timeout) |
|
|
|
except AttributeError: |
|
raise ClosedPoolError(self, "Pool is closed.") from None |
|
|
|
except queue.Empty: |
|
if self.block: |
|
raise EmptyPoolError( |
|
self, |
|
"Pool is empty and a new connection can't be opened due to blocking mode.", |
|
) from None |
|
pass |
|
|
|
|
|
if conn and is_connection_dropped(conn): |
|
log.debug("Resetting dropped connection: %s", self.host) |
|
conn.close() |
|
|
|
return conn or self._new_conn() |
|
|
|
def _put_conn(self, conn: BaseHTTPConnection | None) -> None: |
|
""" |
|
Put a connection back into the pool. |
|
|
|
:param conn: |
|
Connection object for the current host and port as returned by |
|
:meth:`._new_conn` or :meth:`._get_conn`. |
|
|
|
If the pool is already full, the connection is closed and discarded |
|
because we exceeded maxsize. If connections are discarded frequently, |
|
then maxsize should be increased. |
|
|
|
If the pool is closed, then the connection will be closed and discarded. |
|
""" |
|
if self.pool is not None: |
|
try: |
|
self.pool.put(conn, block=False) |
|
return |
|
except AttributeError: |
|
|
|
pass |
|
except queue.Full: |
|
|
|
if conn: |
|
conn.close() |
|
|
|
if self.block: |
|
|
|
raise FullPoolError( |
|
self, |
|
"Pool reached maximum size and no more connections are allowed.", |
|
) from None |
|
|
|
log.warning( |
|
"Connection pool is full, discarding connection: %s. Connection pool size: %s", |
|
self.host, |
|
self.pool.qsize(), |
|
) |
|
|
|
|
|
if conn: |
|
conn.close() |
|
|
|
def _validate_conn(self, conn: BaseHTTPConnection) -> None: |
|
""" |
|
Called right before a request is made, after the socket is created. |
|
""" |
|
|
|
def _prepare_proxy(self, conn: BaseHTTPConnection) -> None: |
|
|
|
pass |
|
|
|
def _get_timeout(self, timeout: _TYPE_TIMEOUT) -> Timeout: |
|
"""Helper that always returns a :class:`urllib3.util.Timeout`""" |
|
if timeout is _DEFAULT_TIMEOUT: |
|
return self.timeout.clone() |
|
|
|
if isinstance(timeout, Timeout): |
|
return timeout.clone() |
|
else: |
|
|
|
|
|
return Timeout.from_float(timeout) |
|
|
|
def _raise_timeout( |
|
self, |
|
err: BaseSSLError | OSError | SocketTimeout, |
|
url: str, |
|
timeout_value: _TYPE_TIMEOUT | None, |
|
) -> None: |
|
"""Is the error actually a timeout? Will raise a ReadTimeout or pass""" |
|
|
|
if isinstance(err, SocketTimeout): |
|
raise ReadTimeoutError( |
|
self, url, f"Read timed out. (read timeout={timeout_value})" |
|
) from err |
|
|
|
|
|
if hasattr(err, "errno") and err.errno in _blocking_errnos: |
|
raise ReadTimeoutError( |
|
self, url, f"Read timed out. (read timeout={timeout_value})" |
|
) from err |
|
|
|
def _make_request( |
|
self, |
|
conn: BaseHTTPConnection, |
|
method: str, |
|
url: str, |
|
body: _TYPE_BODY | None = None, |
|
headers: typing.Mapping[str, str] | None = None, |
|
retries: Retry | None = None, |
|
timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, |
|
chunked: bool = False, |
|
response_conn: BaseHTTPConnection | None = None, |
|
preload_content: bool = True, |
|
decode_content: bool = True, |
|
enforce_content_length: bool = True, |
|
) -> BaseHTTPResponse: |
|
""" |
|
Perform a request on a given urllib connection object taken from our |
|
pool. |
|
|
|
:param conn: |
|
a connection from one of our connection pools |
|
|
|
:param method: |
|
HTTP request method (such as GET, POST, PUT, etc.) |
|
|
|
:param url: |
|
The URL to perform the request on. |
|
|
|
:param body: |
|
Data to send in the request body, either :class:`str`, :class:`bytes`, |
|
an iterable of :class:`str`/:class:`bytes`, or a file-like object. |
|
|
|
:param headers: |
|
Dictionary of custom headers to send, such as User-Agent, |
|
If-None-Match, etc. If None, pool headers are used. If provided, |
|
these headers completely replace any pool-specific headers. |
|
|
|
:param retries: |
|
Configure the number of retries to allow before raising a |
|
:class:`~urllib3.exceptions.MaxRetryError` exception. |
|
|
|
Pass ``None`` to retry until you receive a response. Pass a |
|
:class:`~urllib3.util.retry.Retry` object for fine-grained control |
|
over different types of retries. |
|
Pass an integer number to retry connection errors that many times, |
|
but no other types of errors. Pass zero to never retry. |
|
|
|
If ``False``, then retries are disabled and any exception is raised |
|
immediately. Also, instead of raising a MaxRetryError on redirects, |
|
the redirect response will be returned. |
|
|
|
:type retries: :class:`~urllib3.util.retry.Retry`, False, or an int. |
|
|
|
:param timeout: |
|
If specified, overrides the default timeout for this one |
|
request. It may be a float (in seconds) or an instance of |
|
:class:`urllib3.util.Timeout`. |
|
|
|
:param chunked: |
|
If True, urllib3 will send the body using chunked transfer |
|
encoding. Otherwise, urllib3 will send the body using the standard |
|
content-length form. Defaults to False. |
|
|
|
:param response_conn: |
|
Set this to ``None`` if you will handle releasing the connection or |
|
set the connection to have the response release it. |
|
|
|
:param preload_content: |
|
If True, the response's body will be preloaded during construction. |
|
|
|
:param decode_content: |
|
If True, will attempt to decode the body based on the |
|
'content-encoding' header. |
|
|
|
:param enforce_content_length: |
|
Enforce content length checking. Body returned by server must match |
|
value of Content-Length header, if present. Otherwise, raise error. |
|
""" |
|
self.num_requests += 1 |
|
|
|
timeout_obj = self._get_timeout(timeout) |
|
timeout_obj.start_connect() |
|
conn.timeout = Timeout.resolve_default_timeout(timeout_obj.connect_timeout) |
|
|
|
try: |
|
|
|
try: |
|
self._validate_conn(conn) |
|
except (SocketTimeout, BaseSSLError) as e: |
|
self._raise_timeout(err=e, url=url, timeout_value=conn.timeout) |
|
raise |
|
|
|
|
|
|
|
except ( |
|
OSError, |
|
NewConnectionError, |
|
TimeoutError, |
|
BaseSSLError, |
|
CertificateError, |
|
SSLError, |
|
) as e: |
|
new_e: Exception = e |
|
if isinstance(e, (BaseSSLError, CertificateError)): |
|
new_e = SSLError(e) |
|
|
|
|
|
if isinstance( |
|
new_e, (OSError, NewConnectionError, TimeoutError, SSLError) |
|
) and (conn and conn.proxy and not conn.has_connected_to_proxy): |
|
new_e = _wrap_proxy_error(new_e, conn.proxy.scheme) |
|
raise new_e |
|
|
|
|
|
|
|
try: |
|
conn.request( |
|
method, |
|
url, |
|
body=body, |
|
headers=headers, |
|
chunked=chunked, |
|
preload_content=preload_content, |
|
decode_content=decode_content, |
|
enforce_content_length=enforce_content_length, |
|
) |
|
|
|
|
|
|
|
|
|
except BrokenPipeError: |
|
pass |
|
except OSError as e: |
|
|
|
|
|
|
|
|
|
if e.errno != errno.EPROTOTYPE and e.errno != errno.ECONNRESET: |
|
raise |
|
|
|
|
|
read_timeout = timeout_obj.read_timeout |
|
|
|
if not conn.is_closed: |
|
|
|
|
|
|
|
|
|
|
|
if read_timeout == 0: |
|
raise ReadTimeoutError( |
|
self, url, f"Read timed out. (read timeout={read_timeout})" |
|
) |
|
conn.timeout = read_timeout |
|
|
|
|
|
try: |
|
response = conn.getresponse() |
|
except (BaseSSLError, OSError) as e: |
|
self._raise_timeout(err=e, url=url, timeout_value=read_timeout) |
|
raise |
|
|
|
|
|
response.retries = retries |
|
response._connection = response_conn |
|
response._pool = self |
|
|
|
|
|
http_version = getattr(conn, "_http_vsn_str", "HTTP/?") |
|
log.debug( |
|
'%s://%s:%s "%s %s %s" %s %s', |
|
self.scheme, |
|
self.host, |
|
self.port, |
|
method, |
|
url, |
|
|
|
http_version, |
|
response.status, |
|
response.length_remaining, |
|
) |
|
|
|
return response |
|
|
|
def close(self) -> None: |
|
""" |
|
Close all pooled connections and disable the pool. |
|
""" |
|
if self.pool is None: |
|
return |
|
|
|
old_pool, self.pool = self.pool, None |
|
|
|
|
|
_close_pool_connections(old_pool) |
|
|
|
def is_same_host(self, url: str) -> bool: |
|
""" |
|
Check if the given ``url`` is a member of the same host as this |
|
connection pool. |
|
""" |
|
if url.startswith("/"): |
|
return True |
|
|
|
|
|
scheme, _, host, port, *_ = parse_url(url) |
|
scheme = scheme or "http" |
|
if host is not None: |
|
host = _normalize_host(host, scheme=scheme) |
|
|
|
|
|
if self.port and not port: |
|
port = port_by_scheme.get(scheme) |
|
elif not self.port and port == port_by_scheme.get(scheme): |
|
port = None |
|
|
|
return (scheme, host, port) == (self.scheme, self.host, self.port) |
|
|
|
def urlopen( |
|
self, |
|
method: str, |
|
url: str, |
|
body: _TYPE_BODY | None = None, |
|
headers: typing.Mapping[str, str] | None = None, |
|
retries: Retry | bool | int | None = None, |
|
redirect: bool = True, |
|
assert_same_host: bool = True, |
|
timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, |
|
pool_timeout: int | None = None, |
|
release_conn: bool | None = None, |
|
chunked: bool = False, |
|
body_pos: _TYPE_BODY_POSITION | None = None, |
|
preload_content: bool = True, |
|
decode_content: bool = True, |
|
**response_kw: typing.Any, |
|
) -> BaseHTTPResponse: |
|
""" |
|
Get a connection from the pool and perform an HTTP request. This is the |
|
lowest level call for making a request, so you'll need to specify all |
|
the raw details. |
|
|
|
.. note:: |
|
|
|
More commonly, it's appropriate to use a convenience method |
|
such as :meth:`request`. |
|
|
|
.. note:: |
|
|
|
`release_conn` will only behave as expected if |
|
`preload_content=False` because we want to make |
|
`preload_content=False` the default behaviour someday soon without |
|
breaking backwards compatibility. |
|
|
|
:param method: |
|
HTTP request method (such as GET, POST, PUT, etc.) |
|
|
|
:param url: |
|
The URL to perform the request on. |
|
|
|
:param body: |
|
Data to send in the request body, either :class:`str`, :class:`bytes`, |
|
an iterable of :class:`str`/:class:`bytes`, or a file-like object. |
|
|
|
:param headers: |
|
Dictionary of custom headers to send, such as User-Agent, |
|
If-None-Match, etc. If None, pool headers are used. If provided, |
|
these headers completely replace any pool-specific headers. |
|
|
|
:param retries: |
|
Configure the number of retries to allow before raising a |
|
:class:`~urllib3.exceptions.MaxRetryError` exception. |
|
|
|
If ``None`` (default) will retry 3 times, see ``Retry.DEFAULT``. Pass a |
|
:class:`~urllib3.util.retry.Retry` object for fine-grained control |
|
over different types of retries. |
|
Pass an integer number to retry connection errors that many times, |
|
but no other types of errors. Pass zero to never retry. |
|
|
|
If ``False``, then retries are disabled and any exception is raised |
|
immediately. Also, instead of raising a MaxRetryError on redirects, |
|
the redirect response will be returned. |
|
|
|
:type retries: :class:`~urllib3.util.retry.Retry`, False, or an int. |
|
|
|
:param redirect: |
|
If True, automatically handle redirects (status codes 301, 302, |
|
303, 307, 308). Each redirect counts as a retry. Disabling retries |
|
will disable redirect, too. |
|
|
|
:param assert_same_host: |
|
If ``True``, will make sure that the host of the pool requests is |
|
consistent else will raise HostChangedError. When ``False``, you can |
|
use the pool on an HTTP proxy and request foreign hosts. |
|
|
|
:param timeout: |
|
If specified, overrides the default timeout for this one |
|
request. It may be a float (in seconds) or an instance of |
|
:class:`urllib3.util.Timeout`. |
|
|
|
:param pool_timeout: |
|
If set and the pool is set to block=True, then this method will |
|
block for ``pool_timeout`` seconds and raise EmptyPoolError if no |
|
connection is available within the time period. |
|
|
|
:param bool preload_content: |
|
If True, the response's body will be preloaded into memory. |
|
|
|
:param bool decode_content: |
|
If True, will attempt to decode the body based on the |
|
'content-encoding' header. |
|
|
|
:param release_conn: |
|
If False, then the urlopen call will not release the connection |
|
back into the pool once a response is received (but will release if |
|
you read the entire contents of the response such as when |
|
`preload_content=True`). This is useful if you're not preloading |
|
the response's content immediately. You will need to call |
|
``r.release_conn()`` on the response ``r`` to return the connection |
|
back into the pool. If None, it takes the value of ``preload_content`` |
|
which defaults to ``True``. |
|
|
|
:param bool chunked: |
|
If True, urllib3 will send the body using chunked transfer |
|
encoding. Otherwise, urllib3 will send the body using the standard |
|
content-length form. Defaults to False. |
|
|
|
:param int body_pos: |
|
Position to seek to in file-like body in the event of a retry or |
|
redirect. Typically this won't need to be set because urllib3 will |
|
auto-populate the value when needed. |
|
""" |
|
parsed_url = parse_url(url) |
|
destination_scheme = parsed_url.scheme |
|
|
|
if headers is None: |
|
headers = self.headers |
|
|
|
if not isinstance(retries, Retry): |
|
retries = Retry.from_int(retries, redirect=redirect, default=self.retries) |
|
|
|
if release_conn is None: |
|
release_conn = preload_content |
|
|
|
|
|
if assert_same_host and not self.is_same_host(url): |
|
raise HostChangedError(self, url, retries) |
|
|
|
|
|
if url.startswith("/"): |
|
url = to_str(_encode_target(url)) |
|
else: |
|
url = to_str(parsed_url.url) |
|
|
|
conn = None |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
release_this_conn = release_conn |
|
|
|
http_tunnel_required = connection_requires_http_tunnel( |
|
self.proxy, self.proxy_config, destination_scheme |
|
) |
|
|
|
|
|
|
|
|
|
if not http_tunnel_required: |
|
headers = headers.copy() |
|
headers.update(self.proxy_headers) |
|
|
|
|
|
|
|
err = None |
|
|
|
|
|
|
|
clean_exit = False |
|
|
|
|
|
|
|
body_pos = set_file_position(body, body_pos) |
|
|
|
try: |
|
|
|
timeout_obj = self._get_timeout(timeout) |
|
conn = self._get_conn(timeout=pool_timeout) |
|
|
|
conn.timeout = timeout_obj.connect_timeout |
|
|
|
|
|
if self.proxy is not None and http_tunnel_required and conn.is_closed: |
|
try: |
|
self._prepare_proxy(conn) |
|
except (BaseSSLError, OSError, SocketTimeout) as e: |
|
self._raise_timeout( |
|
err=e, url=self.proxy.url, timeout_value=conn.timeout |
|
) |
|
raise |
|
|
|
|
|
|
|
|
|
|
|
response_conn = conn if not release_conn else None |
|
|
|
|
|
response = self._make_request( |
|
conn, |
|
method, |
|
url, |
|
timeout=timeout_obj, |
|
body=body, |
|
headers=headers, |
|
chunked=chunked, |
|
retries=retries, |
|
response_conn=response_conn, |
|
preload_content=preload_content, |
|
decode_content=decode_content, |
|
**response_kw, |
|
) |
|
|
|
|
|
clean_exit = True |
|
|
|
except EmptyPoolError: |
|
|
|
clean_exit = True |
|
release_this_conn = False |
|
raise |
|
|
|
except ( |
|
TimeoutError, |
|
HTTPException, |
|
OSError, |
|
ProtocolError, |
|
BaseSSLError, |
|
SSLError, |
|
CertificateError, |
|
ProxyError, |
|
) as e: |
|
|
|
|
|
clean_exit = False |
|
new_e: Exception = e |
|
if isinstance(e, (BaseSSLError, CertificateError)): |
|
new_e = SSLError(e) |
|
if isinstance( |
|
new_e, |
|
( |
|
OSError, |
|
NewConnectionError, |
|
TimeoutError, |
|
SSLError, |
|
HTTPException, |
|
), |
|
) and (conn and conn.proxy and not conn.has_connected_to_proxy): |
|
new_e = _wrap_proxy_error(new_e, conn.proxy.scheme) |
|
elif isinstance(new_e, (OSError, HTTPException)): |
|
new_e = ProtocolError("Connection aborted.", new_e) |
|
|
|
retries = retries.increment( |
|
method, url, error=new_e, _pool=self, _stacktrace=sys.exc_info()[2] |
|
) |
|
retries.sleep() |
|
|
|
|
|
err = e |
|
|
|
finally: |
|
if not clean_exit: |
|
|
|
|
|
|
|
|
|
if conn: |
|
conn.close() |
|
conn = None |
|
release_this_conn = True |
|
|
|
if release_this_conn: |
|
|
|
|
|
|
|
self._put_conn(conn) |
|
|
|
if not conn: |
|
|
|
log.warning( |
|
"Retrying (%r) after connection broken by '%r': %s", retries, err, url |
|
) |
|
return self.urlopen( |
|
method, |
|
url, |
|
body, |
|
headers, |
|
retries, |
|
redirect, |
|
assert_same_host, |
|
timeout=timeout, |
|
pool_timeout=pool_timeout, |
|
release_conn=release_conn, |
|
chunked=chunked, |
|
body_pos=body_pos, |
|
preload_content=preload_content, |
|
decode_content=decode_content, |
|
**response_kw, |
|
) |
|
|
|
|
|
redirect_location = redirect and response.get_redirect_location() |
|
if redirect_location: |
|
if response.status == 303: |
|
|
|
method = "GET" |
|
|
|
body = None |
|
headers = HTTPHeaderDict(headers)._prepare_for_method_change() |
|
|
|
try: |
|
retries = retries.increment(method, url, response=response, _pool=self) |
|
except MaxRetryError: |
|
if retries.raise_on_redirect: |
|
response.drain_conn() |
|
raise |
|
return response |
|
|
|
response.drain_conn() |
|
retries.sleep_for_retry(response) |
|
log.debug("Redirecting %s -> %s", url, redirect_location) |
|
return self.urlopen( |
|
method, |
|
redirect_location, |
|
body, |
|
headers, |
|
retries=retries, |
|
redirect=redirect, |
|
assert_same_host=assert_same_host, |
|
timeout=timeout, |
|
pool_timeout=pool_timeout, |
|
release_conn=release_conn, |
|
chunked=chunked, |
|
body_pos=body_pos, |
|
preload_content=preload_content, |
|
decode_content=decode_content, |
|
**response_kw, |
|
) |
|
|
|
|
|
has_retry_after = bool(response.headers.get("Retry-After")) |
|
if retries.is_retry(method, response.status, has_retry_after): |
|
try: |
|
retries = retries.increment(method, url, response=response, _pool=self) |
|
except MaxRetryError: |
|
if retries.raise_on_status: |
|
response.drain_conn() |
|
raise |
|
return response |
|
|
|
response.drain_conn() |
|
retries.sleep(response) |
|
log.debug("Retry: %s", url) |
|
return self.urlopen( |
|
method, |
|
url, |
|
body, |
|
headers, |
|
retries=retries, |
|
redirect=redirect, |
|
assert_same_host=assert_same_host, |
|
timeout=timeout, |
|
pool_timeout=pool_timeout, |
|
release_conn=release_conn, |
|
chunked=chunked, |
|
body_pos=body_pos, |
|
preload_content=preload_content, |
|
decode_content=decode_content, |
|
**response_kw, |
|
) |
|
|
|
return response |
|
|
|
|
|
class HTTPSConnectionPool(HTTPConnectionPool): |
|
""" |
|
Same as :class:`.HTTPConnectionPool`, but HTTPS. |
|
|
|
:class:`.HTTPSConnection` uses one of ``assert_fingerprint``, |
|
``assert_hostname`` and ``host`` in this order to verify connections. |
|
If ``assert_hostname`` is False, no verification is done. |
|
|
|
The ``key_file``, ``cert_file``, ``cert_reqs``, ``ca_certs``, |
|
``ca_cert_dir``, ``ssl_version``, ``key_password`` are only used if :mod:`ssl` |
|
is available and are fed into :meth:`urllib3.util.ssl_wrap_socket` to upgrade |
|
the connection socket into an SSL socket. |
|
""" |
|
|
|
scheme = "https" |
|
ConnectionCls: type[BaseHTTPSConnection] = HTTPSConnection |
|
|
|
def __init__( |
|
self, |
|
host: str, |
|
port: int | None = None, |
|
timeout: _TYPE_TIMEOUT | None = _DEFAULT_TIMEOUT, |
|
maxsize: int = 1, |
|
block: bool = False, |
|
headers: typing.Mapping[str, str] | None = None, |
|
retries: Retry | bool | int | None = None, |
|
_proxy: Url | None = None, |
|
_proxy_headers: typing.Mapping[str, str] | None = None, |
|
key_file: str | None = None, |
|
cert_file: str | None = None, |
|
cert_reqs: int | str | None = None, |
|
key_password: str | None = None, |
|
ca_certs: str | None = None, |
|
ssl_version: int | str | None = None, |
|
ssl_minimum_version: ssl.TLSVersion | None = None, |
|
ssl_maximum_version: ssl.TLSVersion | None = None, |
|
assert_hostname: str | Literal[False] | None = None, |
|
assert_fingerprint: str | None = None, |
|
ca_cert_dir: str | None = None, |
|
**conn_kw: typing.Any, |
|
) -> None: |
|
super().__init__( |
|
host, |
|
port, |
|
timeout, |
|
maxsize, |
|
block, |
|
headers, |
|
retries, |
|
_proxy, |
|
_proxy_headers, |
|
**conn_kw, |
|
) |
|
|
|
self.key_file = key_file |
|
self.cert_file = cert_file |
|
self.cert_reqs = cert_reqs |
|
self.key_password = key_password |
|
self.ca_certs = ca_certs |
|
self.ca_cert_dir = ca_cert_dir |
|
self.ssl_version = ssl_version |
|
self.ssl_minimum_version = ssl_minimum_version |
|
self.ssl_maximum_version = ssl_maximum_version |
|
self.assert_hostname = assert_hostname |
|
self.assert_fingerprint = assert_fingerprint |
|
|
|
def _prepare_proxy(self, conn: HTTPSConnection) -> None: |
|
"""Establishes a tunnel connection through HTTP CONNECT.""" |
|
if self.proxy and self.proxy.scheme == "https": |
|
tunnel_scheme = "https" |
|
else: |
|
tunnel_scheme = "http" |
|
|
|
conn.set_tunnel( |
|
scheme=tunnel_scheme, |
|
host=self._tunnel_host, |
|
port=self.port, |
|
headers=self.proxy_headers, |
|
) |
|
conn.connect() |
|
|
|
def _new_conn(self) -> BaseHTTPSConnection: |
|
""" |
|
Return a fresh :class:`urllib3.connection.HTTPConnection`. |
|
""" |
|
self.num_connections += 1 |
|
log.debug( |
|
"Starting new HTTPS connection (%d): %s:%s", |
|
self.num_connections, |
|
self.host, |
|
self.port or "443", |
|
) |
|
|
|
if not self.ConnectionCls or self.ConnectionCls is DummyConnection: |
|
raise ImportError( |
|
"Can't connect to HTTPS URL because the SSL module is not available." |
|
) |
|
|
|
actual_host: str = self.host |
|
actual_port = self.port |
|
if self.proxy is not None and self.proxy.host is not None: |
|
actual_host = self.proxy.host |
|
actual_port = self.proxy.port |
|
|
|
return self.ConnectionCls( |
|
host=actual_host, |
|
port=actual_port, |
|
timeout=self.timeout.connect_timeout, |
|
cert_file=self.cert_file, |
|
key_file=self.key_file, |
|
key_password=self.key_password, |
|
cert_reqs=self.cert_reqs, |
|
ca_certs=self.ca_certs, |
|
ca_cert_dir=self.ca_cert_dir, |
|
assert_hostname=self.assert_hostname, |
|
assert_fingerprint=self.assert_fingerprint, |
|
ssl_version=self.ssl_version, |
|
ssl_minimum_version=self.ssl_minimum_version, |
|
ssl_maximum_version=self.ssl_maximum_version, |
|
**self.conn_kw, |
|
) |
|
|
|
def _validate_conn(self, conn: BaseHTTPConnection) -> None: |
|
""" |
|
Called right before a request is made, after the socket is created. |
|
""" |
|
super()._validate_conn(conn) |
|
|
|
|
|
if conn.is_closed: |
|
conn.connect() |
|
|
|
|
|
if not conn.is_verified and not conn.proxy_is_verified: |
|
warnings.warn( |
|
( |
|
f"Unverified HTTPS request is being made to host '{conn.host}'. " |
|
"Adding certificate verification is strongly advised. See: " |
|
"https://urllib3.readthedocs.io/en/latest/advanced-usage.html" |
|
"#tls-warnings" |
|
), |
|
InsecureRequestWarning, |
|
) |
|
|
|
|
|
def connection_from_url(url: str, **kw: typing.Any) -> HTTPConnectionPool: |
|
""" |
|
Given a url, return an :class:`.ConnectionPool` instance of its host. |
|
|
|
This is a shortcut for not having to parse out the scheme, host, and port |
|
of the url before creating an :class:`.ConnectionPool` instance. |
|
|
|
:param url: |
|
Absolute URL string that must include the scheme. Port is optional. |
|
|
|
:param \\**kw: |
|
Passes additional parameters to the constructor of the appropriate |
|
:class:`.ConnectionPool`. Useful for specifying things like |
|
timeout, maxsize, headers, etc. |
|
|
|
Example:: |
|
|
|
>>> conn = connection_from_url('http://google.com/') |
|
>>> r = conn.request('GET', '/') |
|
""" |
|
scheme, _, host, port, *_ = parse_url(url) |
|
scheme = scheme or "http" |
|
port = port or port_by_scheme.get(scheme, 80) |
|
if scheme == "https": |
|
return HTTPSConnectionPool(host, port=port, **kw) |
|
else: |
|
return HTTPConnectionPool(host, port=port, **kw) |
|
|
|
|
|
@typing.overload |
|
def _normalize_host(host: None, scheme: str | None) -> None: |
|
... |
|
|
|
|
|
@typing.overload |
|
def _normalize_host(host: str, scheme: str | None) -> str: |
|
... |
|
|
|
|
|
def _normalize_host(host: str | None, scheme: str | None) -> str | None: |
|
""" |
|
Normalize hosts for comparisons and use with sockets. |
|
""" |
|
|
|
host = normalize_host(host, scheme) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if host and host.startswith("[") and host.endswith("]"): |
|
host = host[1:-1] |
|
return host |
|
|
|
|
|
def _url_from_pool( |
|
pool: HTTPConnectionPool | HTTPSConnectionPool, path: str | None = None |
|
) -> str: |
|
"""Returns the URL from a given connection pool. This is mainly used for testing and logging.""" |
|
return Url(scheme=pool.scheme, host=pool.host, port=pool.port, path=path).url |
|
|
|
|
|
def _close_pool_connections(pool: queue.LifoQueue[typing.Any]) -> None: |
|
"""Drains a queue of connections and closes each one.""" |
|
try: |
|
while True: |
|
conn = pool.get(block=False) |
|
if conn: |
|
conn.close() |
|
except queue.Empty: |
|
pass |
|
|