-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add internal support for file-like object responses to improve adapte…
…r plugin support (#1094) * Support `requests.response.raw` being a file-like object Previously HTTPie relied on `requests.models.Response.raw` being `urllib3.HTTPResponse`. The `requests` documentation specifies that (requests.models.Response.raw)[https://docs.python-requests.org/en/master/api/#requests.Response.raw] is a file-like object but allows for other types for internal use. This change introduces graceful handling for scenarios when `requests.models.Response.raw` is not `urllib3.HTTPResponse`. In such a scenario HTTPie now falls back to extracting metadata from `requests.models.Response` directly instead of direct access from protected protected members such as `response.raw._original_response`. A side effect in this fallback procedure is that we can no longer determine HTTP protocol version and report it as `1.1`. This change is necessary to make it possible to implement `TransportPlugins` without having to also needing to emulate internal behavior of `urlib3` and `http.client`. * Load cookies from `response.headers` instead of `response.raw._original_response.msg._headers` `response.cookies` was not utilized as it not possible to construct original payload from `http.cookiejar.Cookie`. Data is stored in lossy format. For example `Cookie.secure` defaults to `False` so we cannot distinguish if `Cookie.secure` was set to `False` or was not set at all. Same problem applies to other fields also. * Simpler HTTP envelope data extraction * Test cookie extraction and make cookie presentment backwards compatible Co-authored-by: Mickaël Schoentgen <contact@tiger-222.fr> Co-authored-by: Jakub Roztocil <jakub@roztocil.co>
- Loading branch information
1 parent
b7300c1
commit 147a066
Showing
7 changed files
with
143 additions
and
35 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
from http.cookies import SimpleCookie | ||
from http.server import BaseHTTPRequestHandler, HTTPServer | ||
from threading import Thread | ||
|
||
from .utils import http | ||
|
||
|
||
class TestIntegration: | ||
|
||
def setup_mock_server(self, handler): | ||
"""Configure mock server.""" | ||
# Passing 0 as the port will cause a random free port to be chosen. | ||
self.mock_server = HTTPServer(('localhost', 0), handler) | ||
_, self.mock_server_port = self.mock_server.server_address | ||
|
||
# Start running mock server in a separate thread. | ||
# Daemon threads automatically shut down when the main process exits. | ||
self.mock_server_thread = Thread(target=self.mock_server.serve_forever) | ||
self.mock_server_thread.setDaemon(True) | ||
self.mock_server_thread.start() | ||
|
||
def test_cookie_parser(self): | ||
"""Not directly testing HTTPie but `requests` to ensure their cookies handling | ||
is still as expected by `get_expired_cookies()`. | ||
""" | ||
|
||
class MockServerRequestHandler(BaseHTTPRequestHandler): | ||
""""HTTP request handler.""" | ||
|
||
def do_GET(self): | ||
"""Handle GET requests.""" | ||
# Craft multiple cookies | ||
cookie = SimpleCookie() | ||
cookie['hello'] = 'world' | ||
cookie['hello']['path'] = self.path | ||
cookie['oatmeal_raisin'] = 'is the best' | ||
cookie['oatmeal_raisin']['path'] = self.path | ||
|
||
# Send HTTP headers | ||
self.send_response(200) | ||
self.send_header('Set-Cookie', cookie.output()) | ||
self.end_headers() | ||
|
||
self.setup_mock_server(MockServerRequestHandler) | ||
response = http(f'http://localhost:{self.mock_server_port}/') | ||
assert 'Set-Cookie: hello=world; Path=/' in response | ||
assert 'Set-Cookie: oatmeal_raisin="is the best"; Path=/' in response |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
from io import BytesIO | ||
|
||
from requests.adapters import BaseAdapter | ||
from requests.models import Response | ||
from requests.utils import get_encoding_from_headers | ||
|
||
from httpie.plugins import TransportPlugin | ||
from httpie.plugins.registry import plugin_manager | ||
|
||
from .utils import HTTP_OK, http | ||
|
||
SCHEME = 'http+fake' | ||
|
||
|
||
class FakeAdapter(BaseAdapter): | ||
def send(self, request, **kwargs): | ||
response = Response() | ||
response.status_code = 200 | ||
response.reason = 'OK' | ||
response.headers = { | ||
'Content-Type': 'text/html; charset=UTF-8', | ||
} | ||
response.encoding = get_encoding_from_headers(response.headers) | ||
response.raw = BytesIO(b'<!doctype html><html>Hello</html>') | ||
return response | ||
|
||
|
||
class FakeTransportPlugin(TransportPlugin): | ||
name = 'Fake Transport' | ||
|
||
prefix = SCHEME | ||
|
||
def get_adapter(self): | ||
return FakeAdapter() | ||
|
||
|
||
def test_transport_from_requests_response(httpbin): | ||
plugin_manager.register(FakeTransportPlugin) | ||
try: | ||
r = http(f'{SCHEME}://example.com') | ||
assert HTTP_OK in r | ||
assert 'Hello' in r | ||
assert 'Content-Type: text/html; charset=UTF-8' in r | ||
finally: | ||
plugin_manager.unregister(FakeTransportPlugin) |