Skip to content
Merged
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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ metadata/update/DNS/TLS inspection modes, and executes requests via `src/http`.
- Rust `-vvv` output prints config, DNS, TCP, TLS/QUIC, and TTFB debug metadata through the central printer, including color policy and the blank response-header separator before formatted bodies.
- Rust `--timing` enables DNS pre-resolution timing and transport connection timing so the waterfall includes DNS, TCP, TLS/QUIC, TTFB, and Body phases. The direct transport owns DNS/TCP/TLS/QUIC setup, so keep new timing instrumentation in `src/http/transport.rs` and `src/net.rs`.
- `src/main.rs` runs the Tokio runtime on an explicitly larger stack thread, and the top-level app future in `src/app.rs` is heap-pinned before the shutdown-signal `tokio::select!`; do not move it back to `tokio::pin!` or the default process main stack because the combined async request/WebSocket/inspection state can overflow Windows' smaller main-thread stack even for metadata commands.
- Rust response body paging is controlled by `--pager auto|on|off` or `pager = ...`; `auto` routes terminal stdout through `$PAGER` when set, otherwise falls back to `less -FIRX`, `on` forces the pager, and `off` disables it. `NO_PAGER` disables the default `auto` pager, and `$LESS` is honored by not adding fallback `less` flags when it is set. Image responses and output-file writes bypass the pager.
- Rust response body paging is controlled by `--pager auto|on|off` or `pager = ...`; `auto` routes terminal stdout through `$PAGER` when set, otherwise falls back to `less -FIRX`, `on` forces the pager, and `off` disables it. `NO_PAGER` disables the default `auto` pager, and `$LESS` is honored by not adding fallback `less` flags when it is set. `$PAGER` is split with POSIX shell-style quoting but launched directly, so shell operators are not interpreted. Image responses and output-file writes bypass the pager.
- Timeout duration parsing, Go-style duration formatting, elapsed request budgets, connect/DNS timeout caps, and shared `request timed out after ...` errors live in `src/duration.rs`; HTTP, WebSocket, DNS inspection, and TLS inspection paths should reuse `TimeoutBudget` instead of recomputing remaining time locally, and response body deadlines should preserve the original request timeout diagnostic.
- HTTP retries keep the original `--timeout` operation deadline across attempts, cap retry sleeps to the remaining budget, refresh per-request timeouts before each send, and use best-effort byte/time-bounded drains for retry cleanup responses.
- Custom/pre-resolved DNS observes timeout budgets before the transport client is built: `--connect-timeout` bounds DNS resolution when set, otherwise DNS uses the remaining `--timeout` budget, and DoH lookup clients receive the same budget.
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ rustls-platform-verifier = "=0.7.0"
serde = { version = "=1.0.228", features = ["derive"] }
serde_json = { version = "=1.0.150", features = ["arbitrary_precision", "preserve_order"] }
sha2 = "=0.11.0"
shlex = "=2.0.1"
socket2 = { version = "=0.6.4", features = ["all"] }
tar = "=0.4.46"
thiserror = "=2.0.18"
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,12 @@ without mixing diagnostics into the pipe.
When stdout is a terminal, supported response bodies are formatted and may open
in the pager from `$PAGER`; set `NO_PAGER` or use `--pager off` to write
directly. If `$PAGER` is unset, fetch falls back to `less -FIRX` and honors
`$LESS` instead of adding default flags. When stdout is redirected or piped,
formatting turns off by default; use `--format on` to force formatted output in
a pipe. Binary-looking responses are not printed to a terminal unless you
explicitly choose an output path with `-o file`, force raw stdout with
`$LESS` instead of adding default flags. `$PAGER` is split with POSIX
shell-style quoting, but fetch launches the pager directly and does not
interpret shell operators such as pipes or redirects. When stdout is redirected
or piped, formatting turns off by default; use `--format on` to force formatted
output in a pipe. Binary-looking responses are not printed to a terminal unless
you explicitly choose an output path with `-o file`, force raw stdout with
`-o - > file`, or disable terminal image rendering with `--image off`.

## Examples
Expand Down
4 changes: 3 additions & 1 deletion docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,9 @@ fetch --pager off example.com
When paging is enabled, fetch uses `$PAGER` if it is set. Set `NO_PAGER` to
disable the default `auto` pager. If `$PAGER` is unset, fetch falls back to
`less -FIRX`; when `$LESS` is set, fetch runs `less` without adding its default
flags so your `LESS` options apply.
flags so your `LESS` options apply. `$PAGER` is split with POSIX shell-style
quoting, but fetch launches the pager directly and does not interpret shell
operators such as pipes or redirects.

## Sessions

Expand Down
4 changes: 3 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,9 @@ stdout is a terminal, `on` forces pager use, and `off` disables the pager. When
paging is enabled, fetch uses `$PAGER` if it is set. Set `NO_PAGER` to disable
the default `auto` pager. If `$PAGER` is unset, fetch falls back to `less -FIRX`;
when `$LESS` is set, fetch runs `less` without adding its default flags so your
`LESS` options apply.
`LESS` options apply. `$PAGER` is split with POSIX shell-style quoting, but
fetch launches the pager directly and does not interpret shell operators such as
pipes or redirects.

```ini
# Disable pager
Expand Down
7 changes: 6 additions & 1 deletion docs/output-formatting.md
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,12 @@ fetch --pager off example.com/large-response

### Pager Environment

When paging is enabled, fetch uses `$PAGER` if it is set. Set `NO_PAGER` to disable the default `auto` pager. If `$PAGER` is unset, fetch falls back to `less -FIRX`. When `$LESS` is set, fetch runs `less` without adding its default flags so your `LESS` options apply.
When paging is enabled, fetch uses `$PAGER` if it is set. Set `NO_PAGER` to
disable the default `auto` pager. If `$PAGER` is unset, fetch falls back to
`less -FIRX`. When `$LESS` is set, fetch runs `less` without adding its default
flags so your `LESS` options apply. `$PAGER` is split with POSIX shell-style
quoting, but fetch launches the pager directly and does not interpret shell
operators such as pipes or redirects.

The fallback `less -FIRX` flags are:

Expand Down
71 changes: 33 additions & 38 deletions src/http/response/stdout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ where
F: FnMut(&str) -> Option<String>,
{
if let Some(value) = get_env("PAGER") {
let args = split_command_args(&value);
let args = shlex::split(&value).unwrap_or_default();
if let Some((program, args)) = args.split_first() {
return PagerCommand {
program: program.clone(),
Expand All @@ -143,43 +143,6 @@ where
}
}

fn split_command_args(value: &str) -> Vec<String> {
let mut args = Vec::new();
let mut cur = Vec::new();
let bytes = value.trim().as_bytes();
let mut idx = 0;
while idx < bytes.len() {
match bytes[idx] {
b' ' | b'\t' => {
if !cur.is_empty() {
args.push(String::from_utf8_lossy(&cur).into_owned());
cur.clear();
}
idx += 1;
}
b'\'' | b'"' => {
let quote = bytes[idx];
idx += 1;
while idx < bytes.len() && bytes[idx] != quote {
cur.push(bytes[idx]);
idx += 1;
}
if idx < bytes.len() {
idx += 1;
}
}
ch => {
cur.push(ch);
idx += 1;
}
}
}
if !cur.is_empty() {
args.push(String::from_utf8_lossy(&cur).into_owned());
}
args
}

pub(super) fn response_header_content_type(headers: &HeaderMap) -> ContentType {
let content_type = headers
.get(http::header::CONTENT_TYPE)
Expand Down Expand Up @@ -298,6 +261,38 @@ mod tests {
);
}

#[test]
fn pager_command_uses_shlex_for_pager_environment() {
let got = pager_command_with_env(|name| match name {
"PAGER" => Some(r#"less --prompt='fetch > ' --pattern=a\ b"#.to_string()),
_ => None,
});
assert_eq!(
got,
PagerCommand {
program: "less".to_string(),
args: vec!["--prompt=fetch > ".to_string(), "--pattern=a b".to_string(),],
is_fallback: false,
}
);
}

#[test]
fn pager_command_falls_back_for_invalid_pager_environment() {
let got = pager_command_with_env(|name| match name {
"PAGER" => Some(r#"less "unterminated"#.to_string()),
_ => None,
});
assert_eq!(
got,
PagerCommand {
program: "less".to_string(),
args: vec!["-FIRX".to_string()],
is_fallback: true,
}
);
}

#[test]
fn pager_auto_uses_stdout_terminal_and_skips_images() {
let cli = Cli::try_parse_from(["fetch", "https://example.com"]).unwrap();
Expand Down
Loading