Skip to content

Commit 4335669

Browse files
committed
Add H1 same-connection redirect follow-up
1 parent c0adfd3 commit 4335669

4 files changed

Lines changed: 76 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## Unreleased
6+
7+
**🚀 Features**
8+
9+
* Add `Session::h1_set_same_connection_followup` for following an HTTP/1.1 redirect on a single pooled upstream connection; disables cache for the skipped intermediate hop when caching is enabled.
10+
11+
**📚 Documentation**
12+
13+
* `ProxyHttp::response_filter`: point to the new follow-up API for same-connection redirect handling.
14+
515
## [0.8.0](https://github.com/cloudflare/pingora/compare/0.7.0...0.8.0) - 2026-03-02
616

717

pingora-proxy/src/lib.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,8 @@ pub struct Session {
480480
upstream_body_bytes_received: usize,
481481
/// Upstream write pending time. Set by proxy layer (HTTP/1.x only).
482482
upstream_write_pending_time: Duration,
483+
h1_skip_downstream_response: bool,
484+
h1_same_connection_followup: Option<RequestHeader>,
483485
/// Flag that is set when the shutdown process has begun.
484486
shutdown_flag: Arc<AtomicBool>,
485487
}
@@ -502,6 +504,8 @@ impl Session {
502504
downstream_modules_ctx: downstream_modules.build_ctx(),
503505
upstream_body_bytes_received: 0,
504506
upstream_write_pending_time: Duration::ZERO,
507+
h1_skip_downstream_response: false,
508+
h1_same_connection_followup: None,
505509
shutdown_flag,
506510
}
507511
}
@@ -742,6 +746,23 @@ impl Session {
742746
self.upstream_write_pending_time = d;
743747
}
744748

749+
/// Queue a follow-up request on the same HTTP/1.1 upstream connection after the current
750+
/// response body is consumed, without sending that response downstream. Use from
751+
/// `response_filter` (e.g. after a 3xx with a same-origin `Location`). Disables caching for
752+
/// the skipped hop when cache is enabled.
753+
pub fn h1_set_same_connection_followup(&mut self, request: RequestHeader) {
754+
if self.cache.enabled() {
755+
self.cache
756+
.disable(NoCacheReason::Custom("H1SameConnectionRedirect"));
757+
}
758+
self.h1_skip_downstream_response = true;
759+
self.h1_same_connection_followup = Some(request);
760+
}
761+
762+
pub(crate) fn h1_skip_downstream_for_upstream_response(&self) -> bool {
763+
self.h1_skip_downstream_response
764+
}
765+
745766
/// Is the proxy process in the process of shutting down (e.g. due to graceful upgrade)?
746767
pub fn is_process_shutting_down(&self) -> bool {
747768
self.shutdown_flag.load(Ordering::Acquire)
@@ -1483,3 +1504,21 @@ where
14831504
Service::new(name, proxy)
14841505
}
14851506
}
1507+
1508+
#[cfg(test)]
1509+
mod h1_same_connection_tests {
1510+
use super::{RequestHeader, Session};
1511+
use http::Method;
1512+
use pingora_core::protocols::Stream;
1513+
1514+
#[tokio::test]
1515+
async fn followup_skips_intermediate_response_downstream() {
1516+
let input = b"GET /a HTTP/1.1\r\nHost: t\r\n\r\n";
1517+
let io = tokio_test::io::Builder::new().read(&input[..]).build();
1518+
let mut session = Session::new_h1(Box::new(io) as Stream);
1519+
assert!(session.read_request().await.unwrap());
1520+
let next = RequestHeader::build(Method::GET, b"/b", None).unwrap();
1521+
session.h1_set_same_connection_followup(next);
1522+
assert!(session.h1_skip_downstream_for_upstream_response());
1523+
}
1524+
}

pingora-proxy/src/proxy_h1.rs

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -167,14 +167,28 @@ where
167167
return (false, false, Some(e));
168168
}
169169

170-
let (server_session_reuse, client_session_reuse, error) =
171-
self.proxy_1to1(session, client_session, peer, ctx).await;
170+
let mut server_session_reuse;
171+
let mut client_session_reuse;
172+
let mut error: Option<Box<Error>>;
173+
174+
loop {
175+
if let Some(next_req) = session.h1_same_connection_followup.take() {
176+
*session.req_header_mut() = next_req;
177+
session.h1_skip_downstream_response = false;
178+
}
179+
180+
(server_session_reuse, client_session_reuse, error) =
181+
self.proxy_1to1(session, client_session, peer, ctx).await;
182+
183+
if error.is_none() && session.h1_same_connection_followup.is_some() {
184+
continue;
185+
}
186+
break;
187+
}
172188

173-
// Record upstream response body bytes received (payload only) for logging consumers.
174189
let upstream_bytes_total = client_session.body_bytes_received();
175190
session.set_upstream_body_bytes_received(upstream_bytes_total);
176191

177-
// Record upstream write pending time for this session only (delta from baseline).
178192
let current_write_pending = client_session.stream().get_write_pending_time();
179193
let upstream_write_pending = current_write_pending.saturating_sub(initial_write_pending);
180194
session.set_upstream_write_pending_time(upstream_write_pending);
@@ -324,15 +338,19 @@ where
324338
}
325339
// check error and abort
326340
// otherwise the error is surfaced via write_response_tasks()
327-
if !serve_from_cache.should_send_to_downstream() {
341+
if !serve_from_cache.should_send_to_downstream()
342+
|| session.h1_skip_downstream_for_upstream_response()
343+
{
328344
if let HttpTask::Failed(e) = task {
329345
return Err(e);
330346
}
331347
}
332348
filtered_tasks.push(task);
333349
}
334350

335-
if !serve_from_cache.should_send_to_downstream() {
351+
if !serve_from_cache.should_send_to_downstream()
352+
|| session.h1_skip_downstream_for_upstream_response()
353+
{
336354
// TODO: need to derive response_done from filtered_tasks in case downstream failed already
337355
return Ok(None);
338356
}

pingora-proxy/src/proxy_trait.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,9 @@ pub trait ProxyHttp {
349349
///
350350
/// The modification is after caching. This filter is called for all responses including
351351
/// responses served from cache.
352+
///
353+
/// To follow a redirect on the same HTTP/1.1 upstream connection (without using the outer
354+
/// proxy retry path), use [`crate::Session::h1_set_same_connection_followup`].
352355
async fn response_filter(
353356
&self,
354357
_session: &mut Session,

0 commit comments

Comments
 (0)