diff --git a/src-tauri/yaak-http/src/sender.rs b/src-tauri/yaak-http/src/sender.rs index 1d56cff4..0e62a3d2 100644 --- a/src-tauri/yaak-http/src/sender.rs +++ b/src-tauri/yaak-http/src/sender.rs @@ -4,7 +4,6 @@ use crate::types::{SendableBody, SendableHttpRequest}; use async_trait::async_trait; use futures_util::StreamExt; use reqwest::{Client, Method, Version}; -use std::collections::HashMap; use std::fmt::Display; use std::pin::Pin; use std::task::{Context, Poll}; @@ -153,10 +152,10 @@ pub struct HttpResponse { pub status: u16, /// HTTP status reason phrase (e.g., "OK", "Not Found") pub status_reason: Option, - /// Response headers - pub headers: HashMap, - /// Request headers - pub request_headers: HashMap, + /// Response headers (Vec to support multiple headers with same name, e.g., Set-Cookie) + pub headers: Vec<(String, String)>, + /// Request headers (Vec to support multiple headers with same name) + pub request_headers: Vec<(String, String)>, /// Content-Length from headers (may differ from actual body size) pub content_length: Option, /// Final URL (after redirects) @@ -194,8 +193,8 @@ impl HttpResponse { pub fn new( status: u16, status_reason: Option, - headers: HashMap, - request_headers: HashMap, + headers: Vec<(String, String)>, + request_headers: Vec<(String, String)>, content_length: Option, url: String, remote_addr: Option, @@ -395,10 +394,10 @@ impl HttpSender for ReqwestSender { method: sendable_req.method().to_string(), }); - let mut request_headers = HashMap::new(); + let mut request_headers = Vec::new(); for (name, value) in sendable_req.headers() { let v = value.to_str().unwrap_or_default().to_string(); - request_headers.insert(name.to_string(), v.clone()); + request_headers.push((name.to_string(), v.clone())); send_event(HttpResponseEvent::HeaderUp(name.to_string(), v)); } send_event(HttpResponseEvent::Info("Sending request to server".to_string())); @@ -426,12 +425,12 @@ impl HttpSender for ReqwestSender { status: response.status().to_string(), }); - // Extract headers - let mut headers = HashMap::new(); + // Extract headers (use Vec to preserve duplicates like Set-Cookie) + let mut headers = Vec::new(); for (key, value) in response.headers() { if let Ok(v) = value.to_str() { send_event(HttpResponseEvent::HeaderDown(key.to_string(), v.to_string())); - headers.insert(key.to_string(), v.to_string()); + headers.push((key.to_string(), v.to_string())); } } diff --git a/src-tauri/yaak-http/src/transaction.rs b/src-tauri/yaak-http/src/transaction.rs index 83dbff22..9c997edc 100644 --- a/src-tauri/yaak-http/src/transaction.rs +++ b/src-tauri/yaak-http/src/transaction.rs @@ -264,7 +264,6 @@ mod tests { use crate::decompress::ContentEncoding; use crate::sender::{HttpResponseEvent, HttpSender}; use async_trait::async_trait; - use std::collections::HashMap; use std::pin::Pin; use std::sync::Arc; use tokio::io::AsyncRead; @@ -277,7 +276,7 @@ mod tests { struct MockResponse { status: u16, - headers: HashMap, + headers: Vec<(String, String)>, body: Vec, } @@ -306,7 +305,7 @@ mod tests { mock.status, None, // status_reason mock.headers, - HashMap::new(), + Vec::new(), None, // content_length "https://example.com".to_string(), // url None, // remote_addr @@ -320,7 +319,7 @@ mod tests { #[tokio::test] async fn test_transaction_no_redirect() { - let response = MockResponse { status: 200, headers: HashMap::new(), body: b"OK".to_vec() }; + let response = MockResponse { status: 200, headers: Vec::new(), body: b"OK".to_vec() }; let sender = MockSender::new(vec![response]); let transaction = HttpTransaction::new(sender); @@ -343,12 +342,11 @@ mod tests { #[tokio::test] async fn test_transaction_single_redirect() { - let mut redirect_headers = HashMap::new(); - redirect_headers.insert("Location".to_string(), "https://example.com/new".to_string()); + let redirect_headers = vec![("Location".to_string(), "https://example.com/new".to_string())]; let responses = vec![ MockResponse { status: 302, headers: redirect_headers, body: vec![] }, - MockResponse { status: 200, headers: HashMap::new(), body: b"Final".to_vec() }, + MockResponse { status: 200, headers: Vec::new(), body: b"Final".to_vec() }, ]; let sender = MockSender::new(responses); @@ -375,8 +373,7 @@ mod tests { #[tokio::test] async fn test_transaction_max_redirects_exceeded() { - let mut redirect_headers = HashMap::new(); - redirect_headers.insert("Location".to_string(), "https://example.com/loop".to_string()); + let redirect_headers = vec![("Location".to_string(), "https://example.com/loop".to_string())]; // Create more redirects than allowed let responses: Vec = (0..12) @@ -474,8 +471,8 @@ mod tests { Ok(HttpResponse::new( 200, None, - HashMap::new(), - HashMap::new(), + Vec::new(), + Vec::new(), None, "https://example.com".to_string(), None, @@ -528,8 +525,7 @@ mod tests { _request: SendableHttpRequest, _event_tx: mpsc::Sender, ) -> Result { - let mut headers = HashMap::new(); - headers.insert("set-cookie".to_string(), "session=xyz789; Path=/".to_string()); + let headers = vec![("set-cookie".to_string(), "session=xyz789; Path=/".to_string())]; let body_stream: Pin> = Box::pin(std::io::Cursor::new(vec![])); @@ -537,7 +533,7 @@ mod tests { 200, None, headers, - HashMap::new(), + Vec::new(), None, "https://example.com".to_string(), None, @@ -569,6 +565,79 @@ mod tests { assert!(cookies[0].raw_cookie.contains("session=xyz789")); } + #[tokio::test] + async fn test_multiple_set_cookie_headers() { + // Create a cookie store + let cookie_store = CookieStore::new(); + + // Mock sender that returns multiple Set-Cookie headers + struct MultiSetCookieSender; + + #[async_trait] + impl HttpSender for MultiSetCookieSender { + async fn send( + &self, + _request: SendableHttpRequest, + _event_tx: mpsc::Sender, + ) -> Result { + // Multiple Set-Cookie headers (this is standard HTTP behavior) + let headers = vec![ + ("set-cookie".to_string(), "session=abc123; Path=/".to_string()), + ("set-cookie".to_string(), "user_id=42; Path=/".to_string()), + ("set-cookie".to_string(), "preferences=dark; Path=/; Max-Age=86400".to_string()), + ]; + + let body_stream: Pin> = + Box::pin(std::io::Cursor::new(vec![])); + Ok(HttpResponse::new( + 200, + None, + headers, + Vec::new(), + None, + "https://example.com".to_string(), + None, + Some("HTTP/1.1".to_string()), + body_stream, + ContentEncoding::Identity, + )) + } + } + + let sender = MultiSetCookieSender; + let transaction = HttpTransaction::with_cookie_store(sender, cookie_store.clone()); + + let request = SendableHttpRequest { + url: "https://example.com/login".to_string(), + method: "POST".to_string(), + headers: vec![], + ..Default::default() + }; + + let (_tx, rx) = tokio::sync::watch::channel(false); + let (event_tx, _event_rx) = mpsc::channel(100); + let result = transaction.execute_with_cancellation(request, rx, event_tx).await; + assert!(result.is_ok()); + + // Verify all three cookies were stored + let cookies = cookie_store.get_all_cookies(); + assert_eq!(cookies.len(), 3, "All three Set-Cookie headers should be parsed and stored"); + + let cookie_values: Vec<&str> = cookies.iter().map(|c| c.raw_cookie.as_str()).collect(); + assert!( + cookie_values.iter().any(|c| c.contains("session=abc123")), + "session cookie should be stored" + ); + assert!( + cookie_values.iter().any(|c| c.contains("user_id=42")), + "user_id cookie should be stored" + ); + assert!( + cookie_values.iter().any(|c| c.contains("preferences=dark")), + "preferences cookie should be stored" + ); + } + #[tokio::test] async fn test_cookies_across_redirects() { use std::sync::atomic::{AtomicUsize, Ordering}; @@ -595,9 +664,10 @@ mod tests { let (status, headers) = if count == 0 { // First request: return redirect with Set-Cookie - let mut h = HashMap::new(); - h.insert("location".to_string(), "https://example.com/final".to_string()); - h.insert("set-cookie".to_string(), "redirect_cookie=value1".to_string()); + let h = vec![ + ("location".to_string(), "https://example.com/final".to_string()), + ("set-cookie".to_string(), "redirect_cookie=value1".to_string()), + ]; (302, h) } else { // Second request: verify cookie was sent @@ -610,7 +680,7 @@ mod tests { "Redirect cookie should be included" ); - (200, HashMap::new()) + (200, Vec::new()) }; let body_stream: Pin> = @@ -619,7 +689,7 @@ mod tests { status, None, headers, - HashMap::new(), + Vec::new(), None, "https://example.com".to_string(), None,