Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor internal APIs to use our own HTTPResponse #2649

Merged
merged 42 commits into from
Nov 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
c5ed237
init commit of changes to be made for disscusion
shadycuz Jun 21, 2022
25a8474
refactor common types into its own util class
shadycuz Jun 21, 2022
44259be
return HTTPResponse object from urlopen
shadycuz Jun 21, 2022
2964e87
add length to HTTPResponse
shadycuz Jun 21, 2022
527927e
pass retries to HTTPResponse
shadycuz Jun 21, 2022
824e5d8
linting fixes
shadycuz Jun 22, 2022
ea254b9
fix mypy typing issues
shadycuz Jun 22, 2022
6f67900
attempt to fix test case
shadycuz Jun 22, 2022
1a77d07
add comment on why import is in method
shadycuz Jun 22, 2022
781a629
add back missing type ignore
shadycuz Jun 22, 2022
a2cf139
fix raise_once func by updating behavior
shadycuz Jun 22, 2022
f833362
re-implemt custom reponse class feature
shadycuz Jun 22, 2022
1c33e3f
make header parsing work again
shadycuz Jun 23, 2022
40dc0f3
refactor from_httplib into getresponse
shadycuz Jun 30, 2022
4328582
remove response class override
shadycuz Jun 30, 2022
314aa61
remove duplicate length
shadycuz Jun 30, 2022
3eeb8ee
types and comments cleanup
shadycuz Jun 30, 2022
0613e3d
Revert "refactor common types into its own util class"
shadycuz Jun 30, 2022
f0e8684
unconditionally convert .msg into HTTPHeaderDict
shadycuz Jul 6, 2022
0c1bebd
reorder arguments to match order listed in the constructor
shadycuz Jul 6, 2022
ae81873
refactor _absolute_url into multiple helper functions
shadycuz Jul 6, 2022
55f973e
set connection backed to response_conn
shadycuz Jul 7, 2022
01cc47d
refactor out httplib_request_kw
shadycuz Jul 7, 2022
8c0f880
make mock func behave like the real func
shadycuz Jul 7, 2022
9df9e73
remove request_ prefix to match other func's like urlopen
shadycuz Jul 7, 2022
b8f6bf6
force kwargs for getresponse
shadycuz Jul 7, 2022
ee01d14
use kwargs when passing body and headers
shadycuz Jul 7, 2022
ce50569
make url_from_pool a private api
shadycuz Jul 7, 2022
12ed1d8
make url_from_connection a private API
shadycuz Jul 7, 2022
b3bd9fa
remove comment explaining why import happens further down
shadycuz Jul 7, 2022
bf7eb2e
remove some whitespace
shadycuz Jul 7, 2022
1acbce5
update param order and documentation
shadycuz Jul 7, 2022
32aa265
update docstring and param order for getresponse
shadycuz Jul 8, 2022
c1d9d59
add tests for getresponse returning a urllib3 HTTPResponse
shadycuz Jul 8, 2022
4118fae
add change log entry
shadycuz Jul 8, 2022
8e5bb07
Try to make integration tests pass
sethmlarson Jul 11, 2022
67df385
Merge pull request #1 from sethmlarson/pr-2649
shadycuz Jul 16, 2022
c0ea937
test that getresponse throws ResponseNotReady
shadycuz Jul 16, 2022
4804b83
Merge branch 'main' into refactor_http_response
sethmlarson Aug 31, 2022
1d2321c
Remove _HttplibResponse
sethmlarson Aug 31, 2022
bc48f62
Skip failing psf/requests tests for chunking
sethmlarson Nov 7, 2022
54be61a
Update 2648.feature.rst
sethmlarson Nov 7, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog/2648.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Modified ``urllib3.HTTPConnection.getresponse`` to return an instance of ``urllib3.HTTPResponse`` instead of ``http.client.HTTPResponse``.

Removed ``urllib3.HTTPResponse.from_httplib``.
File renamed without changes.
40 changes: 40 additions & 0 deletions ci/0004-requests-chunked-requests.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
diff --git a/tests/test_lowlevel.py b/tests/test_lowlevel.py
index 859d07e8..91fbce7c 100644
--- a/tests/test_lowlevel.py
+++ b/tests/test_lowlevel.py
@@ -20,7 +20,7 @@ def echo_response_handler(sock):
) % (len(request_content), request_content)
sock.send(text_200)

-
+@pytest.mark.xfail
def test_chunked_upload():
"""can safely send generators"""
close_server = threading.Event()
@@ -35,7 +35,7 @@ def test_chunked_upload():
assert r.status_code == 200
assert r.request.headers["Transfer-Encoding"] == "chunked"

-
+@pytest.mark.xfail
def test_chunked_encoding_error():
"""get a ChunkedEncodingError if the server returns a bad response"""

@@ -59,7 +59,7 @@ def test_chunked_encoding_error():
requests.get(url)
close_server.set() # release server block

-
+@pytest.mark.xfail
def test_chunked_upload_uses_only_specified_host_header():
"""Ensure we use only the specified Host header for chunked requests."""
close_server = threading.Event()
@@ -77,7 +77,7 @@ def test_chunked_upload_uses_only_specified_host_header():
assert expected_header in r.content
assert r.content.count(b"Host: ") == 1

-
+@pytest.mark.xfail
def test_chunked_upload_doesnt_skip_host_header():
"""Ensure we don't omit all Host headers with chunked requests."""
close_server = threading.Event()
7 changes: 6 additions & 1 deletion noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,12 @@ def downstream_requests(session: nox.Session) -> None:
session.cd(tmp_dir)
git_clone(session, "https://github.com/psf/requests")
session.chdir("requests")
session.run("git", "apply", f"{root}/ci/requests.patch", external=True)
session.run(
"git", "apply", f"{root}/ci/0003-requests-removed-warnings.patch", external=True
)
session.run(
"git", "apply", f"{root}/ci/0004-requests-chunked-requests.patch", external=True
)
session.run("git", "rev-parse", "HEAD", external=True)
session.install(".[socks]", silent=False)
session.install("-r", "requirements-dev.txt", silent=False)
Expand Down
2 changes: 0 additions & 2 deletions src/urllib3/_request_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,6 @@ def request(
"""
method = method.upper()

urlopen_kw["request_url"] = url

if json is not None and body is not None:
raise TypeError(
"request got values for both 'body' and 'json' parameters which are mutually exclusive"
Expand Down
98 changes: 97 additions & 1 deletion src/urllib3/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import warnings
from http.client import HTTPConnection as _HTTPConnection
from http.client import HTTPException as HTTPException # noqa: F401
from http.client import ResponseNotReady
from socket import timeout as SocketTimeout
from typing import (
IO,
Expand All @@ -24,9 +25,12 @@
if TYPE_CHECKING:
from typing_extensions import Literal

from .response import HTTPResponse
from .util.ssl_ import _TYPE_PEER_CERT_RET_DICT
from .util.ssltransport import SSLTransport

from ._collections import HTTPHeaderDict
from .util.response import assert_header_parsing
from .util.timeout import _DEFAULT_TIMEOUT, _TYPE_TIMEOUT, Timeout
from .util.util import to_str

Expand All @@ -44,6 +48,7 @@ class BaseSSLError(BaseException): # type: ignore[no-redef]
from ._version import __version__
from .exceptions import (
ConnectTimeoutError,
HeaderParsingError,
NameResolutionError,
NewConnectionError,
ProxyError,
Expand Down Expand Up @@ -77,7 +82,6 @@ class BaseSSLError(BaseException): # type: ignore[no-redef]

_CONTAINS_CONTROL_CHAR_RE = re.compile(r"[^-!#$%&'*+.^_`|~0-9a-zA-Z]")


_TYPE_BODY = Union[bytes, IO[Any], Iterable[bytes], str]


Expand All @@ -88,6 +92,16 @@ class ProxyConfig(NamedTuple):
assert_fingerprint: Optional[str]


class _ResponseOptions(NamedTuple):
# TODO: Remove this in favor of a better
# HTTP request/response lifecycle tracking.
request_method: str
request_url: str
preload_content: bool
decode_content: bool
enforce_content_length: bool


class HTTPConnection(_HTTPConnection):
"""
Based on :class:`http.client.HTTPConnection` but provides an extra constructor
Expand Down Expand Up @@ -134,6 +148,7 @@ class HTTPConnection(_HTTPConnection):
_tunnel_host: Optional[str]
_tunnel: Callable[["HTTPConnection"], None]
_connecting_to_proxy: bool
_response_options: Optional[_ResponseOptions]

def __init__(
self,
Expand Down Expand Up @@ -166,6 +181,7 @@ def __init__(
)

self._connecting_to_proxy = False
self._response_options = None

# https://github.com/python/mypy/issues/4125
# Mypy treats this as LSP violation, which is considered a bug.
Expand Down Expand Up @@ -292,8 +308,27 @@ def request( # type: ignore[override]
body: Optional[_TYPE_BODY] = None,
headers: Optional[Mapping[str, str]] = None,
chunked: bool = False,
preload_content: bool = True,
decode_content: bool = True,
enforce_content_length: bool = True,
) -> None:

# Store these values to be fed into the HTTPResponse
# object later. TODO: Remove this in favor of a real
# HTTP lifecycle mechanism.

# We have to store these before we call .request()
# because sometimes we can still salvage a response
# off the wire even if we aren't able to completely
# send the request body.
self._response_options = _ResponseOptions(
request_method=method,
request_url=url,
preload_content=preload_content,
decode_content=decode_content,
enforce_content_length=enforce_content_length,
)

if headers is None:
headers = {}
header_keys = frozenset(to_str(k.lower()) for k in headers)
Expand Down Expand Up @@ -371,6 +406,57 @@ def request_chunked(
"""
self.request(method, url, body=body, headers=headers, chunked=True)

def getresponse( # type: ignore[override]
self,
) -> "HTTPResponse":
"""
Get the response from the server.

If the HTTPConnection is in the correct state, returns an instance of HTTPResponse or of whatever object is returned by the response_class variable.

If a request has not been sent or if a previous response has not be handled, ResponseNotReady is raised. If the HTTP response indicates that the connection should be closed, then it will be closed before the response is returned. When the connection is closed, the underlying socket is closed.
"""
# Raise the same error as http.client.HTTPConnection
if self._response_options is None:
raise ResponseNotReady()

# Reset this attribute for being used again.
resp_options = self._response_options
self._response_options = None

# This is needed here to avoid circular import errors
from .response import HTTPResponse

# Get the response from http.client.HTTPConnection
httplib_response = super().getresponse()

try:
assert_header_parsing(httplib_response.msg)
except (HeaderParsingError, TypeError) as hpe:
log.warning(
"Failed to parse headers (url=%s): %s",
_url_from_connection(self, resp_options.request_url),
hpe,
exc_info=True,
)

headers = HTTPHeaderDict(httplib_response.msg.items())

response = HTTPResponse(
body=httplib_response,
headers=headers,
status=httplib_response.status,
version=httplib_response.version,
reason=httplib_response.reason,
preload_content=resp_options.preload_content,
decode_content=resp_options.decode_content,
original_response=httplib_response,
enforce_content_length=resp_options.enforce_content_length,
request_method=resp_options.request_method,
request_url=resp_options.request_url,
)
return response


class HTTPSConnection(HTTPConnection):
"""
Expand Down Expand Up @@ -748,3 +834,13 @@ class DummyConnection:


VerifiedHTTPSConnection = HTTPSConnection


def _url_from_connection(
conn: Union[HTTPConnection, HTTPSConnection], path: Optional[str] = None
) -> str:
"""Returns the URL from a given connection. This is mainly used for testing and logging."""

scheme = "https" if isinstance(conn, HTTPSConnection) else "http"

return Url(scheme=scheme, host=conn.host, port=conn.port, path=path).url