Add redirect drop metadata and warning UI (#418)

This commit is contained in:
Gregory Schier
2026-03-05 06:14:11 -08:00
committed by GitHub
parent 615f3134d2
commit 88f5f0e045
12 changed files with 292 additions and 71 deletions

View File

@@ -30,6 +30,8 @@ pub enum HttpResponseEvent {
url: String,
status: u16,
behavior: RedirectBehavior,
dropped_body: bool,
dropped_headers: Vec<String>,
},
SendUrl {
method: String,
@@ -67,12 +69,28 @@ impl Display for HttpResponseEvent {
match self {
HttpResponseEvent::Setting(name, value) => write!(f, "* Setting {}={}", name, value),
HttpResponseEvent::Info(s) => write!(f, "* {}", s),
HttpResponseEvent::Redirect { url, status, behavior } => {
HttpResponseEvent::Redirect {
url,
status,
behavior,
dropped_body,
dropped_headers,
} => {
let behavior_str = match behavior {
RedirectBehavior::Preserve => "preserve",
RedirectBehavior::DropBody => "drop body",
};
write!(f, "* Redirect {} -> {} ({})", status, url, behavior_str)
let body_str = if *dropped_body { ", body dropped" } else { "" };
let headers_str = if dropped_headers.is_empty() {
String::new()
} else {
format!(", headers dropped: {}", dropped_headers.join(", "))
};
write!(
f,
"* Redirect {} -> {} ({}{}{})",
status, url, behavior_str, body_str, headers_str
)
}
HttpResponseEvent::SendUrl {
method,
@@ -130,13 +148,21 @@ impl From<HttpResponseEvent> for yaak_models::models::HttpResponseEventData {
match event {
HttpResponseEvent::Setting(name, value) => D::Setting { name, value },
HttpResponseEvent::Info(message) => D::Info { message },
HttpResponseEvent::Redirect { url, status, behavior } => D::Redirect {
HttpResponseEvent::Redirect {
url,
status,
behavior,
dropped_body,
dropped_headers,
} => D::Redirect {
url,
status,
behavior: match behavior {
RedirectBehavior::Preserve => "preserve".to_string(),
RedirectBehavior::DropBody => "drop_body".to_string(),
},
dropped_body,
dropped_headers,
},
HttpResponseEvent::SendUrl {
method,

View File

@@ -1,7 +1,7 @@
use crate::cookies::CookieStore;
use crate::error::Result;
use crate::sender::{HttpResponse, HttpResponseEvent, HttpSender, RedirectBehavior};
use crate::types::SendableHttpRequest;
use crate::types::{SendableBody, SendableHttpRequest};
use log::debug;
use tokio::sync::mpsc;
use tokio::sync::watch::Receiver;
@@ -87,6 +87,11 @@ impl<S: HttpSender> HttpTransaction<S> {
};
// Build request for this iteration
let preserved_body = match &current_body {
Some(SendableBody::Bytes(b)) => Some(SendableBody::Bytes(b.clone())),
_ => None,
};
let request_had_body = current_body.is_some();
let req = SendableHttpRequest {
url: current_url.clone(),
method: current_method.clone(),
@@ -182,8 +187,6 @@ impl<S: HttpSender> HttpTransaction<S> {
format!("{}/{}", base_path, location)
};
Self::remove_sensitive_headers(&mut current_headers, &previous_url, &current_url);
// Determine redirect behavior based on status code and method
let behavior = if status == 303 {
// 303 See Other always changes to GET
@@ -197,11 +200,8 @@ impl<S: HttpSender> HttpTransaction<S> {
RedirectBehavior::Preserve
};
send_event(HttpResponseEvent::Redirect {
url: current_url.clone(),
status,
behavior: behavior.clone(),
});
let mut dropped_headers =
Self::remove_sensitive_headers(&mut current_headers, &previous_url, &current_url);
// Handle method changes for certain redirect codes
if matches!(behavior, RedirectBehavior::DropBody) {
@@ -211,13 +211,40 @@ impl<S: HttpSender> HttpTransaction<S> {
// Remove content-related headers
current_headers.retain(|h| {
let name_lower = h.0.to_lowercase();
!name_lower.starts_with("content-") && name_lower != "transfer-encoding"
let should_drop =
name_lower.starts_with("content-") || name_lower == "transfer-encoding";
if should_drop {
Self::push_header_if_missing(&mut dropped_headers, &h.0);
}
!should_drop
});
}
// Reset body for next iteration (since it was moved in the send call)
// For redirects that change method to GET or for all redirects since body was consumed
current_body = None;
// Restore body for Preserve redirects (307/308), drop for others.
// Stream bodies can't be replayed (same limitation as reqwest).
current_body = if matches!(behavior, RedirectBehavior::Preserve) {
if request_had_body && preserved_body.is_none() {
// Stream body was consumed and can't be replayed (same as reqwest)
return Err(crate::error::Error::RequestError(
"Cannot follow redirect: request body was a stream and cannot be resent"
.to_string(),
));
}
preserved_body
} else {
None
};
// Body was dropped if the request had one but we can't resend it
let dropped_body = request_had_body && current_body.is_none();
send_event(HttpResponseEvent::Redirect {
url: current_url.clone(),
status,
behavior: behavior.clone(),
dropped_body,
dropped_headers,
});
redirect_count += 1;
}
@@ -231,7 +258,8 @@ impl<S: HttpSender> HttpTransaction<S> {
headers: &mut Vec<(String, String)>,
previous_url: &str,
next_url: &str,
) {
) -> Vec<String> {
let mut dropped_headers = Vec::new();
let previous_host = Url::parse(previous_url).ok().and_then(|u| {
u.host_str().map(|h| format!("{}:{}", h, u.port_or_known_default().unwrap_or(0)))
});
@@ -241,13 +269,24 @@ impl<S: HttpSender> HttpTransaction<S> {
if previous_host != next_host {
headers.retain(|h| {
let name_lower = h.0.to_lowercase();
name_lower != "authorization"
&& name_lower != "cookie"
&& name_lower != "cookie2"
&& name_lower != "proxy-authorization"
&& name_lower != "www-authenticate"
let should_drop = name_lower == "authorization"
|| name_lower == "cookie"
|| name_lower == "cookie2"
|| name_lower == "proxy-authorization"
|| name_lower == "www-authenticate";
if should_drop {
Self::push_header_if_missing(&mut dropped_headers, &h.0);
}
!should_drop
});
}
dropped_headers
}
fn push_header_if_missing(headers: &mut Vec<String>, name: &str) {
if !headers.iter().any(|h| h.eq_ignore_ascii_case(name)) {
headers.push(name.to_string());
}
}
/// Check if a status code indicates a redirect