diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 668674b..8fedb8f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use std::time::Instant; use serde::{Serialize, Deserialize}; use tauri::State; +use tokio::sync::{Mutex, oneshot}; #[derive(Serialize, Deserialize)] struct HttpResponse { @@ -9,6 +10,15 @@ struct HttpResponse { headers: HashMap, body: String, time_elapsed: u128, // milliseconds + headers_size: usize, + body_size: usize, + total_size: usize, +} + +#[derive(Serialize, Deserialize)] +struct AppError { + code: String, + message: String, } #[derive(Serialize, Deserialize, Debug)] @@ -42,19 +52,84 @@ struct AuthConfig { struct AppState { client: reqwest::Client, + pending_requests: Mutex>>, +} + +impl AppError { + fn new(code: &str, message: impl Into) -> Self { + Self { + code: code.to_string(), + message: message.into(), + } + } +} + +fn map_reqwest_error(error: reqwest::Error) -> AppError { + if error.is_timeout() { + return AppError::new("timeout", "The request timed out after 30 seconds."); + } + + if error.is_connect() { + return AppError::new("connect", format!("Failed to connect: {error}")); + } + + if error.is_decode() { + return AppError::new("decode", format!("Failed to decode response body: {error}")); + } + + if error.is_request() { + return AppError::new("request", format!("Failed to build or send the request: {error}")); + } + + AppError::new("network", format!("Request failed: {error}")) +} + +async fn perform_request( + request_builder: reqwest::RequestBuilder, +) -> Result { + let start_time = Instant::now(); + let response = request_builder.send().await.map_err(map_reqwest_error)?; + let time_elapsed = start_time.elapsed().as_millis(); + + let status = response.status().as_u16(); + + let mut response_headers = HashMap::new(); + let mut headers_size = 2usize; + for (key, value) in response.headers() { + let val_bytes = value.as_bytes(); + let val_str = value.to_str().unwrap_or("").to_string(); + headers_size += key.as_str().len() + 2 + val_bytes.len() + 2; + response_headers.insert(key.to_string(), val_str); + } + + let body_text = response.text().await.map_err(map_reqwest_error)?; + let body_size = body_text.as_bytes().len(); + let total_size = headers_size + body_size; + + Ok(HttpResponse { + status, + headers: response_headers, + body: body_text, + time_elapsed, + headers_size, + body_size, + total_size, + }) } #[tauri::command] async fn execute_request( state: State<'_, AppState>, + request_id: String, method: String, url: String, headers: HashMap, body: Option, query_params: Option>, auth: Option, -) -> Result { - let req_method = method.parse::().map_err(|e| e.to_string())?; +) -> Result { + let req_method = method.parse::() + .map_err(|e| AppError::new("invalid_method", format!("Invalid HTTP method: {e}")))?; let mut request_builder = state.client.request(req_method, &url); // Add Query Params @@ -99,29 +174,40 @@ async fn execute_request( } } - let start_time = Instant::now(); - // Execute request - let response = request_builder.send().await.map_err(|e| e.to_string())?; - let time_elapsed = start_time.elapsed().as_millis(); - - let status = response.status().as_u16(); - - let mut response_headers = HashMap::new(); - for (key, value) in response.headers() { - // Handle header value to string conversion (skipping non-utf8 for simplicity or lossy conv) - let val_str = value.to_str().unwrap_or("").to_string(); - // Capitalize or keep standard key format? keeping standard. - response_headers.insert(key.to_string(), val_str); + let (cancel_tx, mut cancel_rx) = oneshot::channel::<()>(); + { + let mut pending_requests = state.pending_requests.lock().await; + pending_requests.insert(request_id.clone(), cancel_tx); } - let body_text = response.text().await.map_err(|e| e.to_string())?; + let result = tokio::select! { + _ = &mut cancel_rx => Err(AppError::new("canceled", "The request was canceled.")), + result = perform_request(request_builder) => result, + }; - Ok(HttpResponse { - status, - headers: response_headers, - body: body_text, - time_elapsed, - }) + let mut pending_requests = state.pending_requests.lock().await; + pending_requests.remove(&request_id); + + result +} + +#[tauri::command] +async fn cancel_request( + state: State<'_, AppState>, + request_id: String, +) -> Result { + let sender = { + let mut pending_requests = state.pending_requests.lock().await; + pending_requests.remove(&request_id) + }; + + match sender { + Some(cancel_tx) => { + let _ = cancel_tx.send(()); + Ok(true) + } + None => Ok(false), + } } #[cfg_attr(mobile, tauri::mobile_entry_point)] @@ -132,9 +218,12 @@ pub fn run() { .expect("failed to create HTTP client"); tauri::Builder::default() - .manage(AppState { client }) + .manage(AppState { + client, + pending_requests: Mutex::new(HashMap::new()), + }) .plugin(tauri_plugin_opener::init()) - .invoke_handler(tauri::generate_handler![execute_request]) + .invoke_handler(tauri::generate_handler![execute_request, cancel_request]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src/components/RequestPanel.vue b/src/components/RequestPanel.vue index 0060fb8..6102710 100644 --- a/src/components/RequestPanel.vue +++ b/src/components/RequestPanel.vue @@ -1,22 +1,120 @@