Skip to content

Commit

Permalink
⚗️ Try compatible fork Niquests to supercharge HTTPie
Browse files Browse the repository at this point in the history
  • Loading branch information
Ahmed TAHRI committed Nov 16, 2023
1 parent e52a60e commit 7257a74
Show file tree
Hide file tree
Showing 39 changed files with 364 additions and 205 deletions.
7 changes: 1 addition & 6 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: [3.7, 3.8, 3.9, "3.10"]
pyopenssl: [0, 1]
python-version: [3.7, 3.8, 3.9, "3.10", "3.11", "3.12"]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
Expand All @@ -39,12 +38,8 @@ jobs:
python -m pip install --upgrade pip wheel
python -m pip install --upgrade '.[dev]'
python -m pytest --verbose ./httpie ./tests
env:
HTTPIE_TEST_WITH_PYOPENSSL: ${{ matrix.pyopenssl }}
- name: Linux & Mac setup
if: matrix.os != 'windows-latest'
run: |
make install
make test
env:
HTTPIE_TEST_WITH_PYOPENSSL: ${{ matrix.pyopenssl }}
27 changes: 26 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1833,6 +1833,31 @@ $ http --chunked pie.dev/post @files/data.xml
$ cat files/data.xml | http --chunked pie.dev/post
```
## Disable HTTP/2, or HTTP/3
You can at your own discretion toggle on and off HTTP/2, or/and HTTP/3.
```bash
$ https --disable-http2 PUT pie.dev/put hello=world
```
```bash
$ https --disable-http3 PUT pie.dev/put hello=world
```
## Force HTTP/3
By opposition to the previous section, you can force the HTTP/3 negotiation.
```bash
$ https --http3 pie.dev/get
```
By default, HTTPie cannot negotiate HTTP/3 without a first HTTP/1.1, or HTTP/2 successful response.
The remote server yield its support for HTTP/3 in the Alt-Svc header, if present HTTPie will issue
the successive requests via HTTP/3. You may use that argument in case the remote peer does not support
either HTTP/1.1 or HTTP/2.
## Compressed request body
You can use the `--compress, -x` flag to instruct HTTPie to use `Content-Encoding: deflate` and compress the request data:
Expand Down Expand Up @@ -2556,7 +2581,7 @@ HTTPie has the following community channels:
Under the hood, HTTPie uses these two amazing libraries:
- [Requests](https://requests.readthedocs.io/en/latest/) — Python HTTP library for humans
- [Niquests](https://niquests.readthedocs.io/en/latest/) — Python HTTP library for humans
- [Pygments](https://pygments.org/) — Python syntax highlighter
#### HTTPie friends
Expand Down
8 changes: 4 additions & 4 deletions docs/contributors/fetch.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""
Generate the contributors database.
FIXME: replace `requests` calls with the HTTPie API, when available.
FIXME: replace `niquests` calls with the HTTPie API, when available.
"""
import json
import os
Expand All @@ -14,7 +14,7 @@
from time import sleep
from typing import Any, Dict, Optional, Set

import requests
import niquests

FullNames = Set[str]
GitHubLogins = Set[str]
Expand Down Expand Up @@ -197,10 +197,10 @@ def fetch(url: str, params: Optional[Dict[str, str]] = None) -> UserInfo:
}
for retry in range(1, 6):
debug(f'[{retry}/5]', f'{url = }', f'{params = }')
with requests.get(url, params=params, headers=headers) as req:
with niquests.get(url, params=params, headers=headers) as req:
try:
req.raise_for_status()
except requests.exceptions.HTTPError as exc:
except niquests.exceptions.HTTPError as exc:
if exc.response.status_code == 403:
# 403 Client Error: rate limit exceeded for url: ...
now = int(datetime.utcnow().timestamp())
Expand Down
4 changes: 2 additions & 2 deletions httpie/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"""

__version__ = '3.2.2'
__date__ = '2022-05-06'
__version__ = '4.0.0'
__date__ = '2023-10-11'
__author__ = 'Jakub Roztocil'
__licence__ = 'BSD'
2 changes: 1 addition & 1 deletion httpie/adapters.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from httpie.cli.dicts import HTTPHeadersDict
from requests.adapters import HTTPAdapter
from niquests.adapters import HTTPAdapter


class HTTPieHTTPAdapter(HTTPAdapter):
Expand Down
2 changes: 1 addition & 1 deletion httpie/cli/argparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from textwrap import dedent
from urllib.parse import urlsplit

from requests.utils import get_netrc_auth
from niquests.utils import get_netrc_auth

from .argtypes import (
AuthCredentials, SSLCredentials, KeyValueArgType,
Expand Down
26 changes: 26 additions & 0 deletions httpie/cli/definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,32 @@ def format_auth_help(auth_plugins_mapping, *, isolation_mode: bool = False):
'The Transfer-Encoding header is set to chunked.'
)
)
network.add_argument(
"--disable-http2",
default=False,
action="store_true",
short_help="Disable the HTTP/2 protocol."
)
network.add_argument(
"--disable-http3",
default=False,
action="store_true",
short_help="Disable the HTTP/3 over QUIC protocol."
)
network.add_argument(
"--http3",
default=False,
dest="force_http3",
action="store_true",
short_help="Use the HTTP/3 protocol for the request.",
help="""
By default, HTTPie cannot negotiate HTTP/3 without a first HTTP/1.1, or HTTP/2 successful response.
The remote server yield its support for HTTP/3 in the Alt-Svc header, if present HTTPie will issue
the successive requests via HTTP/3. You may use that argument in case the remote peer does not support
either HTTP/1.1 or HTTP/2.
"""
)

#######################################################################
# SSL
Expand Down
80 changes: 40 additions & 40 deletions httpie/client.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import argparse
import http.client
import json
import sys
from contextlib import contextmanager
from time import monotonic
from typing import Any, Dict, Callable, Iterable
from urllib.parse import urlparse, urlunparse

import requests
import niquests
# noinspection PyPackageRequirements
import urllib3
from urllib3.util import SKIP_HEADER, SKIPPABLE_HEADERS
from urllib3.util import SKIP_HEADER, SKIPPABLE_HEADERS, parse_url

from . import __version__
from .adapters import HTTPieHTTPAdapter
Expand Down Expand Up @@ -44,6 +42,7 @@ def collect_messages(
env: Environment,
args: argparse.Namespace,
request_body_read_callback: Callable[[bytes], None] = None,
prepared_request_readiness: Callable[[niquests.PreparedRequest], None] = None,
) -> Iterable[RequestsMessage]:
httpie_session = None
httpie_session_headers = None
Expand All @@ -68,9 +67,18 @@ def collect_messages(
requests_session = build_requests_session(
ssl_version=args.ssl_version,
ciphers=args.ciphers,
verify=bool(send_kwargs_mergeable_from_env['verify'])
verify=bool(send_kwargs_mergeable_from_env['verify']),
disable_http2=args.disable_http2,
disable_http3=args.disable_http3,
)

if args.disable_http3 is False and args.force_http3 is True:
url = parse_url(args.url)
requests_session.quic_cache_layer.add_domain(
url.host,
url.port or 443,
)

if httpie_session:
httpie_session.update_headers(request_kwargs['headers'])
requests_session.cookies = httpie_session.cookies
Expand All @@ -88,7 +96,12 @@ def collect_messages(
# TODO: reflect the split between request and send kwargs.
dump_request(request_kwargs)

request = requests.Request(**request_kwargs)
hooks = None

if prepared_request_readiness:
hooks = {"pre_send": [prepared_request_readiness]}

request = niquests.Request(**request_kwargs, hooks=hooks)
prepared_request = requests_session.prepare_request(request)
transform_headers(request, prepared_request)
if args.path_as_is:
Expand All @@ -110,12 +123,13 @@ def collect_messages(
url=prepared_request.url,
**send_kwargs_mergeable_from_env,
)
with max_headers(args.max_headers):
response = requests_session.send(
request=prepared_request,
**send_kwargs_merged,
**send_kwargs,
)
response = requests_session.send(
request=prepared_request,
**send_kwargs_merged,
**send_kwargs,
)
if args.max_headers and len(response.headers) > args.max_headers:
raise niquests.ConnectionError(f"got more than {args.max_headers} headers")
response._httpie_headers_parsed_at = monotonic()
expired_cookies += get_expired_cookies(
response.headers.get('Set-Cookie', '')
Expand All @@ -124,7 +138,7 @@ def collect_messages(
response_count += 1
if response.next:
if args.max_redirects and response_count == args.max_redirects:
raise requests.TooManyRedirects
raise niquests.TooManyRedirects
if args.follow:
prepared_request = response.next
if args.all:
Expand All @@ -140,25 +154,14 @@ def collect_messages(
httpie_session.save()


# noinspection PyProtectedMember
@contextmanager
def max_headers(limit):
# <https://github.com/httpie/cli/issues/802>
# noinspection PyUnresolvedReferences
orig = http.client._MAXHEADERS
http.client._MAXHEADERS = limit or float('Inf')
try:
yield
finally:
http.client._MAXHEADERS = orig


def build_requests_session(
verify: bool,
ssl_version: str = None,
ciphers: str = None,
) -> requests.Session:
requests_session = requests.Session()
disable_http2: bool = False,
disable_http3: bool = False,
) -> niquests.Session:
requests_session = niquests.Session()

# Install our adapter.
http_adapter = HTTPieHTTPAdapter()
Expand All @@ -169,6 +172,9 @@ def build_requests_session(
AVAILABLE_SSL_VERSION_ARG_MAPPING[ssl_version]
if ssl_version else None
),
disable_http2=disable_http2,
disable_http3=disable_http3,
quic_cache_layer=requests_session.quic_cache_layer,
)
requests_session.mount('http://', http_adapter)
requests_session.mount('https://', https_adapter)
Expand All @@ -186,7 +192,7 @@ def build_requests_session(

def dump_request(kwargs: dict):
sys.stderr.write(
f'\n>>> requests.request(**{repr_dict(kwargs)})\n\n')
f'\n>>> niquests.request(**{repr_dict(kwargs)})\n\n')


def finalize_headers(headers: HTTPHeadersDict) -> HTTPHeadersDict:
Expand All @@ -210,13 +216,13 @@ def finalize_headers(headers: HTTPHeadersDict) -> HTTPHeadersDict:


def transform_headers(
request: requests.Request,
prepared_request: requests.PreparedRequest
request: niquests.Request,
prepared_request: niquests.PreparedRequest
) -> None:
"""Apply various transformations on top of the `prepared_requests`'s
headers to change the request prepreation behavior."""

# Remove 'Content-Length' when it is misplaced by requests.
# Remove 'Content-Length' when it is misplaced by niquests.
if (
prepared_request.method in IGNORE_CONTENT_LENGTH_METHODS
and prepared_request.headers.get('Content-Length') == '0'
Expand All @@ -232,7 +238,7 @@ def transform_headers(

def apply_missing_repeated_headers(
original_headers: HTTPHeadersDict,
prepared_request: requests.PreparedRequest
prepared_request: niquests.PreparedRequest
) -> None:
"""Update the given `prepared_request`'s headers with the original
ones. This allows the requests to be prepared as usual, and then later
Expand Down Expand Up @@ -290,12 +296,6 @@ def make_send_kwargs_mergeable_from_env(args: argparse.Namespace) -> dict:
if args.cert:
cert = args.cert
if args.cert_key:
# Having a client certificate key passphrase is not supported
# by requests. So we are using our own transportation structure
# which is compatible with their format (a tuple of minimum two
# items).
#
# See: https://github.com/psf/requests/issues/2519
cert = HTTPieCertificate(cert, args.cert_key, args.cert_key_pass.value)

return {
Expand Down Expand Up @@ -329,7 +329,7 @@ def make_request_kwargs(
request_body_read_callback=lambda chunk: chunk
) -> dict:
"""
Translate our `args` into `requests.Request` keyword arguments.
Translate our `args` into `niquests.Request` keyword arguments.
"""
files = args.files
Expand Down
Loading

0 comments on commit 7257a74

Please sign in to comment.