Skip to content

Commit 4a3a6d4

Browse files
committed
feat: add --header / -H flag for custom HTTP headers in fetch command
Allows passing arbitrary HTTP headers to fetch requests, enabling: - Authorization: Bearer tokens for authenticated APIs like SOM Cache - Custom headers for sites that require specific request headers - Multiple headers via repeated --header flags Examples: plasmate fetch https://cache.plasmate.app/v1/som?url=... -H 'Authorization: Bearer sk-...' plasmate fetch https://example.com -H 'X-Custom: value' -H 'Accept-Language: en' The flag is available as --header or -H (matching curl convention). Internally adds build_client_h1_fallback_with_headers() to the network layer.
1 parent 3bc5f8d commit 4a3a6d4

2 files changed

Lines changed: 56 additions & 1 deletion

File tree

src/main.rs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,15 @@ enum Commands {
137137
/// Disable JavaScript execution entirely
138138
#[arg(long)]
139139
no_js: bool,
140+
/// Add custom HTTP headers (can be specified multiple times).
141+
///
142+
/// Format: "Header-Name: value"
143+
///
144+
/// Examples:
145+
/// --header "Authorization: Bearer sk-..."
146+
/// --header "X-Custom: value" --header "Accept-Language: en"
147+
#[arg(long, short = 'H')]
148+
header: Vec<String>,
140149
/// Load cookies from a stored auth profile (domain name)
141150
#[arg(long)]
142151
profile: Option<String>,
@@ -391,6 +400,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
391400
timeout,
392401
no_external,
393402
no_js,
403+
header,
394404
profile,
395405
tls,
396406
plugin: plugin_paths,
@@ -405,6 +415,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
405415
network::tls::set_global(tls_config);
406416
}
407417
let mut plugins = load_plugins(&plugin_paths)?;
418+
// Parse custom headers
419+
let extra_headers = parse_header_args(&header);
408420
cmd_fetch(
409421
&url,
410422
output.as_deref(),
@@ -415,6 +427,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
415427
!no_external,
416428
no_js,
417429
profile.as_deref(),
430+
&extra_headers,
418431
plugins.as_mut(),
419432
)
420433
.await?;
@@ -993,6 +1006,23 @@ fn load_plugins(
9931006
Ok(Some(pm))
9941007
}
9951008

1009+
/// Parse `--header "Key: Value"` arguments into a `HashMap`.
1010+
fn parse_header_args(args: &[String]) -> std::collections::HashMap<String, String> {
1011+
let mut map = std::collections::HashMap::new();
1012+
for arg in args {
1013+
if let Some(pos) = arg.find(':') {
1014+
let key = arg[..pos].trim().to_string();
1015+
let val = arg[pos + 1..].trim().to_string();
1016+
if !key.is_empty() {
1017+
map.insert(key, val);
1018+
}
1019+
} else {
1020+
eprintln!("Warning: ignoring malformed header (expected 'Name: value'): {}", arg);
1021+
}
1022+
}
1023+
map
1024+
}
1025+
9961026
async fn cmd_fetch(
9971027
url: &str,
9981028
output: Option<&str>,
@@ -1003,6 +1033,7 @@ async fn cmd_fetch(
10031033
external_scripts: bool,
10041034
no_js: bool,
10051035
profile: Option<&str>,
1036+
extra_headers: &std::collections::HashMap<String, String>,
10061037
mut plugins: Option<&mut plugin::PluginManager>,
10071038
) -> Result<(), Box<dyn std::error::Error>> {
10081039
// Check if the daemon is running and delegate to it
@@ -1040,7 +1071,8 @@ async fn cmd_fetch(
10401071
}
10411072

10421073
let tls_config = network::tls::global();
1043-
let client = network::fetch::build_client_h1_fallback(user_agent, jar, tls_config)?;
1074+
let headers_opt = if extra_headers.is_empty() { None } else { Some(extra_headers) };
1075+
let client = network::fetch::build_client_h1_fallback_with_headers(user_agent, jar, tls_config, headers_opt)?;
10441076

10451077
// Plugin hook: pre_navigate
10461078
let effective_url = if let Some(pm) = plugins.as_deref_mut() {

src/network/fetch.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,31 @@ pub fn build_client_h1_fallback(
7676
cookie_jar: Arc<Jar>,
7777
tls_config: Option<&TlsConfig>,
7878
) -> Result<Client, FetchError> {
79+
build_client_h1_fallback_with_headers(user_agent, cookie_jar, tls_config, None)
80+
}
81+
82+
/// Build an HTTP/1.1 client with optional extra default headers.
83+
pub fn build_client_h1_fallback_with_headers(
84+
user_agent: Option<&str>,
85+
cookie_jar: Arc<Jar>,
86+
tls_config: Option<&TlsConfig>,
87+
extra_headers: Option<&std::collections::HashMap<String, String>>,
88+
) -> Result<Client, FetchError> {
89+
let mut headers = reqwest::header::HeaderMap::new();
90+
if let Some(eh) = extra_headers {
91+
for (k, v) in eh {
92+
if let (Ok(name), Ok(val)) = (
93+
reqwest::header::HeaderName::from_bytes(k.as_bytes()),
94+
reqwest::header::HeaderValue::from_str(v),
95+
) {
96+
headers.insert(name, val);
97+
}
98+
}
99+
}
100+
79101
let mut builder = Client::builder()
80102
.user_agent(user_agent.unwrap_or(DEFAULT_USER_AGENT))
103+
.default_headers(headers)
81104
.cookie_provider(cookie_jar)
82105
.redirect(reqwest::redirect::Policy::limited(10))
83106
.pool_max_idle_per_host(16)

0 commit comments

Comments
 (0)