From f056894ddb3e8f0c027f92e32e32c4b2e53a92c7 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Wed, 28 Jan 2026 08:41:17 -0800 Subject: [PATCH] Show full URL parts in Timeline debug view (#373) Co-authored-by: Claude Opus 4.5 --- crates/yaak-http/src/sender.rs | 31 +++++++++++++++-- crates/yaak-models/bindings/gen_models.ts | 2 +- crates/yaak-models/src/models.rs | 14 ++++++++ src-web/components/HttpResponseTimeline.tsx | 38 ++++++++++++++++----- 4 files changed, 73 insertions(+), 12 deletions(-) diff --git a/crates/yaak-http/src/sender.rs b/crates/yaak-http/src/sender.rs index cea5d99c..940d0f31 100644 --- a/crates/yaak-http/src/sender.rs +++ b/crates/yaak-http/src/sender.rs @@ -31,7 +31,14 @@ pub enum HttpResponseEvent { }, SendUrl { method: String, + scheme: String, + username: String, + password: String, + host: String, + port: u16, path: String, + query: String, + fragment: String, }, ReceiveUrl { version: Version, @@ -65,7 +72,16 @@ impl Display for HttpResponseEvent { }; write!(f, "* Redirect {} -> {} ({})", status, url, behavior_str) } - HttpResponseEvent::SendUrl { method, path } => write!(f, "> {} {}", method, path), + HttpResponseEvent::SendUrl { method, scheme, username, password, host, port, path, query, fragment } => { + let auth_str = if username.is_empty() && password.is_empty() { + String::new() + } else { + format!("{}:{}@", username, password) + }; + let query_str = if query.is_empty() { String::new() } else { format!("?{}", query) }; + let fragment_str = if fragment.is_empty() { String::new() } else { format!("#{}", fragment) }; + write!(f, "> {} {}://{}{}:{}{}{}{}", method, scheme, auth_str, host, port, path, query_str, fragment_str) + } HttpResponseEvent::ReceiveUrl { version, status } => { write!(f, "< {} {}", version_to_str(version), status) } @@ -104,7 +120,9 @@ impl From for yaak_models::models::HttpResponseEventData { RedirectBehavior::DropBody => "drop_body".to_string(), }, }, - HttpResponseEvent::SendUrl { method, path } => D::SendUrl { method, path }, + HttpResponseEvent::SendUrl { method, scheme, username, password, host, port, path, query, fragment } => { + D::SendUrl { method, scheme, username, password, host, port, path, query, fragment } + } HttpResponseEvent::ReceiveUrl { version, status } => { D::ReceiveUrl { version: format!("{:?}", version), status } } @@ -415,8 +433,15 @@ impl HttpSender for ReqwestSender { )); send_event(HttpResponseEvent::SendUrl { - path: sendable_req.url().path().to_string(), method: sendable_req.method().to_string(), + scheme: sendable_req.url().scheme().to_string(), + username: sendable_req.url().username().to_string(), + password: sendable_req.url().password().unwrap_or_default().to_string(), + host: sendable_req.url().host_str().unwrap_or_default().to_string(), + port: sendable_req.url().port_or_known_default().unwrap_or(0), + path: sendable_req.url().path().to_string(), + query: sendable_req.url().query().unwrap_or_default().to_string(), + fragment: sendable_req.url().fragment().unwrap_or_default().to_string(), }); let mut request_headers = Vec::new(); diff --git a/crates/yaak-models/bindings/gen_models.ts b/crates/yaak-models/bindings/gen_models.ts index 5b226f6f..da1da73c 100644 --- a/crates/yaak-models/bindings/gen_models.ts +++ b/crates/yaak-models/bindings/gen_models.ts @@ -49,7 +49,7 @@ export type HttpResponseEvent = { model: "http_response_event", id: string, crea * This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support. * The `From` impl is in yaak-http to avoid circular dependencies. */ -export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, } | { "type": "send_url", method: string, path: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, } | { "type": "dns_resolved", hostname: string, addresses: Array, duration: bigint, overridden: boolean, }; +export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, } | { "type": "send_url", method: string, scheme: string, username: string, password: string, host: string, port: number, path: string, query: string, fragment: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, } | { "type": "dns_resolved", hostname: string, addresses: Array, duration: bigint, overridden: boolean, }; export type HttpResponseHeader = { name: string, value: string, }; diff --git a/crates/yaak-models/src/models.rs b/crates/yaak-models/src/models.rs index 93988fd4..05401770 100644 --- a/crates/yaak-models/src/models.rs +++ b/crates/yaak-models/src/models.rs @@ -1495,7 +1495,21 @@ pub enum HttpResponseEventData { }, SendUrl { method: String, + #[serde(default)] + scheme: String, + #[serde(default)] + username: String, + #[serde(default)] + password: String, + #[serde(default)] + host: String, + #[serde(default)] + port: u16, path: String, + #[serde(default)] + query: String, + #[serde(default)] + fragment: String, }, ReceiveUrl { version: String, diff --git a/src-web/components/HttpResponseTimeline.tsx b/src-web/components/HttpResponseTimeline.tsx index b520eb6f..a8cce8d9 100644 --- a/src-web/components/HttpResponseTimeline.tsx +++ b/src-web/components/HttpResponseTimeline.tsx @@ -149,14 +149,27 @@ function EventDetails({ ); } - // Request URL - show method and path separately + // Request URL - show all URL parts separately if (e.type === 'send_url') { + const auth = e.username || e.password ? `${e.username}:${e.password}@` : ''; + const isDefaultPort = + (e.scheme === 'http' && e.port === 80) || (e.scheme === 'https' && e.port === 443); + const portStr = isDefaultPort ? '' : `:${e.port}`; + const query = e.query ? `?${e.query}` : ''; + const fragment = e.fragment ? `#${e.fragment}` : ''; + const fullUrl = `${e.scheme}://${auth}${e.host}${portStr}${e.path}${query}${fragment}`; return ( - - - + {fullUrl} + {e.method} + {e.scheme} + {e.username ? {e.username} : null} + {e.password ? {e.password} : null} + {e.host} + {!isDefaultPort ? {e.port} : null} {e.path} + {e.query ? {e.query} : null} + {e.fragment ? {e.fragment} : null} ); } @@ -244,7 +257,10 @@ type EventTextParts = { prefix: '>' | '<' | '*'; text: string }; function getEventTextParts(event: HttpResponseEventData): EventTextParts { switch (event.type) { case 'send_url': - return { prefix: '>', text: `${event.method} ${event.path}` }; + return { + prefix: '>', + text: `${event.method} ${event.path}${event.query ? `?${event.query}` : ''}${event.fragment ? `#${event.fragment}` : ''}`, + }; case 'receive_url': return { prefix: '<', text: `${event.version} ${event.status}` }; case 'header_up': @@ -265,9 +281,15 @@ function getEventTextParts(event: HttpResponseEventData): EventTextParts { return { prefix: '*', text: `[${formatBytes(event.bytes)} received]` }; case 'dns_resolved': if (event.overridden) { - return { prefix: '*', text: `DNS override ${event.hostname} -> ${event.addresses.join(', ')}` }; + return { + prefix: '*', + text: `DNS override ${event.hostname} -> ${event.addresses.join(', ')}`, + }; } - return { prefix: '*', text: `DNS resolved ${event.hostname} to ${event.addresses.join(', ')} (${event.duration}ms)` }; + return { + prefix: '*', + text: `DNS resolved ${event.hostname} to ${event.addresses.join(', ')} (${event.duration}ms)`, + }; default: return { prefix: '*', text: '[unknown event]' }; } @@ -314,7 +336,7 @@ function getEventDisplay(event: HttpResponseEventData): EventDisplay { icon: 'arrow_big_up_dash', color: 'primary', label: 'Request', - summary: `${event.method} ${event.path}`, + summary: `${event.method} ${event.path}${event.query ? `?${event.query}` : ''}${event.fragment ? `#${event.fragment}` : ''}`, }; case 'receive_url': return {