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: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@ metadata/update/DNS/TLS inspection modes, and executes requests via `src/http`.
- Body-producing flags (`--data`, `--json`, `--xml`, `--form`,
`--multipart`, and `--edit`) infer `POST` when `--method` is omitted.
Explicit methods still win, including `-m GET` with a body.
- Schemeless URLs default to HTTPS for hostnames, but `localhost` and all IP
literals default to HTTP.

Retryable requests use replayable request bodies so retries and 307/308 redirects can resend data without holding unrelated state.
Multipart `-F` request bodies are produced with a stable boundary so redirected requests preserve the original body shape.
Expand Down
4 changes: 3 additions & 1 deletion docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ fetch [OPTIONS] [URL]

## URL Handling

When no scheme is provided, `fetch` defaults to HTTPS. Loopback addresses (`localhost`, `127.0.0.1`) default to HTTP.
When no scheme is provided, `fetch` defaults to HTTPS for hostnames. `localhost` and all IP literals default to HTTP.

```sh
fetch example.com # https://example.com
fetch localhost:3000 # http://localhost:3000
fetch 192.168.1.1:8080 # http://192.168.1.1:8080
fetch 1.1.1.1 # http://1.1.1.1
fetch http://example.com # Force HTTP
```

Expand Down
5 changes: 3 additions & 2 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,15 @@ When no scheme is provided, `fetch` defaults to HTTPS:

```sh
fetch example.com # Uses https://example.com
fetch 192.168.1.1:8080 # Uses https://192.168.1.1:8080
```

Loopback addresses default to HTTP for local development:
Loopback addresses and IP literals default to HTTP:

```sh
fetch localhost:3000 # Uses http://localhost:3000
fetch 127.0.0.1:8080 # Uses http://127.0.0.1:8080
fetch 192.168.1.1:8080 # Uses http://192.168.1.1:8080
fetch 1.1.1.1 # Uses http://1.1.1.1
```

You can always specify the scheme explicitly:
Expand Down
35 changes: 30 additions & 5 deletions src/http/metadata.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use super::*;

use std::collections::HashSet;
use std::net::IpAddr;

pub(crate) fn load_session(cli: &Cli) -> Result<Option<crate::session::Session>, FetchError> {
let Some(name) = cli.session.as_deref() else {
Expand Down Expand Up @@ -287,7 +288,7 @@ fn normalize_explicit_url(url: Url) -> Result<Url, FetchError> {

fn normalize_schemeless_url(raw: &str) -> Result<Url, FetchError> {
let probe = Url::parse(&format!("http://{raw}"))?;
let scheme = if probe.host_str().is_some_and(is_loopback) {
let scheme = if probe.host_str().is_some_and(defaults_to_http) {
"http"
} else {
"https"
Expand Down Expand Up @@ -318,6 +319,18 @@ pub(super) fn is_loopback(host: &str) -> bool {
.unwrap_or(false)
}

fn defaults_to_http(host: &str) -> bool {
is_loopback(host) || ip_literal(host).is_some()
}

fn ip_literal(host: &str) -> Option<IpAddr> {
let host = host
.strip_prefix('[')
.and_then(|value| value.strip_suffix(']'))
.unwrap_or(host);
host.parse::<IpAddr>().ok()
}

pub(crate) fn apply_query(url: &mut Url, query: &[String]) {
if query.is_empty() {
return;
Expand Down Expand Up @@ -416,11 +429,23 @@ mod tests {
}

#[test]
fn default_scheme_non_loopback_ip_literals_are_https() {
fn default_scheme_ip_literals_are_http() {
let cases = [
("192.168.1.1:8080/path", "https://192.168.1.1:8080/path"),
("10.0.0.1/path", "https://10.0.0.1/path"),
("[2001:db8::1]/path", "https://[2001:db8::1]/path"),
("10.0.0.1/path", "http://10.0.0.1/path"),
("172.16.0.1/path", "http://172.16.0.1/path"),
("172.31.255.255/path", "http://172.31.255.255/path"),
("172.32.0.1/path", "http://172.32.0.1/path"),
("192.168.1.1:8080/path", "http://192.168.1.1:8080/path"),
("169.254.10.20/path", "http://169.254.10.20/path"),
("1.1.1.1/path", "http://1.1.1.1/path"),
("[fc00::1]/path", "http://[fc00::1]/path"),
("[fd00::1]/path", "http://[fd00::1]/path"),
("[fe80::1]/path", "http://[fe80::1]/path"),
("[2001:db8::1]/path", "http://[2001:db8::1]/path"),
(
"[2001:4860:4860::8888]/path",
"http://[2001:4860:4860::8888]/path",
),
];

for (raw, want) in cases {
Expand Down
Loading