Skip to content

Commit bb4ce6e

Browse files
author
Ahmed TAHRI
committed
improve error messages upon invalid args/values in new flags
1 parent da6cc13 commit bb4ce6e

File tree

4 files changed

+209
-2
lines changed

4 files changed

+209
-2
lines changed

httpie/client.py

+54-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from time import monotonic
1010
from typing import Any, Dict, Callable, Iterable
1111
from urllib.parse import urlparse, urlunparse
12+
import ipaddress
1213

1314
import niquests
1415

@@ -71,12 +72,38 @@ def collect_messages(
7172
source_address = None
7273

7374
if args.interface:
75+
# automatically raises ValueError upon invalid IP
76+
ipaddress.ip_address(args.interface)
77+
7478
source_address = (args.interface, 0)
7579
if args.local_port:
80+
7681
if '-' not in args.local_port:
77-
source_address = (args.interface or "0.0.0.0", int(args.local_port))
82+
try:
83+
parsed_port = int(args.local_port)
84+
except ValueError:
85+
raise ValueError(f'"{args.local_port}" is not a valid port number.')
86+
87+
source_address = (args.interface or "0.0.0.0", parsed_port)
7888
else:
79-
min_port, max_port = args.local_port.split('-', 1)
89+
if args.local_port.count('-') != 1:
90+
raise ValueError(f'"{args.local_port}" is not a valid port range. i.e. we accept value like "25441-65540".')
91+
92+
try:
93+
min_port, max_port = args.local_port.split('-', 1)
94+
except ValueError:
95+
raise ValueError(f'The port range you gave in input "{args.local_port}" is not a valid range.')
96+
97+
if min_port == "":
98+
raise ValueError("Negative port number are all invalid values.")
99+
if max_port == "":
100+
raise ValueError('Port range requires both start and end ports to be specified. e.g. "25441-65540".')
101+
102+
try:
103+
min_port, max_port = int(min_port), int(max_port)
104+
except ValueError:
105+
raise ValueError(f'Either "{min_port}" or/and "{max_port}" is an invalid port number.')
106+
80107
source_address = (args.interface or "0.0.0.0", randint(int(min_port), int(max_port)))
81108

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

121+
force_opt_count = [args.force_http1, args.force_http2, args.force_http3].count(True)
122+
disable_opt_count = [args.disable_http1, args.disable_http2, args.disable_http3].count(True)
123+
124+
if force_opt_count > 1:
125+
raise ValueError(
126+
'You may only force one of --http1, --http2 or --http3. Use --disable-http1, '
127+
'--disable-http2 or --disable-http3 instead if you prefer the excluding logic.'
128+
)
129+
elif force_opt_count == 1 and disable_opt_count:
130+
raise ValueError(
131+
'You cannot both force a http protocol version and disable some other. e.g. '
132+
'--http2 already force HTTP/2, do not use --disable-http1 at the same time.'
133+
)
134+
94135
if args.force_http1:
95136
args.disable_http1 = False
96137
args.disable_http2 = True
@@ -245,11 +286,22 @@ def build_requests_session(
245286
if quic_cache is not None:
246287
requests_session.quic_cache_layer = QuicCapabilityCache(quic_cache)
247288

289+
if urllib3.util.connection.HAS_IPV6 is False and disable_ipv4 is True:
290+
raise ValueError('Unable to force IPv6 because your system lack IPv6 support.')
291+
if disable_ipv4 and disable_ipv6:
292+
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.')
293+
248294
if resolver:
249295
resolver_rebuilt = []
250296
for r in resolver:
251297
# assume it is the in-memory resolver
252298
if "://" not in r:
299+
if ":" not in r or r.count(':') != 1:
300+
raise ValueError("The manual resolver for a specific host requires to be formatted like 'hostname:ip'. e.g. 'pie.dev:1.1.1.1'.")
301+
hostname, override_ip = r.split(':')
302+
if hostname.strip() == "" or override_ip.strip() == "":
303+
raise ValueError("The manual resolver for a specific host requires to be formatted like 'hostname:ip'. e.g. 'pie.dev:1.1.1.1'.")
304+
ipaddress.ip_address(override_ip)
253305
r = f"in-memory://default/?hosts={r}"
254306
resolver_rebuilt.append(r)
255307
resolver = resolver_rebuilt

tests/test_h2n3.py

+37
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,43 @@ def test_force_http3(remote_httpbin_secure):
4646
assert HTTP_OK in r
4747

4848

49+
def test_force_multiple_error(remote_httpbin_secure):
50+
r = http(
51+
"--verify=no",
52+
'--http3',
53+
'--http2',
54+
remote_httpbin_secure + '/get',
55+
tolerate_error_exit_status=True,
56+
)
57+
58+
assert 'You may only force one of --http1, --http2 or --http3.' in r.stderr
59+
60+
61+
def test_disable_all_error_https(remote_httpbin_secure):
62+
r = http(
63+
"--verify=no",
64+
'--disable-http1',
65+
'--disable-http2',
66+
'--disable-http3',
67+
remote_httpbin_secure + '/get',
68+
tolerate_error_exit_status=True,
69+
)
70+
71+
assert 'You disabled every supported protocols.' in r.stderr
72+
73+
74+
def test_disable_all_error_http(remote_httpbin):
75+
r = http(
76+
"--verify=no",
77+
'--disable-http1',
78+
'--disable-http2',
79+
remote_httpbin + '/get',
80+
tolerate_error_exit_status=True,
81+
)
82+
83+
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
84+
85+
4986
@pytest.fixture
5087
def with_quic_cache_persistent(tmp_path):
5188
env = PersistentMockEnvironment()

tests/test_network.py

+84
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,90 @@ def test_ensure_interface_and_port_parameters(httpbin):
3838
assert HTTP_OK in r
3939

4040

41+
def test_invalid_interface_given(httpbin):
42+
r = http(
43+
"--interface=10.25.a.u", # invalid IP
44+
httpbin + "/get",
45+
tolerate_error_exit_status=True,
46+
)
47+
48+
assert "'10.25.a.u' does not appear to be an IPv4 or IPv6 address" in r.stderr
49+
50+
r = http(
51+
"--interface=abc", # invalid IP
52+
httpbin + "/get",
53+
tolerate_error_exit_status=True,
54+
)
55+
56+
assert "'abc' does not appear to be an IPv4 or IPv6 address" in r.stderr
57+
58+
59+
def test_invalid_local_port_given(httpbin):
60+
r = http(
61+
"--local-port=127.0.0.1", # invalid port
62+
httpbin + "/get",
63+
tolerate_error_exit_status=True,
64+
)
65+
66+
assert '"127.0.0.1" is not a valid port number.' in r.stderr
67+
68+
r = http(
69+
"--local-port=a8", # invalid port
70+
httpbin + "/get",
71+
tolerate_error_exit_status=True,
72+
)
73+
74+
assert '"a8" is not a valid port number.' in r.stderr
75+
76+
r = http(
77+
"--local-port=-8", # invalid port
78+
httpbin + "/get",
79+
tolerate_error_exit_status=True,
80+
)
81+
82+
assert 'Negative port number are all invalid values.' in r.stderr
83+
84+
r = http(
85+
"--local-port=a-8", # invalid port range
86+
httpbin + "/get",
87+
tolerate_error_exit_status=True,
88+
)
89+
90+
assert 'Either "a" or/and "8" is an invalid port number.' in r.stderr
91+
92+
r = http(
93+
"--local-port=5555-", # invalid port range
94+
httpbin + "/get",
95+
tolerate_error_exit_status=True,
96+
)
97+
98+
assert 'Port range requires both start and end ports to be specified.' in r.stderr
99+
100+
101+
def test_force_ipv6_on_unsupported_system(remote_httpbin):
102+
from httpie.compat import urllib3
103+
urllib3.util.connection.HAS_IPV6 = False
104+
r = http(
105+
"-6", # invalid port
106+
remote_httpbin + "/get",
107+
tolerate_error_exit_status=True,
108+
)
109+
urllib3.util.connection.HAS_IPV6 = True
110+
111+
assert 'Unable to force IPv6 because your system lack IPv6 support.' in r.stderr
112+
113+
114+
def test_force_both_ipv6_and_ipv4(remote_httpbin):
115+
r = http(
116+
"-6", # force IPv6
117+
"-4", # force IPv4
118+
remote_httpbin + "/get",
119+
tolerate_error_exit_status=True,
120+
)
121+
122+
assert 'Unable to force both IPv4 and IPv6, omit the flags to allow both.' in r.stderr
123+
124+
41125
def test_happy_eyeballs(remote_httpbin_secure):
42126
r = http(
43127
"--heb", # this will automatically and concurrently try IPv6 and IPv4 endpoints

tests/test_resolver.py

+34
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,37 @@ def test_ensure_override_resolver_used(remote_httpbin):
3232
)
3333

3434
assert "Request timed out" in r.stderr or "A socket operation was attempted to an unreachable network" in r.stderr
35+
36+
37+
def test_invalid_override_resolver():
38+
r = http(
39+
"--resolver=pie.dev:abc", # we do this nonsense on purpose
40+
"pie.dev/get",
41+
tolerate_error_exit_status=True
42+
)
43+
44+
assert "'abc' does not appear to be an IPv4 or IPv6 address" in r.stderr
45+
46+
r = http(
47+
"--resolver=abc", # we do this nonsense on purpose
48+
"pie.dev/get",
49+
tolerate_error_exit_status=True
50+
)
51+
52+
assert "The manual resolver for a specific host requires to be formatted like" in r.stderr
53+
54+
r = http(
55+
"--resolver=pie.dev:127.0.0", # we do this nonsense on purpose
56+
"pie.dev/get",
57+
tolerate_error_exit_status=True
58+
)
59+
60+
assert "'127.0.0' does not appear to be an IPv4 or IPv6 address" in r.stderr
61+
62+
r = http(
63+
"--resolver=doz://example.com", # we do this nonsense on purpose
64+
"pie.dev/get",
65+
tolerate_error_exit_status=True
66+
)
67+
68+
assert "'doz' is not a valid ProtocolResolver" in r.stderr

0 commit comments

Comments
 (0)