Skip to main content

pingora_proxy/subrequest/
pipe.rs

1// Copyright 2026 Cloudflare, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Subrequest piping.
16//!
17//! Along with subrequests themselves, subrequest piping as a feature is in
18//! alpha stages, APIs are highly unstable and subject to change at any point.
19//!
20//! Unlike proxy_*, it is not a "true" proxy mode; the functions here help
21//! establish a pipe between the main downstream session and the subrequest (which
22//! in most cases will be used as a downstream session itself).
23//!
24//! Furthermore, only downstream modules are invoked on the main downstream session,
25//! and the ProxyHttp trait filters are not run on the HttpTasks from the main session
26//! (the only relevant one being the request body filter).
27
28use crate::proxy_common::{DownstreamStateMachine, ResponseStateMachine};
29use crate::subrequest::*;
30use crate::{PreparedSubrequest, Session};
31use bytes::Bytes;
32use futures::FutureExt;
33use log::{debug, warn};
34use pingora_core::protocols::http::{subrequest::server::SubrequestHandle, HttpTask};
35use pingora_error::{Error, ErrorType::*, OrErr, Result};
36use tokio::sync::mpsc;
37
38pub enum InputBodyType {
39    /// Preset body
40    Preset(InputBody),
41    /// Body should be saved (up to limit)
42    SaveBody(usize),
43}
44
45/// Outcome of [`pipe_subrequest`].
46#[derive(Debug, Default)]
47pub struct PipeSubrequestState {
48    /// Captured body from the main session.
49    pub saved_body: Option<SavedBody>,
50    /// Did the subrequest produce a response header? Checked before the task
51    /// filter runs, so a filtered-out header still counts.
52    pub header_received: bool,
53    /// The spawned subrequest task handle. Always set after spawn. Caller is
54    /// responsible for awaiting/inspecting state.
55    pub join_handle: Option<tokio::task::JoinHandle<()>>,
56    /// The receiving half of the pipe channel. When the coordinator exits
57    /// `pipe_subrequest` before the subrequest task finishes writing, this
58    /// receiver must be kept alive and drained alongside the join handle;
59    /// otherwise dropping it breaks the pipe and prevents the writer from
60    /// completing its cache-write lifecycle.
61    pub pipe_rx: Option<mpsc::Receiver<HttpTask>>,
62}
63
64impl PipeSubrequestState {
65    /// Creates a snapshot for error reporting, excluding the join handle.
66    /// Moves `pipe_rx` into the snapshot so the receiver stays alive through
67    /// the error path and is not dropped when `self` is cleaned up.
68    /// Used by [`map_pipe_err`] to capture state at the point of failure.
69    pub fn snapshot_for_error(&mut self) -> Self {
70        PipeSubrequestState {
71            saved_body: self.saved_body.clone(),
72            header_received: self.header_received,
73            join_handle: None,
74            pipe_rx: self.pipe_rx.take(),
75        }
76    }
77}
78
79pub struct PipeSubrequestError {
80    pub state: PipeSubrequestState,
81    /// Whether error originated (and was propagated from) subrequest itself
82    /// (vs. an error that occurred while sending task)
83    pub from_subreq: bool,
84    pub error: Box<Error>,
85}
86impl PipeSubrequestError {
87    pub fn new(
88        error: impl Into<Box<Error>>,
89        from_subreq: bool,
90        state: PipeSubrequestState,
91    ) -> Self {
92        PipeSubrequestError {
93            error: error.into(),
94            from_subreq,
95            state,
96        }
97    }
98}
99
100fn map_pipe_err<T, E: Into<Box<Error>>>(
101    result: Result<T, E>,
102    from_subreq: bool,
103    state: &mut PipeSubrequestState,
104) -> Result<T, PipeSubrequestError> {
105    result.map_err(|e| PipeSubrequestError::new(e, from_subreq, state.snapshot_for_error()))
106}
107
108#[derive(Debug, Clone)]
109pub struct SavedBody {
110    body: Vec<Bytes>,
111    complete: bool,
112    truncated: bool,
113    length: usize,
114    max_length: usize,
115}
116
117impl SavedBody {
118    pub fn new(max_length: usize) -> Self {
119        SavedBody {
120            body: vec![],
121            complete: false,
122            truncated: false,
123            length: 0,
124            max_length,
125        }
126    }
127
128    pub fn save_body_bytes(&mut self, body_bytes: Bytes) -> bool {
129        let len = body_bytes.len();
130        if self.length + len > self.max_length {
131            self.truncated = true;
132            return false;
133        }
134        self.length += len;
135        self.body.push(body_bytes);
136        true
137    }
138
139    pub fn is_body_complete(&self) -> bool {
140        self.complete && !self.truncated
141    }
142
143    pub fn set_body_complete(&mut self) {
144        self.complete = true;
145    }
146}
147
148#[derive(Debug, Clone)]
149pub enum InputBody {
150    NoBody,
151    Bytes(Vec<Bytes>),
152    // TODO: stream
153}
154
155impl InputBody {
156    pub(crate) fn into_reader(self) -> InputBodyReader {
157        InputBodyReader(match self {
158            InputBody::NoBody => vec![].into_iter(),
159            InputBody::Bytes(v) => v.into_iter(),
160        })
161    }
162
163    pub fn is_body_empty(&self) -> bool {
164        match self {
165            InputBody::NoBody => true,
166            InputBody::Bytes(v) => v.is_empty(),
167        }
168    }
169}
170
171impl std::convert::From<SavedBody> for InputBody {
172    fn from(body: SavedBody) -> Self {
173        if body.body.is_empty() {
174            InputBody::NoBody
175        } else {
176            InputBody::Bytes(body.body)
177        }
178    }
179}
180
181pub async fn pipe_subrequest<F>(
182    session: &mut Session,
183    mut subrequest: PreparedSubrequest,
184    subrequest_handle: SubrequestHandle,
185    mut task_filter: F,
186    input_body: InputBodyType,
187) -> std::result::Result<PipeSubrequestState, PipeSubrequestError>
188where
189    F: FnMut(HttpTask) -> Result<Option<HttpTask>>,
190{
191    let (maybe_preset_body, saved_body) = match input_body {
192        InputBodyType::Preset(body) => (Some(body), None),
193        InputBodyType::SaveBody(limit) => (None, Some(SavedBody::new(limit))),
194    };
195    let use_preset_body = maybe_preset_body.is_some();
196
197    let mut response_state = ResponseStateMachine::new();
198    let (no_body_input, mut maybe_preset_reader) = if use_preset_body {
199        let preset_body = maybe_preset_body.expect("checked above");
200        (preset_body.is_body_empty(), Some(preset_body.into_reader()))
201    } else {
202        (session.as_mut().is_body_done(), None)
203    };
204    let mut downstream_state = DownstreamStateMachine::new(no_body_input);
205
206    let mut state = PipeSubrequestState {
207        saved_body,
208        ..Default::default()
209    };
210
211    // Remove headers if no body.
212    let join_handle = tokio::spawn(async move {
213        if no_body_input {
214            subrequest
215                .session_mut()
216                .as_subrequest_mut()
217                .expect("PreparedSubrequest must be subrequest")
218                .clear_request_body_headers();
219        }
220        let _ = subrequest.run().await;
221    });
222    state.join_handle = Some(join_handle);
223    let tx = subrequest_handle.tx;
224    // Move rx into state immediately so it survives all exit paths (early `?`
225    // returns, errors, and the normal success path). The select loop borrows it
226    // back via `state.pipe_rx.as_mut().expect(...)`.
227    state.pipe_rx = Some(subrequest_handle.rx);
228
229    let mut wants_body = false;
230    let mut wants_body_rx_err = false;
231    let mut wants_body_rx = subrequest_handle.subreq_wants_body;
232
233    let mut proxy_error_rx_err = false;
234    let mut proxy_error_rx = subrequest_handle.subreq_proxy_error;
235
236    // Note: "upstream" here refers to subrequest session tasks,
237    // downstream refers to main session
238    while !downstream_state.is_done() || !response_state.is_done() {
239        let send_permit = tx
240            .try_reserve()
241            .or_err(InternalError, "try_reserve() body pipe for subrequest");
242
243        tokio::select! {
244            task = state.pipe_rx.as_mut().expect("pipe_rx always set after spawn").recv(), if !response_state.upstream_done() => {
245                debug!("upstream event: {:?}", task);
246                if let Some(t) = task {
247                    // Did the subrequest get headers?
248                    if matches!(&t, HttpTask::Header(..)) {
249                        state.header_received = true;
250                    }
251                    // pull as many tasks as we can
252                    const TASK_BUFFER_SIZE: usize = 4;
253                    let mut tasks = Vec::with_capacity(TASK_BUFFER_SIZE);
254                    let task = map_pipe_err(task_filter(t), false, &mut state)?;
255                    if let Some(filtered) = task {
256                        tasks.push(filtered);
257                    }
258                    // tokio::task::unconstrained because now_or_never may yield None when the future is ready
259                    while let Some(maybe_task) = tokio::task::unconstrained(state.pipe_rx.as_mut().expect("pipe_rx always set after spawn").recv()).now_or_never() {
260                        if let Some(t) = maybe_task {
261                            if matches!(&t, HttpTask::Header(..)) {
262                                state.header_received = true;
263                            }
264                            let task = map_pipe_err(task_filter(t), false, &mut state)?;
265                            if let Some(filtered) = task {
266                                tasks.push(filtered);
267                            }
268                        } else {
269                            break
270                        }
271                    }
272                    // FIXME: if one of these tasks is Failed(e), the session will return that
273                    // error; in this case, the error is actually from the subreq
274                    let response_done = map_pipe_err(session.write_response_tasks(tasks).await, false, &mut state)?;
275
276                    // NOTE: technically it is the downstream whose response state has finished here
277                    // we consider the subrequest's work done however
278                    response_state.maybe_set_upstream_done(response_done);
279                    // unsuccessful upgrade response may force the request done
280                    // (can only happen with a real session, TODO to allow with preset body)
281                    downstream_state.maybe_finished(!use_preset_body && session.is_body_done());
282                } else {
283                    debug!("upstream channel closed early");
284                    response_state.maybe_set_upstream_done(true);
285                }
286            },
287
288            res = &mut wants_body_rx, if !wants_body && !wants_body_rx_err => {
289                // subrequest may need time before it needs body, or it may not actually require it
290                // TODO: tx send permit may not be necessary if no oneshot exists
291                if res.is_err() {
292                    wants_body_rx_err = true;
293                } else {
294                    wants_body = true;
295                }
296            }
297
298            res = &mut proxy_error_rx, if !proxy_error_rx_err => {
299                if let Ok(e) = res {
300                    // propagate proxy error to caller
301                    return Err(PipeSubrequestError::new(e, true, state));
302                } else {
303                    // subrequest dropped, let select loop finish
304                    proxy_error_rx_err = true;
305                }
306            }
307
308            _ = tx.reserve(), if downstream_state.is_reading() && send_permit.is_err() => {
309                // If tx is closed, the upstream has already finished its job.
310                downstream_state.maybe_finished(tx.is_closed());
311                debug!("waiting for permit {send_permit:?}, upstream closed {}", tx.is_closed());
312                /* No permit, wait on more capacity to avoid starving.
313                 * Otherwise this select only blocks on rx, which might send no data
314                 * before the entire body is uploaded.
315                 * once more capacity arrives we just loop back
316                 */
317            },
318
319            body = session.downstream_session.read_body_or_idle(downstream_state.is_done()),
320                if wants_body && !use_preset_body && downstream_state.can_poll() && send_permit.is_ok() => {
321                // this is the first subrequest
322                // send the body
323                debug!("downstream event: main body for subrequest");
324                let body = map_pipe_err(body.map_err(|e| e.into_down()), false, &mut state)?;
325
326                // If the request is websocket, `None` body means the request is closed.
327                // Set the response to be done as well so that the request completes normally.
328                if body.is_none() && session.is_upgrade_req() {
329                    response_state.maybe_set_upstream_done(true);
330                }
331
332                let is_body_done = session.is_body_done();
333                let request_done = map_pipe_err(send_body_to_pipe(
334                    session,
335                    body,
336                    is_body_done,
337                    state.saved_body.as_mut(),
338                    send_permit.expect("checked is_ok()"),
339                )
340                .await, false, &mut state)?;
341
342                downstream_state.maybe_finished(request_done);
343
344            },
345
346            // lazily evaluated async block allows us to expect() inside the select! branch
347            body = async { maybe_preset_reader.as_mut().expect("preset body set").read_body() },
348                if wants_body && use_preset_body && !downstream_state.is_done() && downstream_state.can_poll() && send_permit.is_ok() => {
349                debug!("downstream event: preset body for subrequest");
350
351                // TODO: WebSocket handling to set upstream done?
352
353                // preset None body indicates we are done
354                let is_body_done = body.is_none();
355                // Don't run downstream modules on preset input body
356                let request_done = map_pipe_err(do_send_body_to_pipe(
357                    body,
358                    is_body_done,
359                    None,
360                    send_permit.expect("checked is_ok()"),
361                ), false, &mut state)?;
362                downstream_state.maybe_finished(request_done);
363
364            },
365
366            else => break,
367        }
368    }
369    // The output channel can close in the same poll that publishes a proxy error.
370    // Reconcile the terminal error before reporting successful pipe completion.
371    if let Ok(e) = proxy_error_rx.try_recv() {
372        return Err(PipeSubrequestError::new(e, true, state));
373    }
374    Ok(state)
375}
376
377// Mostly the same as proxy_common, but does not run proxy request_body_filter
378async fn send_body_to_pipe(
379    session: &mut Session,
380    mut data: Option<Bytes>,
381    end_of_body: bool,
382    saved_body: Option<&mut SavedBody>,
383    tx: mpsc::Permit<'_, HttpTask>,
384) -> Result<bool> {
385    // None: end of body
386    // this var is to signal if downstream finish sending the body, which shouldn't be
387    // affected by the request_body_filter
388    let end_of_body = end_of_body || data.is_none();
389
390    session
391        .downstream_modules_ctx
392        .request_body_filter(&mut data, end_of_body)
393        .await?;
394
395    do_send_body_to_pipe(data, end_of_body, saved_body, tx)
396}
397
398fn do_send_body_to_pipe(
399    data: Option<Bytes>,
400    end_of_body: bool,
401    mut saved_body: Option<&mut SavedBody>,
402    tx: mpsc::Permit<'_, HttpTask>,
403) -> Result<bool> {
404    // the flag to signal to upstream
405    let upstream_end_of_body = end_of_body || data.is_none();
406
407    /* It is normal to get 0 bytes because of multi-chunk or request_body_filter decides not to
408     * output anything yet.
409     * Don't write 0 bytes to the network since it will be
410     * treated as the terminating chunk */
411    if !upstream_end_of_body && data.as_ref().is_some_and(|d| d.is_empty()) {
412        return Ok(false);
413    }
414
415    debug!(
416        "Read {} bytes body from downstream",
417        data.as_ref().map_or(-1, |d| d.len() as isize)
418    );
419
420    if let Some(capture) = saved_body.as_mut() {
421        if capture.is_body_complete() {
422            warn!("subrequest trying to save body after body is complete");
423        } else if let Some(d) = data.as_ref() {
424            capture.save_body_bytes(d.clone());
425        }
426        if end_of_body {
427            capture.set_body_complete();
428        }
429    }
430
431    tx.send(HttpTask::Body(data, upstream_end_of_body));
432
433    Ok(end_of_body)
434}
435
436#[cfg(test)]
437mod tests {
438    use super::*;
439    use crate::subrequest::Ctx as SubrequestCtx;
440    use crate::{Session, Subrequest, SubrequestSpawner};
441    use async_trait::async_trait;
442    use pingora_core::protocols::http::ServerSession as HttpSession;
443    use pingora_http::ResponseHeader;
444    use std::sync::{Arc, Barrier};
445    use tokio::io::{AsyncReadExt, AsyncWriteExt};
446
447    /// Drops session without producing output — channels close, rx returns None.
448    struct NoopApp;
449
450    #[async_trait]
451    impl Subrequest for NoopApp {
452        async fn process_subrequest(
453            self: Arc<Self>,
454            _session: Box<HttpSession>,
455            _ctx: Box<SubrequestCtx>,
456        ) {
457        }
458    }
459
460    struct HeaderThenErrorApp {
461        header_selected: Arc<Barrier>,
462    }
463
464    #[async_trait]
465    impl Subrequest for HeaderThenErrorApp {
466        async fn process_subrequest(
467            self: Arc<Self>,
468            mut session: Box<HttpSession>,
469            _ctx: Box<SubrequestCtx>,
470        ) {
471            let mut header =
472                ResponseHeader::build(200, Some(1)).expect("test response header should build");
473            header
474                .insert_header(http::header::CONTENT_LENGTH, "0")
475                .expect("test content-length should be valid");
476            session
477                .write_response_header(Box::new(header))
478                .await
479                .expect("test response header should be written");
480
481            self.header_selected.wait();
482            session.on_proxy_failure(Error::new(FileReadError));
483            self.header_selected.wait();
484        }
485    }
486
487    async fn mock_session() -> Session {
488        let input = b"GET / HTTP/1.1\r\nHost: test\r\n\r\n";
489        let mock_io = tokio_test::io::Builder::new().read(&input[..]).build();
490        let mut session = Session::new_h1(Box::new(mock_io) as pingora_core::protocols::Stream);
491        session
492            .downstream_session
493            .read_request()
494            .await
495            .expect("mock request should parse");
496        session
497    }
498
499    async fn writable_mock_session() -> Session {
500        let input = b"GET / HTTP/1.1\r\nHost: test\r\n\r\n";
501        let (mut client, server) = tokio::io::duplex(1024);
502        client
503            .write_all(input)
504            .await
505            .expect("mock request should be written");
506        tokio::spawn(async move {
507            let mut response = Vec::new();
508            let _ = client.read_to_end(&mut response).await;
509        });
510
511        let mut session = Session::new_h1(Box::new(server) as pingora_core::protocols::Stream);
512        session
513            .downstream_session
514            .read_request()
515            .await
516            .expect("mock request should parse");
517        session
518    }
519
520    fn hold_header_until_error(
521        task: HttpTask,
522        header_selected: &Barrier,
523    ) -> Result<Option<HttpTask>> {
524        if matches!(&task, HttpTask::Header(..)) {
525            header_selected.wait();
526            header_selected.wait();
527        }
528        Ok(Some(task))
529    }
530
531    async fn pipe_header_then_error(
532    ) -> std::result::Result<PipeSubrequestState, PipeSubrequestError> {
533        let header_selected = Arc::new(Barrier::new(2));
534        let mut session = writable_mock_session().await;
535        let app = HeaderThenErrorApp {
536            header_selected: Arc::clone(&header_selected),
537        };
538        let spawner = SubrequestSpawner::new(Arc::new(app));
539        let ctx = SubrequestCtx::builder().body_mode(BodyMode::NoBody).build();
540        let (subrequest, handle) = spawner.create_subrequest(session.as_downstream(), ctx);
541        pipe_subrequest(
542            &mut session,
543            subrequest,
544            handle,
545            move |task| hold_header_until_error(task, &header_selected),
546            InputBodyType::Preset(InputBody::NoBody),
547        )
548        .await
549    }
550
551    #[tokio::test]
552    async fn no_header_received_when_subrequest_exits_silently() {
553        let mut session = mock_session().await;
554
555        let spawner = SubrequestSpawner::new(Arc::new(NoopApp));
556        let ctx = SubrequestCtx::builder().body_mode(BodyMode::NoBody).build();
557        let (subrequest, handle) = spawner.create_subrequest(session.as_downstream(), ctx);
558
559        let result = pipe_subrequest(
560            &mut session,
561            subrequest,
562            handle,
563            |task| Ok(Some(task)),
564            InputBodyType::Preset(InputBody::NoBody),
565        )
566        .await;
567
568        let state =
569            result.unwrap_or_else(|e| panic!("pipe should return Ok, not Err: {:?}", e.error));
570        assert!(
571            !state.header_received,
572            "no header should have been received from the no-op subrequest"
573        );
574        assert!(state.join_handle.is_some(), "task handle should be set");
575    }
576
577    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
578    async fn preserves_proxy_error_queued_before_pipe_completion() {
579        let error = pipe_header_then_error()
580            .await
581            .expect_err("subrequest proxy error should be preserved");
582        assert!(error.from_subreq);
583        assert_eq!(error.error.root_etype(), &FileReadError);
584        assert!(error.state.header_received);
585    }
586}