1use std::time::Duration;
5
6use bytes::Bytes;
7use http::HeaderMap;
8use metrics::histogram;
9use pingora_core::upstreams::peer::{HttpPeer, Peer as _};
10use tracing::{debug, warn};
11
12use super::{
13 body::dispose_session_abnormal,
14 internals::{
15 CircuitGuard, RawExchange, SUBREQUEST_HEADER_DURATION_SECONDS, SubRequestConnector, check_clean_completion,
16 clamp_peer_timeouts, classify_timeout, empty_body_needs_framing, ensure_host_header, min_timeout,
17 record_header_termination, strip_hop_by_hop_headers, strip_request_framing_headers, strip_reserved_headers,
18 },
19 types::{
20 FrameworkHeaders, StreamLimits, StreamingSubResponse, SubRequest, SubRequestError, SubResponse, SubResponseBody,
21 },
22};
23use crate::circuit::{CircuitCheck, PeerKey};
24
25#[derive(Clone, Debug)]
55pub struct SubRequestClient {
56 connector: SubRequestConnector,
58
59 pub(super) max_response_bytes: usize,
62}
63
64impl SubRequestClient {
65 pub fn new(connector: SubRequestConnector) -> Self {
74 Self {
75 connector,
76 max_response_bytes: crate::config::ABSOLUTE_MAX_BODY_BYTES,
77 }
78 }
79
80 pub fn with_max_response_bytes(connector: SubRequestConnector, max_response_bytes: usize) -> Self {
86 Self {
87 connector,
88 max_response_bytes,
89 }
90 }
91
92 pub fn connector(&self) -> &SubRequestConnector {
94 &self.connector
95 }
96
97 pub fn evict_idle_circuits(&self, idle_threshold: Duration) -> usize {
101 self.connector
102 .circuit_breakers
103 .as_ref()
104 .map_or(0, |registry| registry.evict_idle(idle_threshold))
105 }
106
107 #[expect(clippy::large_stack_frames, reason = "Pingora session types are large")]
113 #[expect(clippy::too_many_lines, reason = "sequential HTTP exchange steps")]
114 async fn open_exchange<'a>(
115 &'a self,
116 peer: &HttpPeer,
117 request: &SubRequest,
118 timeout: Duration,
119 framework_headers: Option<&FrameworkHeaders>,
120 ) -> Result<RawExchange<'a>, SubRequestError> {
121 let exchange_started = tokio::time::Instant::now();
122 let deadline = tokio::time::Instant::now()
123 .checked_add(timeout)
124 .ok_or(SubRequestError::DeadlineExceeded)?;
125 let mut bounded_peer = peer.clone();
126 clamp_peer_timeouts(&mut bounded_peer, timeout);
127
128 let path = request
130 .uri
131 .path_and_query()
132 .map_or(b"/".as_slice(), |pq| pq.as_str().as_bytes());
133 let mut req_header = pingora_http::RequestHeader::build(request.method.clone(), path, None)
134 .map_err(|e| SubRequestError::InvalidRequest(e.to_string()))?;
135
136 let mut sanitized = request.headers.clone();
137 strip_hop_by_hop_headers(&mut sanitized);
138 strip_request_framing_headers(&mut sanitized);
139 strip_reserved_headers(&mut sanitized);
140 if let Some(fw) = framework_headers {
141 for (name, value) in fw.iter() {
142 sanitized.insert(name.clone(), value.clone());
143 }
144 }
145 for (name, value) in &sanitized {
146 let _append = req_header.append_header(name.clone(), value.clone());
147 }
148 ensure_host_header(&mut req_header, &bounded_peer)?;
149 if !request.body.is_empty() || empty_body_needs_framing(&request.method) {
150 let _cl = req_header.insert_header("Content-Length", request.body.len().to_string());
151 }
152
153 let peer_key: Option<PeerKey> = bounded_peer
155 .address()
156 .as_inet()
157 .copied()
158 .map(|addr| PeerKey::new(addr, bounded_peer.sni.as_str()));
159
160 if let (Some(registry), Some(key)) = (&self.connector.circuit_breakers, &peer_key)
161 && !registry.precheck(key)
162 {
163 return Err(SubRequestError::CircuitOpen { peer: key.to_string() });
164 }
165
166 let admission_budget = deadline.saturating_duration_since(tokio::time::Instant::now());
168 if admission_budget.is_zero() {
169 return Err(SubRequestError::DeadlineExceeded);
170 }
171 let permit = self.connector.try_acquire_permit(admission_budget).await?;
172
173 let circuit_guard = match (&self.connector.circuit_breakers, peer_key) {
175 (Some(registry), Some(key)) => match registry.try_acquire(key.clone()) {
176 CircuitCheck::Rejected => {
177 return Err(SubRequestError::CircuitOpen { peer: key.to_string() });
178 },
179 CircuitCheck::Allowed(token) => Some(CircuitGuard::new(registry, key, token)),
180 },
181 _ => None,
182 };
183
184 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
186 if remaining.is_zero() {
187 return Err(SubRequestError::DeadlineExceeded);
188 }
189
190 let (mut session, reused) = tokio::time::timeout(
191 remaining,
192 Box::pin(self.connector.connector().get_http_session(&bounded_peer)),
193 )
194 .await
195 .map_err(|_elapsed| SubRequestError::DeadlineExceeded)?
196 .map_err(|e| SubRequestError::Connect(e.to_string()))?;
197
198 debug!(
199 peer = %bounded_peer.address(),
200 reused,
201 method = %request.method,
202 uri = %request.uri,
203 "sub-request: connected"
204 );
205
206 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
207 if remaining.is_zero() {
208 return Err(SubRequestError::DeadlineExceeded);
209 }
210
211 let write_timeout = min_timeout(bounded_peer.options.write_timeout, remaining);
212 tokio::time::timeout(write_timeout, session.write_request_header(Box::new(req_header)))
213 .await
214 .map_err(|_elapsed| classify_timeout(remaining, bounded_peer.options.write_timeout, "write"))?
215 .map_err(|e| SubRequestError::Io(e.to_string()))?;
216
217 if !request.body.is_empty() {
218 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
219 if remaining.is_zero() {
220 session.shutdown().await;
221 return Err(SubRequestError::DeadlineExceeded);
222 }
223 let write_timeout = min_timeout(bounded_peer.options.write_timeout, remaining);
224 tokio::time::timeout(write_timeout, session.write_request_body(request.body.clone(), true))
225 .await
226 .map_err(|_elapsed| classify_timeout(remaining, bounded_peer.options.write_timeout, "write"))?
227 .map_err(|e| SubRequestError::Io(e.to_string()))?;
228 }
229
230 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
231 if remaining.is_zero() {
232 session.shutdown().await;
233 return Err(SubRequestError::DeadlineExceeded);
234 }
235 let write_timeout = min_timeout(bounded_peer.options.write_timeout, remaining);
236 tokio::time::timeout(write_timeout, session.finish_request_body())
237 .await
238 .map_err(|_elapsed| classify_timeout(remaining, bounded_peer.options.write_timeout, "write"))?
239 .map_err(|e| SubRequestError::Io(e.to_string()))?;
240
241 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
242 if remaining.is_zero() {
243 session.shutdown().await;
244 return Err(SubRequestError::DeadlineExceeded);
245 }
246 let read_timeout = min_timeout(bounded_peer.options.read_timeout, remaining);
247
248 tokio::time::timeout(read_timeout, session.read_response_header())
249 .await
250 .map_err(|_elapsed| classify_timeout(remaining, bounded_peer.options.read_timeout, "read"))?
251 .map_err(|e| SubRequestError::Io(e.to_string()))?;
252
253 let resp_header = session
255 .response_header()
256 .ok_or_else(|| SubRequestError::Io("no response header received".to_owned()))?;
257
258 let status = resp_header.status.as_u16();
259 if !(100..=599).contains(&status) {
260 session.shutdown().await;
261 return Err(SubRequestError::Io(format!(
262 "upstream returned unsupported HTTP status {status}"
263 )));
264 }
265 let mut resp_headers = HeaderMap::new();
266 for (name, value) in &resp_header.headers {
267 if let Ok(v) = http::header::HeaderValue::from_bytes(value.as_bytes()) {
268 resp_headers.append(name.clone(), v);
269 }
270 }
271 strip_hop_by_hop_headers(&mut resp_headers);
272 strip_reserved_headers(&mut resp_headers);
273
274 histogram!(SUBREQUEST_HEADER_DURATION_SECONDS).record(exchange_started.elapsed().as_secs_f64());
276
277 Ok(RawExchange {
278 session,
279 peer: bounded_peer,
280 connector: &self.connector,
281 status,
282 headers: resp_headers,
283 circuit_guard,
284 permit,
285 deadline,
286 })
287 }
288
289 #[expect(
312 clippy::too_many_arguments,
313 reason = "framework_headers is the typed metadata injection point"
314 )]
315 #[expect(clippy::large_stack_frames, reason = "Pingora session types are large")]
316 #[expect(clippy::too_many_lines, reason = "sequential HTTP exchange steps")]
317 pub async fn send_streaming(
318 &self,
319 peer: &HttpPeer,
320 request: &SubRequest,
321 timeout: Duration,
322 limits: StreamLimits,
323 framework_headers: Option<&FrameworkHeaders>,
324 ) -> Result<StreamingSubResponse, SubRequestError> {
325 let mut exchange = self.open_exchange(peer, request, timeout, framework_headers).await?;
326
327 let circuit_guard = exchange.circuit_guard.take();
333
334 if exchange.session.response_done() {
336 match check_clean_completion(&mut exchange.session) {
337 Ok(true) => {},
338 Ok(false) => {
339 let e = SubRequestError::Io(
340 "upstream indicated response done but stream is not cleanly terminated".to_owned(),
341 );
342 return Err(Box::pin(fail_header_exchange(exchange, circuit_guard, "header_incomplete", e)).await);
343 },
344 Err(e) => return Err(Box::pin(fail_header_exchange(exchange, circuit_guard, "h2_error", e)).await),
345 }
346 if let Some(guard) = circuit_guard {
347 guard.finalize_success();
348 }
349 exchange
350 .connector
351 .connector()
352 .release_http_session(exchange.session, &exchange.peer, None)
353 .await;
354 record_header_termination("header_only");
355 return Ok(StreamingSubResponse {
356 status: exchange.status,
357 headers: exchange.headers,
358 body: SubResponseBody::new_done(),
359 });
360 }
361
362 if let Some(guard) = circuit_guard {
367 guard.finalize_success();
368 }
369
370 let read_timeout = exchange.peer.options.read_timeout;
374 exchange.session.set_read_timeout(None);
375
376 let stream_deadline = limits
378 .max_stream_duration
379 .map(|d| {
380 tokio::time::Instant::now()
381 .checked_add(d)
382 .ok_or(SubRequestError::DeadlineExceeded)
383 })
384 .transpose()?;
385
386 let body = SubResponseBody {
387 session: Some(exchange.session),
388 peer: Some(exchange.peer),
389 connector: Some(exchange.connector.clone()),
390 permit: exchange.permit,
391 read_timeout,
392 idle_timeout: limits.idle_timeout,
393 stream_deadline,
394 max_total_bytes: limits.max_total_bytes,
395 received_bytes: 0,
396 chunk_count: 0,
397 stream_started_at: tokio::time::Instant::now(),
398 done: false,
399 };
400
401 debug!(
402 status = exchange.status,
403 header_count = exchange.headers.len(),
404 "sub-request: streaming handoff"
405 );
406
407 Ok(StreamingSubResponse {
408 status: exchange.status,
409 headers: exchange.headers,
410 body,
411 })
412 }
413
414 #[expect(
436 clippy::too_many_arguments,
437 reason = "framework_headers is the typed metadata injection point"
438 )]
439 #[expect(clippy::large_stack_frames, reason = "Pingora session types are large")]
440 #[expect(clippy::too_many_lines, reason = "inline body collection loop")]
441 pub async fn execute(
442 &self,
443 peer: &HttpPeer,
444 request: &SubRequest,
445 max_response_bytes: usize,
446 timeout: Duration,
447 framework_headers: Option<&FrameworkHeaders>,
448 ) -> Result<SubResponse, SubRequestError> {
449 let exchange = self.open_exchange(peer, request, timeout, framework_headers).await;
450
451 let RawExchange {
452 mut session,
453 peer: bounded_peer,
454 connector,
455 status,
456 headers: resp_headers,
457 circuit_guard,
458 permit: _permit,
459 deadline,
460 } = match exchange {
461 Ok(ex) => ex,
462 Err(e) => return Err(e),
463 };
464
465 let effective_limit = max_response_bytes.min(self.max_response_bytes);
466
467 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
469 if remaining.is_zero() {
470 return Err(SubRequestError::DeadlineExceeded);
471 }
472
473 let body_result: Result<Bytes, SubRequestError> = tokio::time::timeout(remaining, async {
474 let mut body_buf = Vec::new();
475 while !session.response_done() {
476 match session.read_response_body().await {
477 Ok(Some(chunk)) => {
478 if body_buf.len() + chunk.len() > effective_limit {
479 warn!(
480 current = body_buf.len(),
481 chunk = chunk.len(),
482 limit = effective_limit,
483 "sub-request response body exceeded limit"
484 );
485 session.shutdown().await;
486 return Err(SubRequestError::ResponseTooLarge {
487 actual: body_buf.len() + chunk.len(),
488 limit: effective_limit,
489 });
490 }
491 body_buf.extend_from_slice(&chunk);
492 },
493 Ok(None) => break,
494 Err(e) => {
495 session.shutdown().await;
496 return Err(SubRequestError::Io(e.to_string()));
497 },
498 }
499 }
500
501 debug!(status, body_bytes = body_buf.len(), "sub-request: response received");
502
503 connector
504 .connector()
505 .release_http_session(session, &bounded_peer, None)
506 .await;
507
508 Ok(Bytes::from(body_buf))
509 })
510 .await
511 .unwrap_or_else(|_elapsed| Err(SubRequestError::DeadlineExceeded));
512
513 let result = body_result.map(|body| SubResponse {
515 status,
516 headers: resp_headers,
517 body,
518 });
519 if let Some(guard) = circuit_guard {
520 guard.finalize(&result);
521 }
522 result
523 }
524}
525
526async fn fail_header_exchange(
534 exchange: RawExchange<'_>,
535 circuit_guard: Option<CircuitGuard<'_>>,
536 termination: &str,
537 error: SubRequestError,
538) -> SubRequestError {
539 drop(circuit_guard);
540 dispose_session_abnormal(
541 exchange.session,
542 Some(&exchange.peer),
543 Some(exchange.connector.connector()),
544 )
545 .await;
546 record_header_termination(termination);
547 error
548}