openpencil/desktop/src/http.rs
Danila Poyarkov 6d81d53d1d fix(tauri): preserve HTTP timeout and response semantics
- Apply validated request timeouts without overriding the shared client default

- Preserve abort behavior and null-body response statuses

Co-authored-by: Joseph Cumines <joeycumines@gmail.com>
2026-08-13 20:33:10 +03:00

120 lines
3.5 KiB
Rust

use reqwest::{
header::{HeaderMap, HeaderName, HeaderValue},
Method,
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
#[derive(Clone, Deserialize, Serialize)]
pub struct ProxyHttpHeader {
name: String,
value: String,
}
#[derive(Deserialize)]
pub struct ProxyHttpRequest {
url: String,
method: Option<String>,
headers: Option<Vec<ProxyHttpHeader>>,
body: Option<Vec<u8>>,
max_response_bytes: Option<usize>,
follow_redirects: Option<bool>,
timeout_ms: Option<u64>,
}
#[derive(Serialize)]
pub struct ProxyHttpResponse {
status: u16,
headers: Vec<ProxyHttpHeader>,
body: Vec<u8>,
url: String,
}
fn request_headers(headers: Option<Vec<ProxyHttpHeader>>) -> HeaderMap {
let mut mapped = HeaderMap::new();
for header in headers.unwrap_or_default() {
let Ok(header_name) = HeaderName::from_bytes(header.name.as_bytes()) else {
continue;
};
let Ok(header_value) = HeaderValue::from_str(&header.value) else {
continue;
};
mapped.insert(header_name, header_value);
}
mapped
}
fn request_method(method: Option<String>) -> Result<Method, String> {
let raw = method.unwrap_or_else(|| "GET".into());
Method::from_bytes(raw.as_bytes()).map_err(|e| e.to_string())
}
#[tauri::command]
pub async fn proxy_http_request(request: ProxyHttpRequest) -> Result<ProxyHttpResponse, String> {
let parsed = reqwest::Url::parse(&request.url).map_err(|e| e.to_string())?;
if !matches!(parsed.scheme(), "http" | "https") {
return Err("Only HTTP(S) requests are supported".into());
}
let redirect_policy = if request.follow_redirects.unwrap_or(true) {
reqwest::redirect::Policy::limited(5)
} else {
reqwest::redirect::Policy::none()
};
let client = reqwest::Client::builder()
.redirect(redirect_policy)
.timeout(Duration::from_secs(30))
.build()
.map_err(|e| e.to_string())?;
let mut builder = client
.request(request_method(request.method)?, parsed)
.headers(request_headers(request.headers));
if let Some(body) = request.body {
builder = builder.body(body);
}
if let Some(timeout_ms) = request.timeout_ms {
if timeout_ms == 0 {
return Err("timeoutMs must be greater than 0".into());
}
builder = builder.timeout(Duration::from_millis(timeout_ms));
}
let mut response = builder.send().await.map_err(|e| e.to_string())?;
let status = response.status().as_u16();
let url = response.url().to_string();
let headers = response
.headers()
.iter()
.filter_map(|(name, value)| {
Some(ProxyHttpHeader {
name: name.as_str().to_owned(),
value: value.to_str().ok()?.to_owned(),
})
})
.collect();
if let (Some(limit), Some(content_length)) =
(request.max_response_bytes, response.content_length())
{
if content_length > limit as u64 {
return Err("HTTP response exceeds the configured size limit".into());
}
}
let mut body = Vec::new();
while let Some(chunk) = response.chunk().await.map_err(|e| e.to_string())? {
if request
.max_response_bytes
.is_some_and(|limit| body.len() + chunk.len() > limit)
{
return Err("HTTP response exceeds the configured size limit".into());
}
body.extend_from_slice(&chunk);
}
Ok(ProxyHttpResponse {
status,
headers,
body,
url,
})
}