httpie/cli
github.com/httpie/cli ↗ · 262 files · 969 KB
HTTPie CLI: human-friendly HTTP client for the API era HTTPie (pronounced aitch-tee-tee-pie) is a command-line HTTP client.
Overview
HTTPie CLI is a Python-implemented, human-friendly command-line HTTP client that ships three console entry points -- `http`, `https`, and the `httpie` management interface -- all wired through `httpie.core.raw_main` (setup.cfg:70-74, httpie/core.py:32). Its distinguishing trait is a request-item mini-language (`Name:Value`, `name==value`, `field=value`, `field:=json`, `field@file`) that argparse parses into headers/params/data/files before delegating the actual transport to `requests` with HTTPie-specific adapters layered on top (httpie/cli/constants.py:15-27, httpie/client.py:43). Around that core sit a colorized/prettified output pipeline built on Pygments and Rich, JSON-file-backed sessions and config, a pip-driven plugin installer, and a detached background updater daemon. It targets API developers and shell scripters on Python 3.7-3.12 across Linux, macOS, and Windows, and is packaged for PyPI, Homebrew, Snap, Chocolatey, Fedora, and as a standalone PyInstaller binary.
System map
Three console_scripts entry points converge on a single error-handling shell: `http`/`https` map to `httpie.__main__:main` and `httpie` maps to `httpie.manager.__main__:main`, both of which call `httpie.core.raw_main` with a different parser and main_program. The package splits into `cli/` (argparse spec, request-item parsing, nested-JSON), `client.py` (requests glue), `output/` (streams, formatters, Rich UI), `sessions.py`/`config.py` (JSON persistence under the config dir), `plugins/` (entry-point registry), `manager/` (the `httpie` admin subcommands), and `internal/` (background update daemon). There is no database -- all state is JSON files under `$HTTPIE_CONFIG_DIR` or the XDG config dir. External integrations are the `requests`/`urllib3` stack, Pygments, Rich, defusedxml, requests-toolbelt, and `pip` shelled out for plugin installation. Build/test is Makefile-driven (`make install`, `make test`, `make codestyle`) with GitHub Actions running a 3-OS x 6-Python x pyopenssl matrix.
- setup.cfg declares three console_scripts: `http` and `https` both point at `httpie.__main__:main`, while `httpie` points at `httpie.manager.__main__:main` -- the `https` alias is what makes `_process_url` default to the https:// scheme.
- `httpie/__main__.py:main()` is a thin wrapper that imports `httpie.core.main` lazily, catches KeyboardInterrupt into `ExitStatus.ERROR_CTRL_C`, and returns `exit_status.value` -- keeping import cost off the Ctrl-C path.
- `raw_main()` is the shared shell for both binaries: it sets `env.program_name`, decodes bytes argv via `decode_raw_args`, short-circuits into `run_daemon_task` when `--daemon` is present, loads installed plugins from `env.config.plugins_dir`, and prepends `config.default_options` to argv.
- The CLI surface is declared declaratively as a `ParserSpec` of named `Group`s in httpie/cli/definition.py (Positional arguments, Predefined content types, Content processing options, Output processing, Output options, Sessions, Authentication, Network, SSL, Troubleshooting) and only then converted to argparse by `to_argparse()`.
- `httpie/client.py:collect_messages()` is the transport boundary: it builds a `requests.Session` via `build_requests_session`, mounts `HTTPieHTTPAdapter` on http:// and `HTTPieHTTPSAdapter` on https://, plus any `TransportPlugin` prefixes, then yields PreparedRequest/Response pairs as a generator so output can stream.
- Storage is JSON-file based via `BaseConfigDict`, which subclasses `dict`, writes with `json.dumps(indent=4, sort_keys=True, ensure_ascii=True)` and creates the parent dir with mode 0o700; `Config` (config.json) and `Session` (sessions/<host>/<name>.json) both derive from it.
- Config-dir resolution is a four-step fallback: `$HTTPIE_CONFIG_DIR`, then `%APPDATA%/httpie` on Windows, then a legacy `~/.httpie` if it exists, then `$XDG_CONFIG_HOME/httpie` (default `~/.config/httpie`).
- The `httpie` manager binary owns a nested COMMANDS dict producing `httpie cli export-args`, `httpie cli check-updates`, `httpie cli sessions upgrade|upgrade-all`, and `httpie plugins install|upgrade|uninstall|list`; `generate_subparsers()` recursively materializes them into argparse subparsers.
- `Environment` is the injected runtime boundary for all I/O -- stdin/stdout/stderr, isatty flags, encodings, color depth (probed via `curses.tigetnum('colors')`), Windows colorama stream wrapping, and lazily constructed Rich consoles -- which is precisely what the test suite overrides with `MockEnvironment`.
- Build and test commands live in the Makefile: `make install` creates a venv and pip-installs `.[dev] .[test]` editable, `make test` runs pytest, `make test-cover` adds `--cov=httpie --cov=tests`, `make codestyle` runs flake8 over httpie/ tests/ extras/profiling/ docs/packaging/brew/, and `make build` swaps `__build_channel__.py` to `BUILD_CHANNEL = "pip"` before building the wheel.
- pytest configuration is split across two files: setup.cfg sets `testpaths = httpie tests` with `--doctest-modules` (so module docstrings such as `ensure_path_as_is` and `humanize_bytes` are executable tests), while pytest.ini registers the `requires_installation` and `requires_external_processes` markers.
- Deployment is many-target and workflow_dispatch-driven: release-pypi.yml publishes via `pypa/gh-action-pypi-publish`, release-linux-standalone.yml runs `extras/packaging/linux/get_release_artifacts.sh` to produce a PyInstaller `http` binary plus .deb/.rpm, and .packit.yaml proposes the Fedora rawhide downstream update on release.
Feature catalog
The user-facing surface is one request-sending command with roughly fifty flags grouped into ten sections, plus a separate management binary with two command trees. Request construction supports JSON (default), form, multipart, raw body, nested-JSON keys, file embedding, and stdin piping. Output is controlled by a five-letter `--print` alphabet (H/B/h/b/m) with prettification, syntax highlighting, and streaming variants. Persistence features are named and anonymous sessions bound per host. Operational features include resumable downloads with Rich progress bars, offline request rendering, SSL/cipher/client-cert control, proxying, and a background update checker. Extensibility features are the four plugin categories plus a pip-backed plugin installer.
- Request-item syntax is the headline feature: `:` headers, `;` empty header, `:@` header-from-file, `==` query params, `==@` query-from-file, `=` data string, `:=` raw JSON, `@` file upload, `=@` embed file contents, `:=@` embed raw JSON file -- each mapped to a dedicated processor function in `RequestItems.from_args`.
- Nested-JSON keys (`foo[bar][0]=baz`) are a self-contained, dependency-free sub-language with its own tokenizer, recursive-descent `parse()` returning `Path` objects, and `interpret_nested_json()` builder; syntax errors surface as `NestedJSONSyntaxError` handled specially in `raw_main`.
- Content-type modes are mutually exclusive via a `RequestType` enum: `--json/-j`, `--form/-f`, `--multipart` (with `--boundary`), and `--raw` for a literal body; `_process_request_type` expands the enum into the `args.json`/`args.form`/`args.multipart` booleans the rest of the code reads.
- Output selection uses a compact alphabet -- `H` request headers, `B` request body, `h` response headers, `b` response body, `m` response metadata -- exposed via `--print`, with `--verbose` (=HBhb), `-vv` (all, incl. meta), `--headers`, `--body`, `--meta`, `--all`, and `--history-print` as shortcuts.
- `--meta`/`-vv` prints an `Elapsed time` line computed as `response.elapsed` plus `monotonic() - response._httpie_headers_parsed_at`, i.e. header time plus body-read time, and is highlighted by a dedicated `MetadataLexer`.
- Sessions come in two flavours: named sessions stored at `<config>/sessions/<host_port>/<name>.json`, and anonymous sessions triggered when the `--session` value contains a path separator, stored at the user-supplied path; `--session-read-only` loads without persisting back.
- Download mode (`--download/-d`) implies `--follow`, moves normal output to stderr, derives a filename from Content-Disposition or the URL path (with mimetypes extension guessing and uniquifying `-N` suffixes), and supports `--continue` via a `Range: bytes=N-` header validated against the Content-Range response.
- Download progress is rendered through Rich with three interchangeable displays -- `ProgressDisplay` when Content-Length is known, `StatusDisplay` (spinner) when it isn't, and `DummyDisplay` when `env.show_displays` is false (as in tests).
- `--offline` renders the request without sending it: `collect_messages` yields the prepared request and never enters the send branch, and the parser flips default output options to `OUTPUT_OPTIONS_DEFAULT_OFFLINE` (request head + body).
- Authentication ships three builtin plugins -- `basic` (unicode-safe latin1 header re-encoding), `digest`, and `bearer` (which sets `auth_parse = False` so the raw `-a` value becomes the token) -- and `--auth-type` choices are populated from the plugin registry.
- SSL control includes `--verify` (yes/no/CA-bundle path), `--ssl` mapped through `AVAILABLE_SSL_VERSION_ARG_MAPPING` (ssl2.3 through tls1.3, filtered by what the local `ssl` module exposes), `--ciphers`, and client certs via `--cert`/`--cert-key`/`--cert-key-pass`.
- `httpie plugins install/upgrade/uninstall/list` drives `pip install --prefer-binary --prefix=<plugins_dir> --no-warn-script-location` and implements uninstall by hand (unlinking `distribution.files`) because pip cannot uninstall from a custom prefix.
Data flow
Input arrives from four boundaries -- argv, stdin, files referenced by request items, and the JSON config/session files -- and is normalized inside `HTTPieArgumentParser.parse_args`, which mutates a single `argparse.Namespace` through an ordered chain of `_process_*` methods. `RequestItems.from_args` fans the raw `KeyValueArg` list into four typed multi-dicts (headers, data, params, files) plus a multipart ordering dict. `client.collect_messages` then translates the namespace into `requests.Request` kwargs, applies session headers/cookies/auth, prepares the request, optionally deflates it, and yields message objects lazily. Output flows back through `OutputOptions.from_message` into `write_message`, which picks a stream class (Raw/Encoded/Pretty/BufferedPretty) and writes bytes to `env.stdout.buffer`. On the way out, session cookies and headers are re-materialized and written back to JSON.
- The parse pipeline is strictly ordered and order-dependent: `_apply_no_options` -> `_process_request_type` -> `_process_download_options` -> `_setup_standard_streams` -> `_process_output_options` -> `_process_pretty_options` -> `_process_format_options` -> `_guess_method` -> `_parse_items` -> `_process_url` -> `_process_auth` -> `_process_ssl_cert`, followed by stdin/`--raw` body ingestion.
- `config.default_options` is prepended to argv (not merged post-parse), so user defaults behave exactly like typed flags and can be individually reverted with the generic `--no-OPTION` mechanism implemented in `_apply_no_options`.
- Method inference is data-driven: with no METHOD, POST is chosen if there is stdin/`--raw` input else GET; when the first positional is not alphabetic, the parser re-interprets it as a request item and shifts URL/method, then chooses POST if any item separator is in `SEPARATOR_GROUP_DATA_ITEMS`.
- In JSON mode, nested-JSON items are split out first via `split_iterable` and interpreted as a batch (so `[]` append semantics work across items), while all remaining items are processed one-by-one into their target dicts.
- Headers use `HTTPHeadersDict`, a case-insensitive multidict where `add(key, None)` means 'explicitly unset'; `finalize_headers` later converts `None` values for urllib3's SKIPPABLE_HEADERS into `SKIP_HEADER` and strips/encodes all other values.
- Because `requests`' PreparedRequest collapses duplicate headers, `apply_missing_repeated_headers` re-injects the original repeated values after preparation, skipping any field that was overridden along the way.
- Default headers are computed from the request shape: User-Agent is always `HTTPie/<version>`, JSON mode adds `Accept: application/json, */*;q=0.5` and `Content-Type: application/json`, and form mode without files adds `application/x-www-form-urlencoded; charset=utf-8`.
- Session headers are merged beneath CLI headers (`headers.update(base_headers)` then `headers.update(args.headers)`), so a header typed on the command line always wins over the stored session value.
- Request bodies are normalized by `prepare_request_body`: str/bytes become bytes, `RequestDataDict` becomes urlencoded bytes, file-likes go through `_prepare_file_for_upload`, and `--chunked` wraps everything in a `ChunkedUploadStream`/`ChunkedMultipartUploadStream` generator.
- Upload progress is echoed back to stdout through a callback loop: `program()` passes `request_body_read_callback` into `collect_messages`, which threads it into `prepare_request_body`, which wraps `file.read` so each chunk is re-emitted via `write_raw_data`.
- Redirects are handled manually rather than by requests: `allow_redirects` is hard-set to False in `make_send_kwargs`, and `collect_messages` walks `response.next` itself so that each hop can be yielded for `--all`/`--history-print` and counted against `--max-redirects`.
- Response bytes are decoded adaptively: `EncodedStream` uses the declared charset if present, otherwise accumulates `charset_normalizer` guesses and locks one in after `ENCODING_GUESS_THRESHOLD` (3) consecutive agreeing guesses; bodies containing NUL bytes raise `BinarySuppressedError` and print the 'binary data not shown' notice instead.
Behavior traces
Three flows carry most of the behaviour. Startup walks __main__ -> core.raw_main -> plugin loading -> parser.parse_args -> check_updates -> core.program. The primary request flow walks program -> collect_messages -> requests.Session.send -> OutputOptions.from_message -> write_message -> stream class -> stdout buffer, message by message, as a generator so headers can print before the body arrives. Persistence walks get_httpie_session -> Session.load -> pre_process_data (with legacy fixups) -> update_headers -> save -> post_process_data. A fourth notable flow is the update daemon, which double-forks out of the main process to fetch a version index without blocking the user.
- Startup, step by step: `__main__.main()` imports `core.main`, which imports `httpie.cli.definition.parser` and calls `raw_main(parser, program, argv, Environment())`; `raw_main` sets `env.program_name`, calls `decode_raw_args`, checks `is_daemon_mode`, calls `plugin_manager.load_installed_plugins`, prepends default options, then `parser.parse_args(args, env)`.
- `--debug` is handled before parsing: `print_debug_info` writes HTTPie/Requests/Pygments/Python/platform versions plus `repr(env)` and `repr(plugin_manager)` to stderr, and a bare `http --debug` returns SUCCESS without ever building a request.
- After a successful parse, `check_updates(env)` runs before `main_program`; the `_update_checker` decorator wraps it so that every exception is swallowed by `suppress(BaseException)` unless `developer_mode` is set in config.json.
- Primary request trace: `program()` builds `ProcessingOptions.from_raw_args`, optionally constructs a `Downloader` and calls `downloader.pre_request(args.headers)`, then iterates `collect_messages(env, args, request_body_read_callback)`, calling `OutputOptions.from_message` and `write_message` per message.
- Inside `collect_messages` the order is: resolve session -> `make_request_kwargs` -> `make_send_kwargs` -> `build_requests_session` -> apply session headers/cookies/auth -> `requests.Request(**kwargs)` -> `session.prepare_request` -> `transform_headers` -> `ensure_path_as_is` (if `--path-as-is`) -> `compress_request` (if `--compress`) -> yield/send loop.
- `max_headers` is a context manager that temporarily monkey-patches `http.client._MAXHEADERS` around the single `session.send()` call so `--max-headers` can exceed the stdlib's default cap, restoring it in a finally block.
- Write trace: `write_message` -> `build_output_stream_for_message` -> `get_stream_type_and_kwargs` picks RawStream (non-tty, no prettify), EncodedStream (tty, no prettify), PrettyStream (`--stream` or `text/event-stream` auto-detect) or BufferedPrettyStream, then `write_stream` pushes each chunk to `outfile.buffer`, flushing when tty or streaming.
- `BaseStream.__iter__` defines the on-wire ordering for one message: headers, then `\r\n\r\n`, then body chunks (each fed to `on_body_chunk_downloaded`), then -- if `--meta` -- a blank line and the metadata block.
- Message separation is stateful: `program()` tracks `prev_with_body` and `force_separator`, emitting `MESSAGE_SEPARATOR_BYTES` between messages when the previous one had a body and either output is redirected or a streamed upload forced it.
- Download trace: after the response loop, `downloader.start(initial_url, final_response)` builds a `RawStream` over the response with `chunk_downloaded` as the progress callback, `write_stream` drains it to the output file, `downloader.finish()` stops the display, and a size mismatch sets `downloader.interrupted` and an error exit status.
- Session persistence trace: `get_httpie_session` resolves the bound hostname (falling back to `localhost` for hostname-less URLs), builds the path, and calls `Session.load()`, which runs `read_raw_config` then `pre_process_data` -- importing cookies into a `RequestsCookieJar` with `HTTPieCookiePolicy` and headers into an `HTTPHeadersDict`.
- On the way out, the session is saved only when it is new or `--session-read-only` was not used: cookies are replaced with the requests session jar, expired cookies removed, then `save()` writes JSON and stamps `__meta__.httpie` with the current version.
Testing signals
Testing is pytest-based with pytest-httpbin providing a real local HTTP/HTTPS server, plus `responses`, `pytest-mock` and a hand-rolled `http_server` fixture for cases httpbin cannot express. Roughly 6,600 lines across ~35 test modules under tests/, exercising the CLI end-to-end through a `MockEnvironment` that swaps stdin/stdout for temp files and disables colors and progress displays. `--doctest-modules` means docstring examples inside httpie/ are also collected. Coverage is strongest on sessions, output, JSON, uploads, and CLI parsing; weakest on the daemon-spawning internals, Rich help/palette rendering, and the manager plugin installer (which is gated behind a `requires_installation` marker). CI runs the suite across 3 OSes x 6 Python versions x pyopenssl on/off, with a separate coverage job and a flake8 style job.
- The suite runs real HTTP against pytest-httpbin: conftest.py auto-uses an `httpbin_add_ca_bundle` fixture that monkeypatches `REQUESTS_CA_BUNDLE` to pytest-httpbin's cert, and adds an `httpbin_secure_untrusted` variant that deliberately removes it for negative TLS tests.
- `tests/utils/__init__.py` defines the `MockEnvironment` harness: colors=0, stdout/stdin as temp files, `show_displays=False`, plus an `Encoder`/`FakeBytesIOBuffer` pair that smuggles undecodable binary output through a text stream as `hash(N)` placeholders.
- Tests globally disable the updater by setting `Config.DEFAULTS['disable_update_warnings'] = True` at import time, explicitly to stop hundreds of subprocesses hitting the network during a run.
- Two pytest markers gate the expensive tests: `requires_installation` (all of tests/test_plugins_cli.py and tests/test_httpie_cli.py, which shell out to a real install) and `requires_external_processes` (parts of test_uploads.py and test_update_warnings.py), with pytest.ini documenting how to deselect them.
- tests/utils/http_server.py provides a decorator-registry `TestHandler` over `BaseHTTPRequestHandler` with `handlers[method][path]`, used for behaviours httpbin cannot produce -- notably cookie-on-redirect semantics.
- The largest test modules by line count are test_sessions.py (852), test_output.py (631), test_json.py (583), test_uploads.py (403), test_cli.py (377) and test_httpie.py (372) -- i.e. persistence, rendering, and the request-item language get the most attention.
- Plugin extension points are covered by executable examples rather than mocks: test_transport_plugin.py registers a `FakeTransportPlugin` with prefix `http+fake`, asserts the fake adapter's response renders correctly, and unregisters it in a finally block.
- The ParserSpec serialization contract has a golden-output test asserting the exact JSON shape (options, is_optional, is_variadic, metavar, choices, python_type_name), which is what protects `httpie cli export-args` consumers.
- A dedicated `tests/utils/matching` package (parsing.py, tokens.py, test_matching.py) exists to assert on tokenized terminal output, letting colorized output be checked structurally instead of by brittle ANSI string comparison.
- `--doctest-modules` in setup.cfg means library docstrings are part of the suite -- e.g. `ensure_path_as_is`, `humanize_bytes`, `KeyValueArgType.tokenize`, and `detect_encoding` all carry assertions that fail the build if behaviour drifts.
- tests.yml runs the full matrix of {ubuntu-latest, macos-13, windows-latest} x Python {3.7..3.12} x pyopenssl {0,1}, with `fail-fast: false`; Windows runs pytest directly while Linux/macOS go through `make install && make test`.
- The pyopenssl matrix dimension is wired through `HTTPIE_TEST_WITH_PYOPENSSL`, which a session-scoped autouse fixture reads to call `urllib3.contrib.pyopenssl.inject_into_urllib3()` and hard-fails if the module is missing.
Risk map
The sharpest edges are in code that leaves the process or touches the filesystem outside the repo: the update daemon double-forks and re-executes the CLI, the plugin installer shells out to pip and then unlinks files by hand, and man-page display execs `man`. TLS handling has two deliberate-but-loud decisions -- a global `urllib3.disable_warnings()` and a `verify=False` fetch of the version index. Filesystem risk concentrates in download filename derivation and lock-file handling in the system temp dir. Structurally, `core.program()` and `_process_auth()` are both flagged in-repo as needing refactoring, and the argparse Namespace is a mutable god-object threaded through most modules, so changes to parse ordering have wide blast radius.
- `_fetch_updates` performs `requests.get(PACKAGE_INDEX_LINK, verify=False)` against https://packages.httpie.io/latest.json -- an unauthenticated, certificate-unverified network call whose result drives the version-comparison warning shown to users.
- `client.py` calls `urllib3.disable_warnings()` unconditionally at import, which silences InsecureRequestWarning process-wide -- including for `--verify=no` -- so a user disabling verification gets no library-level warning.
- Daemon mode is decided by a raw substring check (`'--daemon' in args`) before argparse ever runs, so the flag is reachable from ordinary argv; the blast radius is limited only by the `assert options.task_id in DAEMONIZED_TASKS` in `run_daemon_task`, and asserts vanish under `python -O`.
- `_spawn_posix` double-forks, calls `os.setsid()`, closes stdin/stdout/stderr, and then re-enters `httpie.core.main(['http'] + args)` in-process with `suppress(BaseException)` -- a path that is effectively untestable in-process and whose failures are silent by construction.
- `spawn_daemon` copies the full parent environment into the child process context and injects a computed `PYTHONPATH` derived from `inspect.stack()[0][1]`, so anything in the caller's env (including secrets) is inherited by the detached process.
- `PluginInstaller._uninstall` deletes files directly with `os.unlink(distribution.locate_file(file))` for every entry in `distribution.files`; the only guard is a check that `self.dir` is in `base_dir.parents`, and the code's own TODO notes failures are not rolled back.
- `_clear_metadata` calls `shutil.rmtree(meta_path)` on `.dist-info`/`.egg-info` directories whose version differs from the freshly installed one, driven by parsing the last line of pip's stdout (`Successfully installed ...`) -- a string-parsing dependency on pip's output format.
- Plugin loading executes arbitrary third-party code at startup: `load_installed_plugins` calls `entry_point.load()` for every matching entry point, catching only `BaseException` into a warning -- so a malicious or broken plugin on the plugins path runs on every `http` invocation.
- Download filenames are attacker-influenced: `filename_from_content_disposition` takes the server-supplied header and sanitizes it only with `os.path.basename(...).lstrip('.').strip()`, after which `get_unique_filename` opens the result for append in the current working directory.
- `open_with_lockfile` derives a lock path as base64 of the target path inside the shared system temp dir and relies on `touch(exist_ok=False)` for mutual exclusion; the lock is only removed in the finally block, so a hard kill leaves a stale lock that permanently raises `LockFileError` for that file.
- Credentials flow into session files: when `--auth` is given alongside `--session`, `collect_messages` writes `{'type': auth_type, 'raw_auth': plugin.raw_auth}` into the session JSON, meaning plaintext `user:password` is persisted to disk under the config directory.
- `_process_auth` also silently harvests credentials from two implicit sources -- a `user:pass@host` URL and the user's netrc via `get_netrc_auth` (for plugins with `netrc_parse = True`, i.e. basic and digest) -- unless `--ignore-netrc` forces an `ExplicitNullAuth`.
Extension points
Extension is designed around four setuptools entry-point groups consumed by a single `PluginManager`, which is itself just a `list` subclass with typed filters. Built-in behaviour is registered through the same mechanism, so formatters and auth methods are not special-cased. Beyond plugins, the CLI itself is data: `ParserSpec`/`Group`/`Argument` describe the interface declaratively, the manager's COMMANDS dict describes subcommands, and `CLI_TASKS` and `DAEMONIZED_TASKS` are plain dict dispatch tables. Adding output behaviour means either a FormatterPlugin or a new `BaseStream` subclass selected in `get_stream_type_and_kwargs`.
- The four plugin entry-point groups are `httpie.plugins.auth.v1`, `httpie.plugins.converter.v1`, `httpie.plugins.formatter.v1` and `httpie.plugins.transport.v1`, mapped to their base classes in `ENTRY_POINT_CLASSES`; adding a fifth category means editing that dict.
- `plugin_manager` is a module-level singleton in httpie/plugins/registry.py where all builtins are registered (BasicAuthPlugin, DigestAuthPlugin, BearerAuthPlugin, HeadersFormatter, JSONFormatter, XMLFormatter, ColorFormatter) -- the canonical place to add a new builtin.
- `PluginManager` exposes `register`/`unregister`/`filter` plus typed accessors (`get_auth_plugins`, `get_auth_plugin_mapping`, `get_formatters_grouped`, `get_converters`, `get_transport_plugins`), making runtime registration in tests a two-line operation.
- A new `AuthPlugin` needs only `auth_type` plus `get_auth()`, and can tune behaviour through the class-level switches `auth_require`, `auth_parse`, `netrc_parse`, and `prompt_password` -- all of which `_process_auth` consults.
- `TransportPlugin` extension is prefix-based: `build_requests_session` mounts `plugin.get_adapter()` at `plugin.prefix`, which is how httpie-unixsocket-style schemes (e.g. `http+unix://`) plug in.
- `FormatterPlugin` subclasses declare a `group_name` ('format' by default, 'colors' for ColorFormatter) and are collected by `get_formatters_grouped`; `Formatting.__init__` then instantiates only the groups named by `--pretty` and keeps those whose `self.enabled` is true.
- `ConverterPlugin` is the hook for binary payloads: `Conversion.get_converter(mime)` asks each registered converter's `supports(mime)` classmethod, and PrettyStream/BufferedPrettyStream call it when they hit a NUL byte, turning what would be a 'binary suppressed' notice into rendered text.
- New CLI flags are added declaratively to a `Group` in httpie/cli/definition.py (with `short_help`, `help`, and optional `nested_options`), never by touching argparse directly -- `to_argparse` strips the HTTPie-only keys listed in `ARGPARSE_IGNORE_KEYS` before delegating.
- Dynamically computed choice lists use the custom `lazy_choices` argparse action registered in `to_argparse`, so expensive enumerations (available Pygments styles, auth types, SSL versions) are only materialized when help or export-args is requested.
- New manager subcommands plug into two places: the nested `COMMANDS` dict in httpie/manager/cli.py (which `generate_subparsers` walks recursively) and the `CLI_TASKS` dispatch dict in httpie/manager/tasks/__init__.py keyed by action name.
- Background work plugs into `DAEMONIZED_TASKS`, a name->callable dict in daemon_runner.py; `_check_status` is an example entry that exists purely so tests can assert the spawn path works without touching the network.
- New session-format migrations plug into `FIXERS_TO_VERSIONS`, a version-string -> `fix_layout` callable mapping consulted by `upgrade_session` via `is_version_greater`.
Contributor handoff
Read httpie/core.py, httpie/cli/argparser.py, httpie/client.py and httpie/cli/definition.py before touching anything -- those four hold the control flow, the mutation order, the transport glue, and the CLI contract. The project is conservative: contributions without tests are explicitly rejected, behaviour changes require documentation, and CHANGELOG.md is maintained by hand. Generated artefacts (extras/man/*.1, docs/installation/README.md) must never be hand-edited. The safest edit boundaries are adding a new plugin, adding a declarative flag to a group in definition.py, or adding a formatter; the riskiest are reordering `_process_*` calls, changing the `collect_messages` generator contract, and anything in httpie/internal/.
- Read first, in this order: httpie/core.py (both entry shells), httpie/cli/argparser.py (the ordered mutation chain), httpie/client.py (requests translation), httpie/cli/definition.py (the flag contract), and httpie/context.py (the I/O boundary every test overrides).
- CONTRIBUTING.md is explicit that PRs with code changes but no tests will generally not be merged, and that behaviour changes need documentation -- treat a failing or absent test as a blocker, not a nice-to-have.
- Local workflow is `make install` (creates ./venv and pip-installs the dev+test extras editable), then `make test`; for a single module use the venv's pytest directly, and `make codestyle` before pushing since a separate CI job enforces flake8.
- Never hand-edit extras/man/*.1 or docs/installation/README.md -- both are generated by `make content` (via extras/scripts/generate_man_pages.py and docs/installation/generate.py) and auto-PR'd by the content.yml workflow.
- Man-page generation sets `HTTPIE_BUILDING_MAN_PAGES=1` before importing httpie, which flips `IS_MAN_PAGE` in definition.py so machine-specific defaults (e.g. the computed `--ciphers` list) are excluded -- any new environment-dependent default must respect that flag.
- Add CLI flags only through `group.add_argument(...)` in httpie/cli/definition.py using `Qualifiers.*` instead of raw argparse constants; `to_argparse` translates them and `serialize()` exports them, so a raw argparse string will break `httpie cli export-args`.
- Every `--OPTION` automatically gets a `--no-OPTION` counterpart handled by `_apply_no_options`, which resets `args.<dest>` to the action's default -- so new flags must have a meaningful `default`, not rely on absence.
- Trap: the order of `_process_*` calls in `parse_args` is load-bearing. `_setup_standard_streams` must run before `_process_output_options` (which reads `env.stdout_isatty`), and `_parse_items` must run before `_process_url`/`_process_auth`. Reordering silently changes defaults.
- Trap: `collect_messages` is a generator whose laziness is the streaming contract -- the session save at the end only runs if the caller exhausts it, so wrapping it in anything that short-circuits will silently stop sessions from persisting.
- Trap: do not print with `print()` or touch `sys.stdout` directly. All output must go through `env.stdout`/`env.stderr` or the Rich consoles, because `--quiet`, `--output`, `--download` and the whole test harness swap those streams.
- Trap: `--doctest-modules` is on, so any docstring in httpie/ containing a `>>>` prompt is a test. Editing `humanize_bytes`, `ensure_path_as_is`, `detect_encoding` or `KeyValueArgType.tokenize` means updating their examples too.
- Trap: tests hit the network by design. `pie.dev` is used for chunked-upload and remote tests via the `httpbin_with_chunked_support` and `remote_httpbin` fixtures, which skip (not fail) when DNS does not resolve -- a green local run may simply have skipped them.
Languages
| Language | Files | Share of source |
|---|---|---|
| Python | 133 | 96% |
| JSON | 26 | docs/config |
| Markdown | 24 | docs/config |
| YAML | 19 | docs/config |
| Shell | 4 | 3% |
| Ruby | 2 | 1% |
Start reading here
Probable source entrypoint: setup.cfg — inferred from the directory tree and manifest, not the upstream docs.
Source roots
httpie/ (78 files)
Source layout — httpie/
| Path | Files |
|---|---|
httpie/cli/ | 15 |
httpie/internal/ | 5 |
httpie/legacy/ | 3 |
httpie/manager/ | 10 |
httpie/output/ | 23 |
httpie/plugins/ | 5 |
httpie/__init__.py | 1 |
httpie/__main__.py | 1 |
httpie/adapters.py | 1 |
httpie/client.py | 1 |
httpie/compat.py | 1 |
httpie/config.py | 1 |
httpie/context.py | 1 |
httpie/cookies.py | 1 |
Repository structure
| Top-level path | Files |
|---|---|
tests/ | 102 |
httpie/ | 78 |
docs/ | 31 |
.github/ | 19 |
extras/ | 16 |
Docs found: CONTRIBUTING.mdREADME.mddocs/README.mddocs/contributors/README.mddocs/installation/README.mddocs/packaging/README.mddocs/packaging/brew/README.mddocs/packaging/linux-arch/README.mddocs/packaging/linux-centos/README.mddocs/packaging/linux-debian/README.md
CI & config entrypoints
README.md— Repository readme.github/workflows/docs-deploy.yml— GitHub Actions workflow.github/workflows/coverage.yml— GitHub Actions workflow.github/workflows/test-package-mac-brew.yml— GitHub Actions workflow.github/workflows/release-choco.yml— GitHub Actions workflow.github/workflows/release-linux-standalone.yml— GitHub Actions workflow.github/workflows/code-style.yml— GitHub Actions workflow.github/workflows/test-package-linux-snap.yml— GitHub Actions workflow.github/workflows/stale.yml— GitHub Actions workflow.github/workflows/benchmark.yml— GitHub Actions workflow
Structural health signals
Average score 8.1/10 across 83 analyzed source files; 7 hotspots surfaced.
httpie/cli/definition.py— 956 lines, score 5.7 (Large file, Deep nesting)httpie/client.py— 400 lines, score 5.9 (Deep nesting, High churn)docs/contributors/fetch.py— 281 lines, score 5.9 (Deep nesting, High churn)
These are deterministic review leads — size, churn, and structural signals — not proof of a bug in this project.
Recent history
5b604c37c6c62024-12-17 — Fix `https` behaviour in fish (#1611)2843b874c60b2024-11-01 — [automated] Update generated content (#1607)2105caa49bae2024-11-01 — 3.2.48560d1196d782024-11-01 — Update test.yml2ef4a57d8c2a2024-11-01 — Fix/refactor default cert loading