improve error messages upon invalid args/values in new flags

This commit is contained in:
Ahmed TAHRI 2024-10-22 18:17:09 +02:00
parent da6cc13b8b
commit e375c259e8
4 changed files with 216 additions and 2 deletions

View File

@ -9,6 +9,7 @@ from random import randint
from time import monotonic
from typing import Any, Dict, Callable, Iterable
from urllib.parse import urlparse, urlunparse
import ipaddress
import niquests
@ -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)
@ -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
@ -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

View File

@ -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 # noqa: F401
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()

View File

@ -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

View File

@ -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