Skip to main content

pingora_core/protocols/http/
client.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
15use bytes::Bytes;
16use pingora_error::Result;
17use pingora_http::{RequestHeader, ResponseHeader};
18use std::time::Duration;
19
20use super::v2::client::Http2Session;
21use super::{custom::client::Session, v1::client::HttpSession as Http1Session};
22use crate::protocols::{Digest, SocketAddr, Stream};
23
24/// A type for Http client session. It can be either an Http1 connection or an Http2 stream.
25pub enum HttpSession<S = ()> {
26    H1(Http1Session),
27    H2(Http2Session),
28    Custom(S),
29}
30
31impl<S: Session> HttpSession<S> {
32    pub fn as_http1(&self) -> Option<&Http1Session> {
33        match self {
34            Self::H1(s) => Some(s),
35            Self::H2(_) => None,
36            Self::Custom(_) => None,
37        }
38    }
39
40    pub fn as_http2(&self) -> Option<&Http2Session> {
41        match self {
42            Self::H1(_) => None,
43            Self::H2(s) => Some(s),
44            Self::Custom(_) => None,
45        }
46    }
47
48    pub fn as_custom(&self) -> Option<&S> {
49        match self {
50            Self::H1(_) => None,
51            Self::H2(_) => None,
52            Self::Custom(c) => Some(c),
53        }
54    }
55
56    pub fn as_custom_mut(&mut self) -> Option<&mut S> {
57        match self {
58            Self::H1(_) => None,
59            Self::H2(_) => None,
60            Self::Custom(c) => Some(c),
61        }
62    }
63
64    /// Write the request header to the server
65    /// After the request header is sent. The caller can either start reading the response or
66    /// sending request body if any.
67    pub async fn write_request_header(&mut self, req: Box<RequestHeader>) -> Result<()> {
68        match self {
69            HttpSession::H1(h1) => {
70                h1.write_request_header(req).await?;
71                Ok(())
72            }
73            HttpSession::H2(h2) => h2.write_request_header(req, false),
74            HttpSession::Custom(c) => c.write_request_header(req, false).await,
75        }
76    }
77
78    /// Write a chunk of the request body.
79    pub async fn write_request_body(&mut self, data: Bytes, end: bool) -> Result<()> {
80        match self {
81            HttpSession::H1(h1) => {
82                // TODO: maybe h1 should also have the concept of `end`
83                h1.write_body(&data).await?;
84                Ok(())
85            }
86            HttpSession::H2(h2) => h2.write_request_body(data, end).await,
87            HttpSession::Custom(c) => c.write_request_body(data, end).await,
88        }
89    }
90
91    /// Signal that the request body has ended
92    pub async fn finish_request_body(&mut self) -> Result<()> {
93        match self {
94            HttpSession::H1(h1) => {
95                h1.finish_body().await?;
96                Ok(())
97            }
98            HttpSession::H2(h2) => h2.finish_request_body(),
99            HttpSession::Custom(c) => c.finish_request_body().await,
100        }
101    }
102
103    /// Set the read timeout for reading header and body.
104    ///
105    /// The timeout is per read operation, not on the overall time reading the entire response
106    pub fn set_read_timeout(&mut self, timeout: Option<Duration>) {
107        match self {
108            HttpSession::H1(h1) => h1.read_timeout = timeout,
109            HttpSession::H2(h2) => h2.read_timeout = timeout,
110            HttpSession::Custom(c) => c.set_read_timeout(timeout),
111        }
112    }
113
114    /// Set the write timeout for writing header and body.
115    ///
116    /// The timeout is per write operation, not on the overall time writing the entire request.
117    pub fn set_write_timeout(&mut self, timeout: Option<Duration>) {
118        match self {
119            HttpSession::H1(h1) => h1.write_timeout = timeout,
120            HttpSession::H2(h2) => h2.write_timeout = timeout,
121            HttpSession::Custom(c) => c.set_write_timeout(timeout),
122        }
123    }
124
125    /// Read the response header from the server
126    /// For http1, this function can be called multiple times, if the headers received are just
127    /// informational headers.
128    pub async fn read_response_header(&mut self) -> Result<()> {
129        match self {
130            HttpSession::H1(h1) => {
131                h1.read_response().await?;
132                Ok(())
133            }
134            HttpSession::H2(h2) => h2.read_response_header().await,
135            HttpSession::Custom(c) => c.read_response_header().await,
136        }
137    }
138
139    /// Read response body
140    ///
141    /// `None` when no more body to read.
142    pub async fn read_response_body(&mut self) -> Result<Option<Bytes>> {
143        match self {
144            HttpSession::H1(h1) => h1.read_body_bytes().await,
145            HttpSession::H2(h2) => h2.read_response_body().await,
146            HttpSession::Custom(c) => c.read_response_body().await,
147        }
148    }
149
150    /// No (more) body to read
151    pub fn response_done(&mut self) -> bool {
152        match self {
153            HttpSession::H1(h1) => h1.is_body_done(),
154            HttpSession::H2(h2) => h2.response_finished(),
155            HttpSession::Custom(c) => c.response_finished(),
156        }
157    }
158
159    /// Give up the http session abruptly.
160    ///
161    /// This is a failure path: the request is abandoned mid-message, so each
162    /// protocol signals it in whatever way lets the peer tell this apart from a
163    /// request that was completed.
164    /// For H1 this will close the underlying connection
165    /// For H2 this will send RST_STREAM frame to end this stream if the stream has not ended at all
166    pub async fn shutdown(&mut self) {
167        match self {
168            Self::H1(s) => s.shutdown().await,
169            Self::H2(s) => s.shutdown(),
170            Self::Custom(c) => c.abandon("shutdown").await,
171        }
172    }
173
174    /// Get the response header of the server
175    ///
176    /// `None` if the response header is not read yet.
177    pub fn response_header(&self) -> Option<&ResponseHeader> {
178        match self {
179            Self::H1(s) => s.resp_header(),
180            Self::H2(s) => s.response_header(),
181            Self::Custom(c) => c.response_header(),
182        }
183    }
184
185    /// Return the [Digest] of the connection
186    ///
187    /// For reused connection, the timing in the digest will reflect its initial handshakes
188    /// The caller should check if the connection is reused to avoid misuse of the timing field.
189    pub fn digest(&self) -> Option<&Digest> {
190        match self {
191            Self::H1(s) => Some(s.digest()),
192            Self::H2(s) => s.digest(),
193            Self::Custom(c) => c.digest(),
194        }
195    }
196
197    /// Return a mutable [Digest] reference for the connection.
198    ///
199    /// Will return `None` if this is an H2 session and multiple streams are open.
200    pub fn digest_mut(&mut self) -> Option<&mut Digest> {
201        match self {
202            Self::H1(s) => Some(s.digest_mut()),
203            Self::H2(s) => s.digest_mut(),
204            Self::Custom(s) => s.digest_mut(),
205        }
206    }
207
208    /// Return the server (peer) address of the connection.
209    pub fn server_addr(&self) -> Option<&SocketAddr> {
210        match self {
211            Self::H1(s) => s.server_addr(),
212            Self::H2(s) => s.server_addr(),
213            Self::Custom(s) => s.server_addr(),
214        }
215    }
216
217    /// Return the client (local) address of the connection.
218    pub fn client_addr(&self) -> Option<&SocketAddr> {
219        match self {
220            Self::H1(s) => s.client_addr(),
221            Self::H2(s) => s.client_addr(),
222            Self::Custom(s) => s.client_addr(),
223        }
224    }
225
226    /// Get the reference of the [Stream] that this HTTP/1 session is operating upon.
227    /// None if the HTTP session is over H2
228    pub fn stream(&self) -> Option<&Stream> {
229        match self {
230            Self::H1(s) => Some(s.stream()),
231            Self::H2(_) => None,
232            Self::Custom(_) => None,
233        }
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::{HttpSession, Session};
240    use crate::protocols::http::custom::{BodyWrite, CustomMessageWrite};
241    use crate::protocols::{Digest, SocketAddr, UniqueIDType};
242    use async_trait::async_trait;
243    use bytes::Bytes;
244    use futures::Stream;
245    use http::HeaderMap;
246    use pingora_error::Result;
247    use pingora_http::{RequestHeader, ResponseHeader};
248    use std::sync::{Arc, Mutex};
249    use std::time::Duration;
250
251    /// Records which shutdown entry point the session dispatcher reached. Every
252    /// other method is `unreachable!()`: this exists to pin the dispatch, not to
253    /// model a protocol.
254    struct ShutdownRecordingCustom {
255        shutdown_calls: Arc<Mutex<Vec<String>>>,
256    }
257
258    #[async_trait]
259    impl Session for ShutdownRecordingCustom {
260        async fn shutdown(&mut self, code: u32, ctx: &str) {
261            self.shutdown_calls
262                .lock()
263                .unwrap()
264                .push(format!("shutdown({code}, {ctx})"));
265        }
266
267        async fn abandon(&mut self, ctx: &str) {
268            self.shutdown_calls
269                .lock()
270                .unwrap()
271                .push(format!("abandon({ctx})"));
272        }
273
274        async fn write_request_header(
275            &mut self,
276            _req: Box<RequestHeader>,
277            _end: bool,
278        ) -> Result<()> {
279            unreachable!("not used by the shutdown dispatch test")
280        }
281
282        async fn write_request_body(&mut self, _data: Bytes, _end: bool) -> Result<()> {
283            unreachable!("not used by the shutdown dispatch test")
284        }
285
286        async fn finish_request_body(&mut self) -> Result<()> {
287            unreachable!("not used by the shutdown dispatch test")
288        }
289
290        fn set_read_timeout(&mut self, _timeout: Option<Duration>) {
291            unreachable!("not used by the shutdown dispatch test")
292        }
293
294        fn set_write_timeout(&mut self, _timeout: Option<Duration>) {
295            unreachable!("not used by the shutdown dispatch test")
296        }
297
298        async fn read_response_header(&mut self) -> Result<()> {
299            unreachable!("not used by the shutdown dispatch test")
300        }
301
302        async fn read_response_body(&mut self) -> Result<Option<Bytes>> {
303            unreachable!("not used by the shutdown dispatch test")
304        }
305
306        fn response_finished(&self) -> bool {
307            unreachable!("not used by the shutdown dispatch test")
308        }
309
310        fn response_header(&self) -> Option<&ResponseHeader> {
311            unreachable!("not used by the shutdown dispatch test")
312        }
313
314        fn was_upgraded(&self) -> bool {
315            unreachable!("not used by the shutdown dispatch test")
316        }
317
318        fn digest(&self) -> Option<&Digest> {
319            unreachable!("not used by the shutdown dispatch test")
320        }
321
322        fn digest_mut(&mut self) -> Option<&mut Digest> {
323            unreachable!("not used by the shutdown dispatch test")
324        }
325
326        fn server_addr(&self) -> Option<&SocketAddr> {
327            unreachable!("not used by the shutdown dispatch test")
328        }
329
330        fn client_addr(&self) -> Option<&SocketAddr> {
331            unreachable!("not used by the shutdown dispatch test")
332        }
333
334        async fn read_trailers(&mut self) -> Result<Option<HeaderMap>> {
335            unreachable!("not used by the shutdown dispatch test")
336        }
337
338        fn fd(&self) -> UniqueIDType {
339            unreachable!("not used by the shutdown dispatch test")
340        }
341
342        async fn check_response_end_or_error(&mut self, _headers: bool) -> Result<bool> {
343            unreachable!("not used by the shutdown dispatch test")
344        }
345
346        fn take_request_body_writer(&mut self) -> Option<Box<dyn BodyWrite>> {
347            unreachable!("not used by the shutdown dispatch test")
348        }
349
350        async fn finish_custom(&mut self) -> Result<()> {
351            unreachable!("not used by the shutdown dispatch test")
352        }
353
354        fn take_custom_message_reader(
355            &mut self,
356        ) -> Option<Box<dyn Stream<Item = Result<Bytes>> + Unpin + Send + Sync + 'static>> {
357            unreachable!("not used by the shutdown dispatch test")
358        }
359
360        async fn drain_custom_messages(&mut self) -> Result<()> {
361            unreachable!("not used by the shutdown dispatch test")
362        }
363
364        fn take_custom_message_writer(&mut self) -> Option<Box<dyn CustomMessageWrite>> {
365            unreachable!("not used by the shutdown dispatch test")
366        }
367    }
368
369    /// `HttpSession::shutdown` abandons a request mid-message, so it must take the
370    /// entry point that lets a custom protocol convey exactly that. Mirrors the
371    /// server-side `custom_session_shutdown_signals_an_incomplete_message`.
372    ///
373    /// This method has no callers in-tree today, so the test guards the routing
374    /// rather than a live path — which is precisely when a silent regression back
375    /// to the benign `shutdown(0, ..)` would go unnoticed.
376    #[tokio::test]
377    async fn custom_session_shutdown_signals_an_incomplete_message() {
378        let shutdown_calls = Arc::new(Mutex::new(Vec::new()));
379        let mut session = HttpSession::Custom(ShutdownRecordingCustom {
380            shutdown_calls: shutdown_calls.clone(),
381        });
382
383        session.shutdown().await;
384
385        assert_eq!(*shutdown_calls.lock().unwrap(), ["abandon(shutdown)"]);
386    }
387}