Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions httpie/cli/definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,31 @@

""",
)
processing_options.add_argument(
'--sequence',
action='store_true',
default=False,
short_help='Execute multiple requests from STDIN sequentially.',
help="""
Read and execute multiple HTTPie request definitions from STDIN,
one request per line, sequentially.

Each line should contain a complete HTTPie command without the
leading "http" program name, e.g.:

GET https://httpbin.org/get
POST https://httpbin.org/post name=alice
GET https://httpbin.org/headers

Example usage:

$ cat requests.http | http --sequence
$ echo -e "GET httpbin.org/get\nPOST httpbin.org/post foo=bar" | http --sequence

Requests are executed strictly in order, one at a time.

""",
)


#######################################################################
Expand Down
221 changes: 156 additions & 65 deletions httpie/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@
from .client import collect_messages
from .context import Environment, LogLevel
from .downloads import Downloader
from .models import (
RequestsMessageKind,
OutputOptions
)
from .models import RequestsMessageKind, OutputOptions
from .output.models import ProcessingOptions
from .output.writer import write_message, write_stream, write_raw_data, MESSAGE_SEPARATOR_BYTES
from .output.writer import (
write_message,
write_stream,
write_raw_data,
MESSAGE_SEPARATOR_BYTES,
)
from .plugins.registry import plugin_manager
from .status import ExitStatus, http_status_to_exit_status
from .utils import unwrap_context
Expand Down Expand Up @@ -48,27 +50,27 @@ def raw_main(
if use_default_options and env.config.default_options:
args = env.config.default_options + args

include_debug_info = '--debug' in args
include_traceback = include_debug_info or '--traceback' in args
include_debug_info = "--debug" in args
include_traceback = include_debug_info or "--traceback" in args

def handle_generic_error(e, annotation=None):
msg = str(e)
if hasattr(e, 'request'):
if hasattr(e, "request"):
request = e.request
if hasattr(request, 'url'):
if hasattr(request, "url"):
msg = (
f'{msg} while doing a {request.method}'
f' request to URL: {request.url}'
f"{msg} while doing a {request.method}"
f" request to URL: {request.url}"
)
if annotation:
msg += annotation
env.log_error(f'{type(e).__name__}: {msg}')
env.log_error(f"{type(e).__name__}: {msg}")
if include_traceback:
raise

if include_debug_info:
print_debug_info(env)
if args == ['--debug']:
if args == ["--debug"]:
return ExitStatus.SUCCESS

exit_status = ExitStatus.SUCCESS
Expand All @@ -84,13 +86,13 @@ def handle_generic_error(e, annotation=None):
raise
exit_status = ExitStatus.ERROR
except KeyboardInterrupt:
env.stderr.write('\n')
env.stderr.write("\n")
if include_traceback:
raise
exit_status = ExitStatus.ERROR_CTRL_C
except SystemExit as e:
if e.code != ExitStatus.SUCCESS:
env.stderr.write('\n')
env.stderr.write("\n")
if include_traceback:
raise
exit_status = ExitStatus.ERROR
Expand All @@ -102,33 +104,32 @@ def handle_generic_error(e, annotation=None):
env=env,
)
except KeyboardInterrupt:
env.stderr.write('\n')
env.stderr.write("\n")
if include_traceback:
raise
exit_status = ExitStatus.ERROR_CTRL_C
except SystemExit as e:
if e.code != ExitStatus.SUCCESS:
env.stderr.write('\n')
env.stderr.write("\n")
if include_traceback:
raise
exit_status = ExitStatus.ERROR
except requests.Timeout:
exit_status = ExitStatus.ERROR_TIMEOUT
env.log_error(f'Request timed out ({parsed_args.timeout}s).')
env.log_error(f"Request timed out ({parsed_args.timeout}s).")
except requests.TooManyRedirects:
exit_status = ExitStatus.ERROR_TOO_MANY_REDIRECTS
env.log_error(
f'Too many redirects'
f' (--max-redirects={parsed_args.max_redirects}).'
f"Too many redirects" f" (--max-redirects={parsed_args.max_redirects})."
)
except requests.exceptions.ConnectionError as exc:
annotation = None
original_exc = unwrap_context(exc)
if isinstance(original_exc, socket.gaierror):
if original_exc.errno == socket.EAI_AGAIN:
annotation = '\nCouldnt connect to a DNS server. Please check your connection and try again.'
annotation = "\nCouldn't connect to a DNS server. Please check your connection and try again."
elif original_exc.errno == socket.EAI_NONAME:
annotation = '\nCouldnt resolve the given hostname. Please check the URL and try again.'
annotation = "\nCouldn't resolve the given hostname. Please check the URL and try again."
propagated_exc = original_exc
else:
propagated_exc = exc
Expand All @@ -144,8 +145,7 @@ def handle_generic_error(e, annotation=None):


def main(
args: List[Union[str, bytes]] = sys.argv,
env: Environment = Environment()
args: List[Union[str, bytes]] = sys.argv, env: Environment = Environment()
) -> ExitStatus:
"""
The main function.
Expand All @@ -159,18 +159,102 @@ def main(

from .cli.definition import parser

return raw_main(
parser=parser,
main_program=program,
args=args,
env=env
)
return raw_main(parser=parser, main_program=program, args=args, env=env)


def run_sequence_mode(args: argparse.Namespace, env: Environment) -> ExitStatus:
"""
Execute multiple HTTPie request definitions from STDIN sequentially.

Each line in STDIN is treated as a separate HTTPie command without
the leading 'http' program name.

Example input:
GET https://httpbin.org/get
POST https://httpbin.org/post name=alice
GET https://httpbin.org/headers
"""
import shlex
from .cli.definition import parser

exit_status = ExitStatus.SUCCESS
request_lines = []

# Read all request definitions from STDIN
try:
for line in env.stdin:
line = line.strip()
if line and not line.startswith("#"):
request_lines.append(line)
except KeyboardInterrupt:
env.stderr.write("\n")
return ExitStatus.ERROR_CTRL_C

if not request_lines:
env.log_error("No request definitions found in STDIN.")
return ExitStatus.ERROR

# Execute each request sequentially
for i, request_line in enumerate(request_lines, 1):
if env.stdout_isatty:
env.stderr.write(f"\n[{i}/{len(request_lines)}] {request_line}\n")

try:
# Parse the request line as if it were CLI arguments
# Add --ignore-stdin to prevent argparser from using stdin for request body
request_args = shlex.split(request_line) + ["--ignore-stdin"]

# Create a fresh environment for this request to avoid stdin conflicts
request_env = Environment(
stdin=None, # No stdin for individual requests in sequence mode
stdout=env.stdout,
stderr=env.stderr,
)
request_env.program_name = env.program_name

# Parse the request
request_namespace = parser.parse_args(
args=request_args,
env=request_env,
)

# Execute the request (call _program to avoid sequence check loop)
request_exit_status = _program(request_namespace, request_env)

# Track the worst exit status
if request_exit_status != ExitStatus.SUCCESS:
exit_status = request_exit_status

except SystemExit as e:
# Handle SystemExit from argument parser errors
if e.code != ExitStatus.SUCCESS:
exit_status = (
ExitStatus.ERROR
if e.code is None or e.code != 0
else ExitStatus(e.code)
)
except Exception as e:
env.log_error(f"Error executing request [{i}]: {e}")
exit_status = ExitStatus.ERROR

return exit_status


def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
"""
The main program without error handling.

"""
# Handle --sequence mode: execute multiple requests from STDIN
if getattr(args, "sequence", False):
return run_sequence_mode(args, env)

return _program(args, env)


def _program(args: argparse.Namespace, env: Environment) -> ExitStatus:
"""
The actual program implementation without the --sequence check.
"""
# TODO: Refactor and drastically simplify, especially so that the separator logic is elsewhere.
exit_status = ExitStatus.SUCCESS
Expand All @@ -180,7 +264,7 @@ def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
processing_options = ProcessingOptions.from_raw_args(args)

def separate():
getattr(env.stdout, 'buffer', env.stdout).write(MESSAGE_SEPARATOR_BYTES)
getattr(env.stdout, "buffer", env.stdout).write(MESSAGE_SEPARATOR_BYTES)

def request_body_read_callback(chunk: bytes):
should_pipe_to_stdout = bool(
Expand All @@ -196,25 +280,32 @@ def request_body_read_callback(chunk: bytes):
env,
chunk,
processing_options=processing_options,
headers=initial_request.headers
headers=initial_request.headers,
)

try:
if args.download:
args.follow = True # --download implies --follow.
downloader = Downloader(env, output_file=args.output_file, resume=args.download_resume)
downloader = Downloader(
env, output_file=args.output_file, resume=args.download_resume
)
downloader.pre_request(args.headers)
messages = collect_messages(env, args=args,
request_body_read_callback=request_body_read_callback)
messages = collect_messages(
env, args=args, request_body_read_callback=request_body_read_callback
)
force_separator = False
prev_with_body = False

# Process messages as theyre generated
# Process messages as they're generated
for message in messages:
output_options = OutputOptions.from_message(message, args.output_options)

do_write_body = output_options.body
if prev_with_body and output_options.any() and (force_separator or not env.stdout_isatty):
if (
prev_with_body
and output_options.any()
and (force_separator or not env.stdout_isatty)
):
# Separate after a previous message with body, if needed. See test_tokens.py.
separate()
force_separator = False
Expand All @@ -228,16 +319,21 @@ def request_body_read_callback(chunk: bytes):
else:
final_response = message
if args.check_status or downloader:
exit_status = http_status_to_exit_status(http_status=message.status_code, follow=args.follow)
if exit_status != ExitStatus.SUCCESS and (not env.stdout_isatty or args.quiet == 1):
env.log_error(f'HTTP {message.raw.status} {message.raw.reason}', level=LogLevel.WARNING)
exit_status = http_status_to_exit_status(
http_status=message.status_code, follow=args.follow
)
if exit_status != ExitStatus.SUCCESS and (
not env.stdout_isatty or args.quiet == 1
):
env.log_error(
f"HTTP {message.raw.status} {message.raw.reason}",
level=LogLevel.WARNING,
)
write_message(
requests_message=message,
env=env,
output_options=output_options._replace(
body=do_write_body
),
processing_options=processing_options
output_options=output_options._replace(body=do_write_body),
processing_options=processing_options,
)
prev_with_body = output_options.body

Expand All @@ -255,8 +351,8 @@ def request_body_read_callback(chunk: bytes):
if downloader.interrupted:
exit_status = ExitStatus.ERROR
env.log_error(
f'Incomplete download: size={downloader.status.total_size};'
f' downloaded={downloader.status.downloaded}'
f"Incomplete download: size={downloader.status.total_size};"
f" downloaded={downloader.status.downloaded}"
)
return exit_status

Expand All @@ -268,31 +364,26 @@ def request_body_read_callback(chunk: bytes):


def print_debug_info(env: Environment):
env.stderr.writelines([
f'HTTPie {httpie_version}\n',
f'Requests {requests_version}\n',
f'Pygments {pygments_version}\n',
f'Python {sys.version}\n{sys.executable}\n',
f'{platform.system()} {platform.release()}',
])
env.stderr.write('\n\n')
env.stderr.writelines(
[
f"HTTPie {httpie_version}\n",
f"Requests {requests_version}\n",
f"Pygments {pygments_version}\n",
f"Python {sys.version}\n{sys.executable}\n",
f"{platform.system()} {platform.release()}",
]
)
env.stderr.write("\n\n")
env.stderr.write(repr(env))
env.stderr.write('\n\n')
env.stderr.write("\n\n")
env.stderr.write(repr(plugin_manager))
env.stderr.write('\n')
env.stderr.write("\n")


def decode_raw_args(
args: List[Union[str, bytes]],
stdin_encoding: str
) -> List[str]:
def decode_raw_args(args: List[Union[str, bytes]], stdin_encoding: str) -> List[str]:
"""
Convert all bytes args to str
by decoding them using stdin encoding.

"""
return [
arg.decode(stdin_encoding)
if type(arg) is bytes else arg
for arg in args
]
return [arg.decode(stdin_encoding) if type(arg) is bytes else arg for arg in args]
Loading
Loading