Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 41 additions & 5 deletions httpie/cli/argparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,32 @@ def _apply_no_options(self, no_options):
invalid.append(option)

if invalid:
self.error(f'unrecognized arguments: {" ".join(invalid)}')
# When options are placed between METHOD and URL, e.g.:
# http POST --auth-type bearer --auth TOKEN https://example.org
# argparse may fail to associate the URL with the correct
# positional argument. The method string ends up in args.url,
# and the real URL (plus any request items) lands here.
if (
self.args.method is None
and re.match(r'^[a-zA-Z]+$', self.args.url)
):
self.args.method = self.args.url.upper()
self.args.url = invalid.pop(0)
if invalid:
if self.args.request_items is None:
self.args.request_items = []
item_type = KeyValueArgType(
*SEPARATOR_GROUP_ALL_ITEMS)
for arg in invalid:
try:
self.args.request_items.append(
item_type(arg))
except argparse.ArgumentTypeError:
self.error(
f'unrecognized arguments: {arg}')
else:
self.error(
f'unrecognized arguments: {" ".join(invalid)}')

def _body_from_file(self, fd):
"""Read the data from a file-like object.
Expand Down Expand Up @@ -412,12 +437,23 @@ def _guess_method(self):

"""
if self.args.method is None:
# Invoked as `http URL'.
assert not self.args.request_items
if self.args.request_items:
# On some Python versions (3.13+), argparse may put the URL
# into request_items instead of no_options. Detect and fix.
if re.match(r'^[a-zA-Z]+$', self.args.url):
self.args.method = self.args.url.upper()
self.args.url = self.args.request_items.pop(0).orig
else:
self.error(
'No valid HTTP method found but request items '
'are present. Please specify the method '
'explicitly, e.g. `http GET URL` or '
'`http POST URL data=value`.'
)
if self.has_input_data:
self.args.method = HTTP_POST
self.args.method = self.args.method or HTTP_POST
else:
self.args.method = HTTP_GET
self.args.method = self.args.method or HTTP_GET

# FIXME: False positive, e.g., "localhost" matches but is a valid URL.
elif not re.match('^[a-zA-Z]+$', self.args.method):
Expand Down
31 changes: 31 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,37 @@ def test_guess_when_method_set_but_invalid_and_item_exists(self):
key='old_item', value='b', sep='=', orig='old_item=b'),
]

def test_guess_when_method_none_url_is_method_and_url_in_request_items(self):
self.parser.args = argparse.Namespace()
self.parser.args.method = None
self.parser.args.url = 'POST'
self.parser.args.request_items = [
KeyValueArg(
key='https', value='//example.com/', sep=':', orig='https://example.com/')
]
self.parser.args.ignore_stdin = False
self.parser.env = MockEnvironment()
self.parser._guess_method()
assert self.parser.args.method == 'POST'
assert self.parser.args.url == 'https://example.com/'
assert self.parser.args.request_items == []


class TestOptionsBeforeURL:

def test_method_with_options_before_url(self, httpbin):
r = http('--ignore-stdin', 'POST', '--verbose', httpbin + '/post')
assert 'POST /post HTTP/1.1' in r

def test_method_with_auth_options_before_url(self, httpbin):
r = http(
'--ignore-stdin', 'GET',
'--auth-type', 'bearer', '--auth', 'TOKEN',
httpbin + '/get',
)
assert HTTP_OK in r
assert 'Bearer TOKEN' in r


class TestNoOptions:

Expand Down
Loading