mirror of
https://github.com/httpie/cli.git
synced 2024-11-28 08:38:44 +02:00
147a066dbe
* 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>
137 lines
3.8 KiB
Python
137 lines
3.8 KiB
Python
from typing import Iterable, Optional
|
|
from urllib.parse import urlsplit
|
|
|
|
from .utils import split_cookies
|
|
|
|
|
|
class HTTPMessage:
|
|
"""Abstract class for HTTP messages."""
|
|
|
|
def __init__(self, orig):
|
|
self._orig = orig
|
|
|
|
def iter_body(self, chunk_size: int) -> Iterable[bytes]:
|
|
"""Return an iterator over the body."""
|
|
raise NotImplementedError()
|
|
|
|
def iter_lines(self, chunk_size: int) -> Iterable[bytes]:
|
|
"""Return an iterator over the body yielding (`line`, `line_feed`)."""
|
|
raise NotImplementedError()
|
|
|
|
@property
|
|
def headers(self) -> str:
|
|
"""Return a `str` with the message's headers."""
|
|
raise NotImplementedError()
|
|
|
|
@property
|
|
def encoding(self) -> Optional[str]:
|
|
"""Return a `str` with the message's encoding, if known."""
|
|
raise NotImplementedError()
|
|
|
|
@property
|
|
def body(self) -> bytes:
|
|
"""Return a `bytes` with the message's body."""
|
|
raise NotImplementedError()
|
|
|
|
@property
|
|
def content_type(self) -> str:
|
|
"""Return the message content type."""
|
|
ct = self._orig.headers.get('Content-Type', '')
|
|
if not isinstance(ct, str):
|
|
ct = ct.decode('utf8')
|
|
return ct
|
|
|
|
|
|
class HTTPResponse(HTTPMessage):
|
|
"""A :class:`requests.models.Response` wrapper."""
|
|
|
|
def iter_body(self, chunk_size=1):
|
|
return self._orig.iter_content(chunk_size=chunk_size)
|
|
|
|
def iter_lines(self, chunk_size):
|
|
return ((line, b'\n') for line in self._orig.iter_lines(chunk_size))
|
|
|
|
# noinspection PyProtectedMember
|
|
@property
|
|
def headers(self):
|
|
try:
|
|
raw_version = self._orig.raw._original_response.version
|
|
except AttributeError:
|
|
# Assume HTTP/1.1
|
|
raw_version = 11
|
|
version = {
|
|
9: '0.9',
|
|
10: '1.0',
|
|
11: '1.1',
|
|
20: '2',
|
|
}[raw_version]
|
|
|
|
original = self._orig
|
|
status_line = f'HTTP/{version} {original.status_code} {original.reason}'
|
|
headers = [status_line]
|
|
headers.extend(
|
|
': '.join(header)
|
|
for header in original.headers.items()
|
|
if header[0] != 'Set-Cookie'
|
|
)
|
|
headers.extend(
|
|
f'Set-Cookie: {cookie}'
|
|
for cookie in split_cookies(original.headers.get('Set-Cookie'))
|
|
)
|
|
return '\r\n'.join(headers)
|
|
|
|
@property
|
|
def encoding(self):
|
|
return self._orig.encoding or 'utf8'
|
|
|
|
@property
|
|
def body(self):
|
|
# Only now the response body is fetched.
|
|
# Shouldn't be touched unless the body is actually needed.
|
|
return self._orig.content
|
|
|
|
|
|
class HTTPRequest(HTTPMessage):
|
|
"""A :class:`requests.models.Request` wrapper."""
|
|
|
|
def iter_body(self, chunk_size):
|
|
yield self.body
|
|
|
|
def iter_lines(self, chunk_size):
|
|
yield self.body, b''
|
|
|
|
@property
|
|
def headers(self):
|
|
url = urlsplit(self._orig.url)
|
|
|
|
request_line = '{method} {path}{query} HTTP/1.1'.format(
|
|
method=self._orig.method,
|
|
path=url.path or '/',
|
|
query=f'?{url.query}' if url.query else ''
|
|
)
|
|
|
|
headers = dict(self._orig.headers)
|
|
if 'Host' not in self._orig.headers:
|
|
headers['Host'] = url.netloc.split('@')[-1]
|
|
|
|
headers = [
|
|
f'{name}: {value if isinstance(value, str) else value.decode("utf-8")}'
|
|
for name, value in headers.items()
|
|
]
|
|
|
|
headers.insert(0, request_line)
|
|
headers = '\r\n'.join(headers).strip()
|
|
return headers
|
|
|
|
@property
|
|
def encoding(self):
|
|
return 'utf8'
|
|
|
|
@property
|
|
def body(self):
|
|
body = self._orig.body
|
|
if isinstance(body, str):
|
|
# Happens with JSON/form request data parsed from the command line.
|
|
body = body.encode('utf8')
|
|
return body or b''
|