Skip to content

Commit

Permalink
improve error messages upon invalid args/values in new flags
Browse files Browse the repository at this point in the history
  • Loading branch information
Ahmed TAHRI committed Oct 22, 2024
1 parent da6cc13 commit bbdb20c
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 2 deletions.
56 changes: 54 additions & 2 deletions httpie/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from time import monotonic
from typing import Any, Dict, Callable, Iterable
from urllib.parse import urlparse, urlunparse
import ipaddress

import niquests

Expand Down Expand Up @@ -71,12 +72,38 @@ def collect_messages(
source_address = None

if args.interface:
# automatically raises ValueError upon invalid IP
ipaddress.ip_address(args.interface)

source_address = (args.interface, 0)
if args.local_port:

if '-' not in args.local_port:
source_address = (args.interface or "0.0.0.0", int(args.local_port))
try:
parsed_port = int(args.local_port)
except ValueError:
raise ValueError(f'"{args.local_port}" is not a valid port number.')

source_address = (args.interface or "0.0.0.0", parsed_port)
else:
min_port, max_port = args.local_port.split('-', 1)
if args.local_port.count('-') != 1:
raise ValueError(f'"{args.local_port}" is not a valid port range. i.e. we accept value like "25441-65540".')

try:
min_port, max_port = args.local_port.split('-', 1)
except ValueError:
raise ValueError(f'The port range you gave in input "{args.local_port}" is not a valid range.')

if min_port == "":
raise ValueError("Negative port number are all invalid values.")
if max_port == "":
raise ValueError('Port range requires both start and end ports to be specified. e.g. "25441-65540".')

try:
min_port, max_port = int(min_port), int(max_port)
except ValueError:
raise ValueError(f'Either "{min_port}" or/and "{max_port}" is an invalid port number.')

source_address = (args.interface or "0.0.0.0", randint(int(min_port), int(max_port)))

parsed_url = parse_url(args.url)
Expand All @@ -91,6 +118,20 @@ def collect_messages(
else:
resolver = [ensure_resolver, "system://"]

force_opt_count = [args.force_http1, args.force_http2, args.force_http3].count(True)
disable_opt_count = [args.disable_http1, args.disable_http2, args.disable_http3].count(True)

if force_opt_count > 1:
raise ValueError(
'You may only force one of --http1, --http2 or --http3. Use --disable-http1, '
'--disable-http2 or --disable-http3 instead if you prefer the excluding logic.'
)
elif force_opt_count == 1 and disable_opt_count:
raise ValueError(
'You cannot both force a http protocol version and disable some other. e.g. '
'--http2 already force HTTP/2, do not use --disable-http1 at the same time.'
)

if args.force_http1:
args.disable_http1 = False
args.disable_http2 = True
Expand Down Expand Up @@ -245,11 +286,22 @@ def build_requests_session(
if quic_cache is not None:
requests_session.quic_cache_layer = QuicCapabilityCache(quic_cache)

if urllib3.util.connection.HAS_IPV6 is False and disable_ipv4 is True:
raise ValueError('Unable to force IPv6 because your system lack IPv6 support.')
if disable_ipv4 and disable_ipv6:
raise ValueError('Unable to force both IPv4 and IPv6, omit the flags to allow both. The flags "-6" and "-4" are meant to force one of them.')

if resolver:
resolver_rebuilt = []
for r in resolver:
# assume it is the in-memory resolver
if "://" not in r:
if ":" not in r or r.count(':') != 1:
raise ValueError("The manual resolver for a specific host requires to be formatted like 'hostname:ip'. e.g. 'pie.dev:1.1.1.1'.")
hostname, override_ip = r.split(':')
if hostname.strip() == "" or override_ip.strip() == "":
raise ValueError("The manual resolver for a specific host requires to be formatted like 'hostname:ip'. e.g. 'pie.dev:1.1.1.1'.")
ipaddress.ip_address(override_ip)
r = f"in-memory://default/?hosts={r}"
resolver_rebuilt.append(r)
resolver = resolver_rebuilt
Expand Down
44 changes: 44 additions & 0 deletions tests/test_h2n3.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,50 @@ def test_force_http3(remote_httpbin_secure):
assert HTTP_OK in r


def test_force_multiple_error(remote_httpbin_secure):
r = http(
"--verify=no",
'--http3',
'--http2',
remote_httpbin_secure + '/get',
tolerate_error_exit_status=True,
)

assert 'You may only force one of --http1, --http2 or --http3.' in r.stderr


def test_disable_all_error_https(remote_httpbin_secure):
r = http(
"--verify=no",
'--disable-http1',
'--disable-http2',
'--disable-http3',
remote_httpbin_secure + '/get',
tolerate_error_exit_status=True,
)

assert 'You disabled every supported protocols.' in r.stderr


def test_disable_all_error_http(remote_httpbin):
r = http(
"--verify=no",
'--disable-http1',
'--disable-http2',
remote_httpbin + '/get',
tolerate_error_exit_status=True,
)

try:
import qh3
except ImportError:
# that branch means that the user does not have HTTP/3
# so, the message says that he disabled everything.
assert 'You disabled every supported protocols.' in r.stderr
else:
assert 'No compatible protocol are enabled to emit request. You currently are connected using TCP Unencrypted and must have HTTP/1.1 or/and HTTP/2 enabled to pursue.' in r.stderr


@pytest.fixture
def with_quic_cache_persistent(tmp_path):
env = PersistentMockEnvironment()
Expand Down
84 changes: 84 additions & 0 deletions tests/test_network.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,90 @@ def test_ensure_interface_and_port_parameters(httpbin):
assert HTTP_OK in r


def test_invalid_interface_given(httpbin):
r = http(
"--interface=10.25.a.u", # invalid IP
httpbin + "/get",
tolerate_error_exit_status=True,
)

assert "'10.25.a.u' does not appear to be an IPv4 or IPv6 address" in r.stderr

r = http(
"--interface=abc", # invalid IP
httpbin + "/get",
tolerate_error_exit_status=True,
)

assert "'abc' does not appear to be an IPv4 or IPv6 address" in r.stderr


def test_invalid_local_port_given(httpbin):
r = http(
"--local-port=127.0.0.1", # invalid port
httpbin + "/get",
tolerate_error_exit_status=True,
)

assert '"127.0.0.1" is not a valid port number.' in r.stderr

r = http(
"--local-port=a8", # invalid port
httpbin + "/get",
tolerate_error_exit_status=True,
)

assert '"a8" is not a valid port number.' in r.stderr

r = http(
"--local-port=-8", # invalid port
httpbin + "/get",
tolerate_error_exit_status=True,
)

assert 'Negative port number are all invalid values.' in r.stderr

r = http(
"--local-port=a-8", # invalid port range
httpbin + "/get",
tolerate_error_exit_status=True,
)

assert 'Either "a" or/and "8" is an invalid port number.' in r.stderr

r = http(
"--local-port=5555-", # invalid port range
httpbin + "/get",
tolerate_error_exit_status=True,
)

assert 'Port range requires both start and end ports to be specified.' in r.stderr


def test_force_ipv6_on_unsupported_system(remote_httpbin):
from httpie.compat import urllib3
urllib3.util.connection.HAS_IPV6 = False
r = http(
"-6", # invalid port
remote_httpbin + "/get",
tolerate_error_exit_status=True,
)
urllib3.util.connection.HAS_IPV6 = True

assert 'Unable to force IPv6 because your system lack IPv6 support.' in r.stderr


def test_force_both_ipv6_and_ipv4(remote_httpbin):
r = http(
"-6", # force IPv6
"-4", # force IPv4
remote_httpbin + "/get",
tolerate_error_exit_status=True,
)

assert 'Unable to force both IPv4 and IPv6, omit the flags to allow both.' in r.stderr


def test_happy_eyeballs(remote_httpbin_secure):
r = http(
"--heb", # this will automatically and concurrently try IPv6 and IPv4 endpoints
Expand Down
34 changes: 34 additions & 0 deletions tests/test_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,37 @@ def test_ensure_override_resolver_used(remote_httpbin):
)

assert "Request timed out" in r.stderr or "A socket operation was attempted to an unreachable network" in r.stderr


def test_invalid_override_resolver():
r = http(
"--resolver=pie.dev:abc", # we do this nonsense on purpose
"pie.dev/get",
tolerate_error_exit_status=True
)

assert "'abc' does not appear to be an IPv4 or IPv6 address" in r.stderr

r = http(
"--resolver=abc", # we do this nonsense on purpose
"pie.dev/get",
tolerate_error_exit_status=True
)

assert "The manual resolver for a specific host requires to be formatted like" in r.stderr

r = http(
"--resolver=pie.dev:127.0.0", # we do this nonsense on purpose
"pie.dev/get",
tolerate_error_exit_status=True
)

assert "'127.0.0' does not appear to be an IPv4 or IPv6 address" in r.stderr

r = http(
"--resolver=doz://example.com", # we do this nonsense on purpose
"pie.dev/get",
tolerate_error_exit_status=True
)

assert "'doz' is not a valid ProtocolResolver" in r.stderr

0 comments on commit bbdb20c

Please sign in to comment.