diff --git a/httpie/cli/argparser.py b/httpie/cli/argparser.py index 9bf09b3b73..0c74c3392e 100644 --- a/httpie/cli/argparser.py +++ b/httpie/cli/argparser.py @@ -413,7 +413,20 @@ 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: + # An optional argument sitting between METHOD/URL and the + # first REQUEST_ITEM splits the positional run in two, and + # argparse fills each run's slots independently: METHOD + # and URL end up absorbing the first run between them, + # leaving REQUEST_ITEM to catch whatever comes after the + # split, which is why method lands as None here even + # though a request item was given. + self.error( + 'METHOD, URL and REQUEST_ITEM all have to sit next to ' + 'each other with nothing else in between; move any ' + 'other arguments before METHOD or after the last ' + 'REQUEST_ITEM.' + ) if self.has_input_data: self.args.method = HTTP_POST else: diff --git a/tests/test_httpie.py b/tests/test_httpie.py index 5824340cda..4396854fa5 100644 --- a/tests/test_httpie.py +++ b/tests/test_httpie.py @@ -1,5 +1,6 @@ """High-level tests.""" import io +import sys from unittest import mock import pytest @@ -143,6 +144,35 @@ def test_form_POST_file_redirected_stdin(httpbin): assert 'cannot be mixed' in r.stderr +@pytest.mark.skipif( + sys.version_info < (3, 13), + reason=( + "argparse only splits the positional run this way on 3.13+; " + "on earlier versions the same command is rejected upstream " + "with its own 'unrecognized arguments' error before _guess_method " + "ever runs, so there's nothing here for the fix to catch" + ), +) +def test_option_between_method_and_url_reports_a_clear_error(): + """ + https://github.com/httpie/cli/issues/1614 + + An option sitting between METHOD and URL splits the positional run + argparse sees into two, and on Python 3.13+ argparse fills each half + on its own: METHOD and URL absorb the first half, URL's real value + gets read back as a REQUEST_ITEM, and METHOD is left unset entirely. + That used to reach an internal assertion and crash with a bare + AssertionError instead of telling the caller what went wrong. + """ + r = http( + 'POST', '--auth-type', 'bearer', '--auth', 'token', + 'http://example.org', + tolerate_error_exit_status=True, + ) + assert r.exit_status == ExitStatus.ERROR + assert 'METHOD, URL and REQUEST_ITEM' in r.stderr + + def test_raw_POST_key_values_supplied(httpbin): r = http( '--raw',