diff --git a/httpie/cli/argparser.py b/httpie/cli/argparser.py index 9bf09b3b73..b5550f5e9f 100644 --- a/httpie/cli/argparser.py +++ b/httpie/cli/argparser.py @@ -377,7 +377,19 @@ 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 URL`), + # argparse may fail to associate the URL with the `url` + # positional and leave it as an unrecognized argument. + # Detect this case and rearrange the arguments. + if (len(invalid) == 1 + and self.args.method is None + and self.args.url + and re.match('^[a-zA-Z]+$', self.args.url)): + self.args.method = self.args.url + self.args.url = invalid[0] + else: + self.error(f'unrecognized arguments: {" ".join(invalid)}') def _body_from_file(self, fd): """Read the data from a file-like object. @@ -412,9 +424,20 @@ def _guess_method(self): """ if self.args.method is None: - # Invoked as `http URL'. - assert not self.args.request_items - if self.has_input_data: + if self.args.request_items: + if re.match('^[a-zA-Z]+$', self.args.url): + # `url` holds a method name and the actual URL was + # parsed as a request item (Python 3.13+ argparse). + self.args.method = self.args.url + first_item = self.args.request_items.pop(0) + self.args.url = first_item.orig + else: + self.error( + 'Got unexpected request items with no HTTP ' + 'method specified. Usage:\n\n' + ' http [METHOD] URL [REQUEST_ITEM ...]\n' + ) + elif self.has_input_data: self.args.method = HTTP_POST else: self.args.method = HTTP_GET diff --git a/tests/test_cli.py b/tests/test_cli.py index 2cd27574af..537671ba52 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -317,6 +317,39 @@ def test_guess_when_method_set_but_invalid_and_item_exists(self): ] + def test_guess_when_method_none_and_request_items_present(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.org', + sep=':', orig='https://example.org') + ] + self.parser.args.ignore_stdin = False + self.parser.env = MockEnvironment() + self.parser.has_input_data = False + self.parser._guess_method() + assert self.parser.args.method == 'POST' + assert self.parser.args.url == 'https://example.org' + assert self.parser.args.request_items == [] + + +class TestOptionsBeforeURL: + + def test_method_with_options_before_url(self): + r = http('--offline', 'POST', '--print=H', + '--auth-type', 'bearer', '--auth', 'token123', + 'https://example.org') + assert 'POST / HTTP/1.1' in r + assert 'Authorization: Bearer token123' in r + + def test_get_with_options_before_url(self): + r = http('--offline', 'GET', '--print=H', + '--verbose', 'https://example.org') + assert 'GET / HTTP/1.1' in r + + class TestNoOptions: def test_valid_no_options(self, httpbin):