From 7063c356cc5b24ce50d5259ee0adcb1698cdbe49 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Wed, 4 Mar 2026 15:51:29 -0800 Subject: [PATCH] Preserve request body across 307/308 redirects Clone Bytes bodies before sending so they can be resent on 307/308 Preserve redirects. For stream bodies that can't be replayed, return an error instead of silently dropping the body. Also fix dropped_body metadata to accurately reflect whether the body was actually dropped. Co-Authored-By: Claude Opus 4.6 --- crates/yaak-http/src/transaction.rs | 28 ++++++++++++++++++++++------ plugins/auth-oauth2/src/index.ts | 1 - 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/crates/yaak-http/src/transaction.rs b/crates/yaak-http/src/transaction.rs index bf97bc40..e7f135ae 100644 --- a/crates/yaak-http/src/transaction.rs +++ b/crates/yaak-http/src/transaction.rs @@ -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,10 @@ impl HttpTransaction { }; // Build request for this iteration + let preserved_body = match ¤t_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(), @@ -216,7 +220,23 @@ impl HttpTransaction { }); } - let dropped_body = matches!(behavior, RedirectBehavior::DropBody) && request_had_body; + // 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(), @@ -226,10 +246,6 @@ impl HttpTransaction { dropped_headers, }); - // 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; - redirect_count += 1; } } diff --git a/plugins/auth-oauth2/src/index.ts b/plugins/auth-oauth2/src/index.ts index 07bfec8d..3dd55a2f 100644 --- a/plugins/auth-oauth2/src/index.ts +++ b/plugins/auth-oauth2/src/index.ts @@ -9,7 +9,6 @@ import type { Algorithm } from 'jsonwebtoken'; import { buildHostedCallbackRedirectUri, DEFAULT_LOCALHOST_PORT, - HOSTED_CALLBACK_URL_BASE, stopActiveServer, } from './callbackServer'; import {