Skip to content

Commit c03268a

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 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 in tests/test_sequence_mode.py - All existing tests continue to pass Closes #1683
1 parent 5b604c3 commit c03268a

3 files changed

Lines changed: 254 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: 68 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,11 +167,76 @@ 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+
request_args = shlex.split(request_line)
210+
211+
# Create a fresh namespace for this request
212+
request_namespace = parser.parse_args(
213+
args=request_args,
214+
env=env,
215+
)
216+
217+
# Execute the request
218+
request_exit_status = program(args=request_namespace, env=env)
219+
220+
# Track the worst exit status
221+
if request_exit_status != ExitStatus.SUCCESS:
222+
exit_status = request_exit_status
223+
224+
except Exception as e:
225+
env.log_error(f'Error executing request [{i}]: {e}')
226+
exit_status = ExitStatus.ERROR
227+
228+
return exit_status
229+
230+
170231
def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
171232
"""
172233
The main program without error handling.
173234
174235
"""
236+
# Handle --sequence mode: execute multiple requests from STDIN
237+
if getattr(args, 'sequence', False):
238+
return run_sequence_mode(args, env)
239+
175240
# TODO: Refactor and drastically simplify, especially so that the separator logic is elsewhere.
176241
exit_status = ExitStatus.SUCCESS
177242
downloader = None
@@ -209,7 +274,7 @@ def request_body_read_callback(chunk: bytes):
209274
force_separator = False
210275
prev_with_body = False
211276

212-
# Process messages as theyre generated
277+
# Process messages as they're generated
213278
for message in messages:
214279
output_options = OutputOptions.from_message(message, args.output_options)
215280

tests/test_sequence_mode.py

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
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+
env.stderr = StringIO()
19+
env.stdout = StringIO()
20+
21+
args = MagicMock()
22+
args.sequence = True
23+
24+
result = run_sequence_mode(args, env)
25+
26+
assert result == ExitStatus.ERROR
27+
assert 'No request definitions found' in env.stderr.getvalue()
28+
29+
def test_sequence_only_comments(self):
30+
"""Sequence mode with only comments should return error."""
31+
env = Environment()
32+
env.stdin = StringIO('# This is a comment\n# Another comment\n')
33+
env.stderr = StringIO()
34+
env.stdout = StringIO()
35+
36+
args = MagicMock()
37+
args.sequence = True
38+
39+
result = run_sequence_mode(args, env)
40+
41+
assert result == ExitStatus.ERROR
42+
assert 'No request definitions found' in env.stderr.getvalue()
43+
44+
def test_sequence_parses_request_lines(self):
45+
"""Sequence mode should parse request lines from STDIN."""
46+
env = Environment()
47+
env.stdin = StringIO('GET http://example.com\nPOST http://example.com data=test\n')
48+
env.stderr = StringIO()
49+
env.stdout = StringIO()
50+
env.stdout_isatty = False
51+
52+
args = MagicMock()
53+
args.sequence = True
54+
55+
# Mock program to avoid actual HTTP requests
56+
with patch('httpie.core.program') as mock_program:
57+
mock_program.return_value = ExitStatus.SUCCESS
58+
59+
result = run_sequence_mode(args, env)
60+
61+
# Should have called program twice (once per request)
62+
assert mock_program.call_count == 2
63+
assert result == ExitStatus.SUCCESS
64+
65+
def test_sequence_skips_empty_lines(self):
66+
"""Sequence mode should skip empty lines."""
67+
env = Environment()
68+
env.stdin = StringIO('\nGET http://example.com\n\nPOST http://example.com\n\n')
69+
env.stderr = StringIO()
70+
env.stdout = StringIO()
71+
env.stdout_isatty = False
72+
73+
args = MagicMock()
74+
args.sequence = True
75+
76+
with patch('httpie.core.program') as mock_program:
77+
mock_program.return_value = ExitStatus.SUCCESS
78+
79+
result = run_sequence_mode(args, env)
80+
81+
# Should have called program twice (empty lines skipped)
82+
assert mock_program.call_count == 2
83+
84+
def test_sequence_handles_keyboard_interrupt(self):
85+
"""Sequence mode should handle KeyboardInterrupt gracefully."""
86+
env = Environment()
87+
env.stdin = StringIO('GET http://example.com\n')
88+
env.stderr = StringIO()
89+
env.stdout = StringIO()
90+
91+
# Simulate KeyboardInterrupt during stdin read
92+
class InterruptingStringIO(StringIO):
93+
def readline(self, *args, **kwargs):
94+
raise KeyboardInterrupt()
95+
96+
env.stdin = InterruptingStringIO('GET http://example.com\n')
97+
98+
args = MagicMock()
99+
args.sequence = True
100+
101+
result = run_sequence_mode(args, env)
102+
103+
assert result == ExitStatus.ERROR_CTRL_C
104+
105+
def test_sequence_propagates_error_status(self):
106+
"""Sequence mode should return error if any request fails."""
107+
env = Environment()
108+
env.stdin = StringIO('GET http://example.com\nPOST http://example.com\n')
109+
env.stderr = StringIO()
110+
env.stdout = StringIO()
111+
env.stdout_isatty = False
112+
113+
args = MagicMock()
114+
args.sequence = True
115+
116+
with patch('httpie.core.program') as mock_program:
117+
# First call succeeds, second fails
118+
mock_program.side_effect = [ExitStatus.SUCCESS, ExitStatus.ERROR]
119+
120+
result = run_sequence_mode(args, env)
121+
122+
assert result == ExitStatus.ERROR
123+
124+
def test_sequence_handles_request_exception(self):
125+
"""Sequence mode should handle exceptions during request execution."""
126+
env = Environment()
127+
env.stdin = StringIO('GET http://example.com\n')
128+
env.stderr = StringIO()
129+
env.stdout = StringIO()
130+
env.stdout_isatty = False
131+
132+
args = MagicMock()
133+
args.sequence = True
134+
135+
with patch('httpie.core.program') as mock_program:
136+
mock_program.side_effect = Exception('Request failed')
137+
138+
result = run_sequence_mode(args, env)
139+
140+
assert result == ExitStatus.ERROR
141+
assert 'Error executing request' in env.stderr.getvalue()
142+
143+
def test_sequence_shows_progress_in_tty(self):
144+
"""Sequence mode should show progress when stdout is a TTY."""
145+
env = Environment()
146+
env.stdin = StringIO('GET http://example.com\nPOST http://example.com\n')
147+
env.stderr = StringIO()
148+
env.stdout = StringIO()
149+
env.stdout_isatty = True
150+
151+
args = MagicMock()
152+
args.sequence = True
153+
154+
with patch('httpie.core.program') as mock_program:
155+
mock_program.return_value = ExitStatus.SUCCESS
156+
157+
result = run_sequence_mode(args, env)
158+
159+
stderr_output = env.stderr.getvalue()
160+
assert '[1/2]' in stderr_output
161+
assert '[2/2]' in stderr_output

0 commit comments

Comments
 (0)