From c6d006f5b79f47721f6860f8bb530c3c30b9a443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Afonso=20Janu=C3=A1rio?= Date: Sat, 29 Aug 2026 12:46:00 +0100 Subject: [PATCH 1/2] Turn a bare AssertionError into a real error message An optional argument sitting between METHOD/URL and the first REQUEST_ITEM splits the positional run argparse sees into two pieces. On Python 3.13, argparse fills each piece's slots independently, so a command like `http POST --auth-type bearer --auth token URL` ends up with METHOD unset, URL holding the string "POST", and the actual URL misparsed as the first REQUEST_ITEM instead. _guess_method assumed that whenever METHOD came back unset, request_items would still be empty, and asserted exactly that, so this specific misparse crashed with a bare AssertionError and no indication of what went wrong. The assertion is now a proper user-facing error explaining that METHOD, URL and REQUEST_ITEM have to sit next to each other with nothing else in between, matching what the reporter said they actually expected to see. Added a regression test reproducing the exact command from the report, checking it exits with an error and prints the new message instead of crashing. --- httpie/cli/argparser.py | 15 ++++++++++++++- tests/test_httpie.py | 20 ++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) 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..6fa85ca646 100644 --- a/tests/test_httpie.py +++ b/tests/test_httpie.py @@ -143,6 +143,26 @@ def test_form_POST_file_redirected_stdin(httpbin): assert 'cannot be mixed' in r.stderr +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', From ea3b9dd15fb99566095ff996a6f707c36724d2b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Afonso=20Janu=C3=A1rio?= Date: Sat, 29 Aug 2026 12:59:04 +0100 Subject: [PATCH 2/2] Skip the METHOD/URL split regression test below Python 3.13 The reported crash only happens because argparse's own handling of a split positional run changed in 3.13: on every earlier version the same command line gets rejected upstream with its own 'unrecognized arguments' error, well before _guess_method ever runs, so there's nothing for this test to exercise there. CI caught it running the suite across the full version matrix, where the assertion on the new error message failed on 3.8 through 3.12 since the command never reaches that code path on those versions at all. --- tests/test_httpie.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_httpie.py b/tests/test_httpie.py index 6fa85ca646..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,15 @@ 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