Skip to content

Commit 34c0f1c

Browse files
author
Agent Zero
committed
feat: Add --sequence flag for sequential request execution from STDIN
Implements the ability to execute multiple HTTPie request definitions from STDIN sequentially, as requested in issue #1683. Features: - New --sequence flag to enable sequential mode - Reads request definitions from STDIN, one per line - Executes requests strictly in order, one at a time - Shows progress indicator in TTY mode - Handles errors gracefully without stopping execution - Comment lines starting with # are ignored - Empty lines are skipped - Uses --ignore-stdin for individual requests to prevent conflicts Example usage: $ cat requests.http | http --sequence $ echo -e "GET httpbin.org/get\nPOST httpbin.org/post foo=bar" | http --sequence Tests: - Added comprehensive test suite (8 tests, all passing) - All existing tests continue to pass Closes #1683
1 parent 5b604c3 commit 34c0f1c

3 files changed

Lines changed: 262 additions & 3 deletions

File tree

httpie/cli/definition.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,31 @@
239239
240240
""",
241241
)
242+
processing_options.add_argument(
243+
'--sequence',
244+
action='store_true',
245+
default=False,
246+
short_help='Execute multiple requests from STDIN sequentially.',
247+
help="""
248+
Read and execute multiple HTTPie request definitions from STDIN,
249+
one request per line, sequentially.
250+
251+
Each line should contain a complete HTTPie command without the
252+
leading "http" program name, e.g.:
253+
254+
GET https://httpbin.org/get
255+
POST https://httpbin.org/post name=alice
256+
GET https://httpbin.org/headers
257+
258+
Example usage:
259+
260+
$ cat requests.http | http --sequence
261+
$ echo -e "GET httpbin.org/get\nPOST httpbin.org/post foo=bar" | http --sequence
262+
263+
Requests are executed strictly in order, one at a time.
264+
265+
""",
266+
)
242267

243268

244269
#######################################################################

httpie/core.py

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,9 +126,9 @@ def handle_generic_error(e, annotation=None):
126126
original_exc = unwrap_context(exc)
127127
if isinstance(original_exc, socket.gaierror):
128128
if original_exc.errno == socket.EAI_AGAIN:
129-
annotation = '\nCouldnt connect to a DNS server. Please check your connection and try again.'
129+
annotation = '\nCouldn\'t connect to a DNS server. Please check your connection and try again.'
130130
elif original_exc.errno == socket.EAI_NONAME:
131-
annotation = '\nCouldnt resolve the given hostname. Please check the URL and try again.'
131+
annotation = '\nCouldn\'t resolve the given hostname. Please check the URL and try again.'
132132
propagated_exc = original_exc
133133
else:
134134
propagated_exc = exc
@@ -167,10 +167,95 @@ def main(
167167
)
168168

169169

170+
def run_sequence_mode(args: argparse.Namespace, env: Environment) -> ExitStatus:
171+
"""
172+
Execute multiple HTTPie request definitions from STDIN sequentially.
173+
174+
Each line in STDIN is treated as a separate HTTPie command without
175+
the leading 'http' program name.
176+
177+
Example input:
178+
GET https://httpbin.org/get
179+
POST https://httpbin.org/post name=alice
180+
GET https://httpbin.org/headers
181+
"""
182+
import shlex
183+
from .cli.definition import parser
184+
185+
exit_status = ExitStatus.SUCCESS
186+
request_lines = []
187+
188+
# Read all request definitions from STDIN
189+
try:
190+
for line in env.stdin:
191+
line = line.strip()
192+
if line and not line.startswith('#'):
193+
request_lines.append(line)
194+
except KeyboardInterrupt:
195+
env.stderr.write('\n')
196+
return ExitStatus.ERROR_CTRL_C
197+
198+
if not request_lines:
199+
env.log_error('No request definitions found in STDIN.')
200+
return ExitStatus.ERROR
201+
202+
# Execute each request sequentially
203+
for i, request_line in enumerate(request_lines, 1):
204+
if env.stdout_isatty:
205+
env.stderr.write(f'\n[{i}/{len(request_lines)}] {request_line}\n')
206+
207+
try:
208+
# Parse the request line as if it were CLI arguments
209+
# Add --ignore-stdin to prevent argparser from using stdin for request body
210+
request_args = shlex.split(request_line) + ['--ignore-stdin']
211+
212+
# Create a fresh environment for this request to avoid stdin conflicts
213+
request_env = Environment(
214+
stdin=None, # No stdin for individual requests in sequence mode
215+
stdout=env.stdout,
216+
stderr=env.stderr,
217+
)
218+
request_env.program_name = env.program_name
219+
220+
# Parse the request
221+
request_namespace = parser.parse_args(
222+
args=request_args,
223+
env=request_env,
224+
)
225+
226+
# Execute the request (call _program to avoid sequence check loop)
227+
request_exit_status = _program(request_namespace, request_env)
228+
229+
# Track the worst exit status
230+
if request_exit_status != ExitStatus.SUCCESS:
231+
exit_status = request_exit_status
232+
233+
except SystemExit as e:
234+
# Handle SystemExit from argument parser errors
235+
if e.code != ExitStatus.SUCCESS:
236+
exit_status = ExitStatus.ERROR if e.code is None or e.code != 0 else ExitStatus(e.code)
237+
except Exception as e:
238+
env.log_error(f'Error executing request [{i}]: {e}')
239+
exit_status = ExitStatus.ERROR
240+
241+
return exit_status
242+
243+
170244
def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
171245
"""
172246
The main program without error handling.
173247
248+
"""
249+
# Handle --sequence mode: execute multiple requests from STDIN
250+
if getattr(args, 'sequence', False):
251+
return run_sequence_mode(args, env)
252+
253+
return _program(args, env)
254+
255+
256+
def _program(args: argparse.Namespace, env: Environment) -> ExitStatus:
257+
"""
258+
The actual program implementation without the --sequence check.
174259
"""
175260
# TODO: Refactor and drastically simplify, especially so that the separator logic is elsewhere.
176261
exit_status = ExitStatus.SUCCESS
@@ -209,7 +294,7 @@ def request_body_read_callback(chunk: bytes):
209294
force_separator = False
210295
prev_with_body = False
211296

212-
# Process messages as theyre generated
297+
# Process messages as they're generated
213298
for message in messages:
214299
output_options = OutputOptions.from_message(message, args.output_options)
215300

tests/test_sequence_mode.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
"""Tests for --sequence mode: execute multiple requests from STDIN."""
2+
import pytest
3+
from io import StringIO
4+
from unittest.mock import patch, MagicMock
5+
6+
from httpie.core import run_sequence_mode, program
7+
from httpie.context import Environment
8+
from httpie.status import ExitStatus
9+
10+
11+
class TestSequenceMode:
12+
"""Test the --sequence feature for executing multiple requests from STDIN."""
13+
14+
def test_sequence_empty_stdin(self):
15+
"""Sequence mode with empty STDIN should return error."""
16+
env = Environment()
17+
env.stdin = StringIO('')
18+
19+
args = MagicMock()
20+
args.sequence = True
21+
22+
result = run_sequence_mode(args, env)
23+
24+
assert result == ExitStatus.ERROR
25+
26+
def test_sequence_only_comments(self):
27+
"""Sequence mode with only comments should return error."""
28+
env = Environment()
29+
env.stdin = StringIO('# This is a comment\n# Another comment\n')
30+
31+
args = MagicMock()
32+
args.sequence = True
33+
34+
result = run_sequence_mode(args, env)
35+
36+
assert result == ExitStatus.ERROR
37+
38+
def test_sequence_parses_request_lines(self):
39+
"""Sequence mode should parse request lines from STDIN."""
40+
env = Environment()
41+
env.stdin = StringIO('GET http://example.com\nPOST http://example.com data=test\n')
42+
env.stdout = StringIO()
43+
env.stdout_isatty = False
44+
45+
args = MagicMock()
46+
args.sequence = True
47+
48+
# Mock _program to avoid actual HTTP requests
49+
with patch('httpie.core._program') as mock_program:
50+
mock_program.return_value = ExitStatus.SUCCESS
51+
52+
result = run_sequence_mode(args, env)
53+
54+
# Should have called _program twice (once per request)
55+
assert mock_program.call_count == 2
56+
assert result == ExitStatus.SUCCESS
57+
58+
def test_sequence_skips_empty_lines(self):
59+
"""Sequence mode should skip empty lines."""
60+
env = Environment()
61+
env.stdin = StringIO('\nGET http://example.com\n\nPOST http://example.com\n\n')
62+
env.stdout = StringIO()
63+
env.stdout_isatty = False
64+
65+
args = MagicMock()
66+
args.sequence = True
67+
68+
with patch('httpie.core._program') as mock_program:
69+
mock_program.return_value = ExitStatus.SUCCESS
70+
71+
result = run_sequence_mode(args, env)
72+
73+
# Should have called _program twice (empty lines skipped)
74+
assert mock_program.call_count == 2
75+
76+
def test_sequence_handles_keyboard_interrupt(self):
77+
"""Sequence mode should handle KeyboardInterrupt gracefully."""
78+
env = Environment()
79+
80+
# Simulate KeyboardInterrupt during stdin read
81+
class InterruptingStringIO(StringIO):
82+
def __iter__(self):
83+
return self
84+
def __next__(self):
85+
raise KeyboardInterrupt()
86+
87+
env.stdin = InterruptingStringIO()
88+
env.stdout = StringIO()
89+
90+
args = MagicMock()
91+
args.sequence = True
92+
93+
result = run_sequence_mode(args, env)
94+
95+
assert result == ExitStatus.ERROR_CTRL_C
96+
97+
def test_sequence_propagates_error_status(self):
98+
"""Sequence mode should return error if any request fails."""
99+
env = Environment()
100+
env.stdin = StringIO('GET http://example.com\nPOST http://example.com\n')
101+
env.stdout = StringIO()
102+
env.stdout_isatty = False
103+
104+
args = MagicMock()
105+
args.sequence = True
106+
107+
with patch('httpie.core._program') as mock_program:
108+
# First call succeeds, second fails
109+
mock_program.side_effect = [ExitStatus.SUCCESS, ExitStatus.ERROR]
110+
111+
result = run_sequence_mode(args, env)
112+
113+
assert result == ExitStatus.ERROR
114+
115+
def test_sequence_handles_request_exception(self):
116+
"""Sequence mode should handle exceptions during request execution."""
117+
env = Environment()
118+
env.stdin = StringIO('GET http://example.com\n')
119+
env.stdout = StringIO()
120+
env.stdout_isatty = False
121+
122+
args = MagicMock()
123+
args.sequence = True
124+
125+
with patch('httpie.core._program') as mock_program:
126+
mock_program.side_effect = Exception('Request failed')
127+
128+
result = run_sequence_mode(args, env)
129+
130+
assert result == ExitStatus.ERROR
131+
132+
def test_sequence_executes_multiple_requests(self):
133+
"""Sequence mode should execute multiple requests sequentially."""
134+
env = Environment()
135+
env.stdin = StringIO('GET http://example.com\nPOST http://example.com\nPUT http://example.com\n')
136+
env.stdout = StringIO()
137+
env.stdout_isatty = False
138+
139+
args = MagicMock()
140+
args.sequence = True
141+
142+
with patch('httpie.core._program') as mock_program:
143+
mock_program.return_value = ExitStatus.SUCCESS
144+
145+
result = run_sequence_mode(args, env)
146+
147+
# Should have called _program 3 times for 3 requests
148+
assert mock_program.call_count == 3
149+
assert result == ExitStatus.SUCCESS

0 commit comments

Comments
 (0)