1use super::{
16 HTTP_HEADER_CONTENT_HTML, HTTP_HEADER_CONTENT_JSON,
17 HTTP_HEADER_CONTENT_TEXT, HTTP_HEADER_NO_CACHE, HTTP_HEADER_NO_STORE,
18 HTTP_HEADER_TRANSFER_CHUNKED, HttpHeader, LOG_TARGET, get_super_ts,
19 new_internal_error,
20};
21use bytes::{Bytes, BytesMut};
22use http::StatusCode;
23use http::header;
24use http::{HeaderName, HeaderValue};
25use pingora::http::ResponseHeader;
26use pingora::proxy::Session;
27use serde::Serialize;
28use std::pin::Pin;
29use tokio::io::AsyncReadExt;
30use tracing::error;
31
32fn new_cache_control_header(
46 max_age: Option<u32>,
47 cache_private: Option<bool>,
48) -> HttpHeader {
49 let max_age = match max_age {
51 Some(0) | None => return HTTP_HEADER_NO_CACHE.clone(),
52 Some(age) => age,
53 };
54
55 let category: &[u8] = if cache_private.unwrap_or_default() {
57 b"private"
58 } else {
59 b"public"
60 };
61
62 let mut buf = BytesMut::with_capacity(category.len() + 9 + 10); buf.extend_from_slice(category);
67 buf.extend_from_slice(b", max-age=");
68 buf.extend_from_slice(itoa::Buffer::new().format(max_age).as_bytes());
69
70 if let Ok(value) = HeaderValue::from_bytes(&buf) {
72 return (header::CACHE_CONTROL, value);
73 }
74 HTTP_HEADER_NO_CACHE.clone()
76}
77
78#[derive(Default, Debug)]
82pub struct HttpResponseBuilder {
83 response: HttpResponse,
85}
86
87impl HttpResponseBuilder {
88 pub fn new(status: StatusCode) -> Self {
90 Self {
91 response: HttpResponse {
92 status,
93 ..Default::default()
94 },
95 }
96 }
97
98 pub fn body(mut self, body: impl Into<Bytes>) -> Self {
103 self.response.body = body.into();
104 self
105 }
106
107 pub fn header(mut self, header: HttpHeader) -> Self {
111 self.response
112 .headers
113 .get_or_insert_with(Vec::new)
114 .push(header);
115 self
116 }
117
118 pub fn headers(mut self, headers: Vec<HttpHeader>) -> Self {
120 self.response
121 .headers
122 .get_or_insert_with(Vec::new)
123 .extend(headers);
124 self
125 }
126
127 pub fn max_age(mut self, seconds: u32, is_private: bool) -> Self {
129 self.response.max_age = Some(seconds);
130 self.response.cache_private = Some(is_private);
131 self
132 }
133
134 pub fn no_store(self) -> Self {
136 self.header(HTTP_HEADER_NO_STORE.clone())
137 }
138
139 pub fn finish(self) -> HttpResponse {
141 self.response
142 }
143}
144
145#[derive(Default, Clone, Debug)]
148pub struct HttpResponse {
149 pub status: StatusCode,
151 pub body: Bytes,
153 pub max_age: Option<u32>,
155 pub created_at: Option<u32>,
157 pub cache_private: Option<bool>,
159 pub headers: Option<Vec<HttpHeader>>,
161}
162
163impl HttpResponse {
164 pub fn builder(status: StatusCode) -> HttpResponseBuilder {
166 HttpResponseBuilder::new(status)
167 }
168
169 pub fn no_content() -> Self {
171 Self::builder(StatusCode::NO_CONTENT).no_store().finish()
172 }
173
174 pub fn bad_request(body: impl Into<Bytes>) -> Self {
176 Self::builder(StatusCode::BAD_REQUEST)
177 .body(body)
178 .header(HTTP_HEADER_CONTENT_TEXT.clone())
179 .no_store()
180 .finish()
181 }
182
183 pub fn not_found(body: impl Into<Bytes>) -> Self {
185 Self::builder(StatusCode::NOT_FOUND)
186 .body(body)
187 .header(HTTP_HEADER_CONTENT_TEXT.clone())
188 .no_store()
189 .finish()
190 }
191
192 pub fn unknown_error(body: impl Into<Bytes>) -> Self {
194 Self::builder(StatusCode::INTERNAL_SERVER_ERROR)
195 .body(body)
196 .header(HTTP_HEADER_CONTENT_TEXT.clone())
197 .no_store()
198 .finish()
199 }
200
201 pub fn html(body: impl Into<Bytes>) -> Self {
203 Self::builder(StatusCode::OK)
204 .body(body)
205 .header(HTTP_HEADER_CONTENT_HTML.clone())
206 .header(HTTP_HEADER_NO_CACHE.clone())
207 .finish()
208 }
209
210 pub fn redirect(location: &str) -> pingora::Result<Self> {
212 let value = HeaderValue::from_str(location).map_err(|e| {
214 error!(error = e.to_string(), "to header value fail");
215 new_internal_error(500, e)
216 })?;
217 Ok(Self::builder(StatusCode::TEMPORARY_REDIRECT)
219 .header((header::LOCATION, value))
220 .header(HTTP_HEADER_NO_CACHE.clone())
221 .finish())
222 }
223
224 pub fn text(body: impl Into<Bytes>) -> Self {
226 Self::builder(StatusCode::OK)
227 .body(body)
228 .header(HTTP_HEADER_CONTENT_TEXT.clone())
229 .header(HTTP_HEADER_NO_CACHE.clone())
230 .finish()
231 }
232
233 pub fn try_from_json<T>(value: &T) -> pingora::Result<Self>
235 where
236 T: ?Sized + Serialize,
237 {
238 let buf = serde_json::to_vec(value).map_err(|e| {
240 error!(target: LOG_TARGET, error = e.to_string(), "to json fail");
241 new_internal_error(400, e)
242 })?;
243 Ok(Self::builder(StatusCode::OK)
245 .body(buf)
246 .header(HTTP_HEADER_CONTENT_JSON.clone())
247 .finish())
248 }
249
250 pub fn try_from_json_status<T>(
252 value: &T,
253 status: StatusCode,
254 ) -> pingora::Result<Self>
255 where
256 T: ?Sized + Serialize,
257 {
258 let mut resp = Self::try_from_json(value)?;
260 resp.status = status;
262 Ok(resp)
263 }
264
265 pub fn new_response_header(&self) -> pingora::Result<ResponseHeader> {
267 let custom = self.headers.as_ref().map_or(0, |headers| headers.len());
271 let mut resp = ResponseHeader::build(self.status, Some(3 + custom))?;
272
273 let mut add_header =
275 |name: &HeaderName, value: &HeaderValue| -> pingora::Result<()> {
276 resp.insert_header(name, value)?;
277 Ok(())
278 };
279
280 add_header(
282 &header::CONTENT_LENGTH,
283 &HeaderValue::from(self.body.len()),
284 )?;
285
286 let (name, value) =
288 new_cache_control_header(self.max_age, self.cache_private);
289 add_header(&name, &value)?;
290
291 if let Some(created_at) = self.created_at {
293 let secs = get_super_ts().saturating_sub(created_at);
294 add_header(&header::AGE, &HeaderValue::from(secs))?;
295 }
296
297 if let Some(headers) = &self.headers {
299 for (name, value) in headers {
300 add_header(name, value)?;
301 }
302 }
303 Ok(resp)
304 }
305
306 pub async fn send(self, session: &mut Session) -> pingora::Result<usize> {
308 let header = self.new_response_header()?;
310 let size = self.body.len();
311 session
313 .write_response_header(Box::new(header), false)
314 .await?;
315 session.write_response_body(Some(self.body), true).await?;
317 session.finish_body().await?;
319 Ok(size)
320 }
321}
322
323pub struct HttpChunkResponse<'r, R> {
327 pub reader: Pin<&'r mut R>,
329 pub chunk_size: usize,
331 pub max_age: Option<u32>,
333 pub cache_private: Option<bool>,
335 pub headers: Option<Vec<HttpHeader>>,
337}
338
339const DEFAULT_BUF_SIZE: usize = 8 * 1024;
341
342impl<'r, R> HttpChunkResponse<'r, R>
343where
344 R: tokio::io::AsyncRead + std::marker::Unpin,
346{
347 pub fn new(r: &'r mut R) -> Self {
349 Self {
350 reader: Pin::new(r),
351 chunk_size: DEFAULT_BUF_SIZE,
352 max_age: None,
353 headers: None,
354 cache_private: None,
355 }
356 }
357
358 pub fn get_response_header(&self) -> pingora::Result<ResponseHeader> {
362 let mut resp = ResponseHeader::build(StatusCode::OK, Some(4))?;
364 if let Some(headers) = &self.headers {
366 for (name, value) in headers {
367 resp.insert_header(name.to_owned(), value)?;
368 }
369 }
370
371 let chunked = HTTP_HEADER_TRANSFER_CHUNKED.clone();
373 resp.insert_header(chunked.0, chunked.1)?;
374
375 let cache_control =
377 new_cache_control_header(self.max_age, self.cache_private);
378 resp.insert_header(cache_control.0, cache_control.1)?;
379 Ok(resp)
380 }
381
382 async fn next_chunk(
391 &mut self,
392 buffer: &mut BytesMut,
393 ) -> pingora::Result<Option<Bytes>> {
394 buffer.reserve(self.chunk_size.max(512));
395 let size = self.reader.read_buf(buffer).await.map_err(|e| {
396 error!(error = e.to_string(), "read data fail");
397 new_internal_error(400, e)
398 })?;
399 if size == 0 {
400 return Ok(None);
401 }
402 Ok(Some(buffer.split().freeze()))
403 }
404
405 pub async fn send(
410 mut self,
411 session: &mut Session,
412 ) -> pingora::Result<usize> {
413 let header = self.get_response_header()?;
415 session
416 .write_response_header(Box::new(header), false)
417 .await?;
418
419 let mut sent = 0;
420 let mut buffer = BytesMut::with_capacity(self.chunk_size.max(512));
421 let mut pending = self.next_chunk(&mut buffer).await?;
424 while let Some(chunk) = pending {
425 sent += chunk.len();
426 pending = self.next_chunk(&mut buffer).await?;
427 session
428 .write_response_body(Some(chunk), pending.is_none())
429 .await?;
430 }
431 session.finish_body().await?;
433
434 Ok(sent)
435 }
436}
437#[cfg(test)]
438mod tests {
439 use super::*;
440 use crate::convert_headers;
441 use bytes::Bytes;
442 use http::StatusCode;
443 use pretty_assertions::assert_eq;
444 use serde::Serialize;
445 use std::io::Write;
446 use tempfile::NamedTempFile;
447 use tokio::fs;
448 #[test]
449 fn test_new_cache_control_header() {
450 assert_eq!(
451 r###"("cache-control", "private, max-age=3600")"###,
452 format!("{:?}", new_cache_control_header(Some(3600), Some(true)))
453 );
454 assert_eq!(
455 r###"("cache-control", "public, max-age=3600")"###,
456 format!("{:?}", new_cache_control_header(Some(3600), None))
457 );
458 assert_eq!(
459 r###"("cache-control", "private, no-cache")"###,
460 format!("{:?}", new_cache_control_header(Some(0), Some(false)))
461 );
462 assert_eq!(
463 r###"("cache-control", "private, no-cache")"###,
464 format!("{:?}", new_cache_control_header(None, None))
465 );
466 }
467
468 #[test]
469 fn test_http_response() {
470 assert_eq!(
471 r###"HttpResponse { status: 204, body: b"", max_age: None, created_at: None, cache_private: None, headers: Some([("cache-control", "private, no-store")]) }"###,
472 format!("{:?}", HttpResponse::no_content())
473 );
474 assert_eq!(
475 r###"HttpResponse { status: 404, body: b"Not Found", max_age: None, created_at: None, cache_private: None, headers: Some([("content-type", "text/plain; charset=utf-8"), ("cache-control", "private, no-store")]) }"###,
476 format!("{:?}", HttpResponse::not_found("Not Found"))
477 );
478 assert_eq!(
479 r###"HttpResponse { status: 500, body: b"Unknown Error", max_age: None, created_at: None, cache_private: None, headers: Some([("content-type", "text/plain; charset=utf-8"), ("cache-control", "private, no-store")]) }"###,
480 format!("{:?}", HttpResponse::unknown_error("Unknown Error"))
481 );
482
483 assert_eq!(
484 r###"HttpResponse { status: 400, body: b"Bad Request", max_age: None, created_at: None, cache_private: None, headers: Some([("content-type", "text/plain; charset=utf-8"), ("cache-control", "private, no-store")]) }"###,
485 format!("{:?}", HttpResponse::bad_request("Bad Request"))
486 );
487
488 assert_eq!(
489 r###"HttpResponse { status: 200, body: b"<p>Pingap</p>", max_age: None, created_at: None, cache_private: None, headers: Some([("content-type", "text/html; charset=utf-8"), ("cache-control", "private, no-cache")]) }"###,
490 format!("{:?}", HttpResponse::html("<p>Pingap</p>"))
491 );
492
493 assert_eq!(
494 r###"HttpResponse { status: 307, body: b"", max_age: None, created_at: None, cache_private: None, headers: Some([("location", "http://example.com/"), ("cache-control", "private, no-cache")]) }"###,
495 format!(
496 "{:?}",
497 HttpResponse::redirect("http://example.com/").unwrap()
498 )
499 );
500
501 assert_eq!(
502 r###"HttpResponse { status: 200, body: b"Hello World!", max_age: None, created_at: None, cache_private: None, headers: Some([("content-type", "text/plain; charset=utf-8"), ("cache-control", "private, no-cache")]) }"###,
503 format!("{:?}", HttpResponse::text("Hello World!"))
504 );
505
506 #[derive(Serialize)]
507 struct Data {
508 message: String,
509 }
510 let resp = HttpResponse::try_from_json_status(
511 &Data {
512 message: "Hello World!".to_string(),
513 },
514 StatusCode::BAD_REQUEST,
515 )
516 .unwrap();
517 assert_eq!(
518 r###"HttpResponse { status: 400, body: b"{\"message\":\"Hello World!\"}", max_age: None, created_at: None, cache_private: None, headers: Some([("content-type", "application/json; charset=utf-8")]) }"###,
519 format!("{resp:?}")
520 );
521 let resp = HttpResponse::try_from_json(&Data {
522 message: "Hello World!".to_string(),
523 })
524 .unwrap();
525 assert_eq!(
526 r###"HttpResponse { status: 200, body: b"{\"message\":\"Hello World!\"}", max_age: None, created_at: None, cache_private: None, headers: Some([("content-type", "application/json; charset=utf-8")]) }"###,
527 format!("{resp:?}")
528 );
529
530 let resp = HttpResponse {
531 status: StatusCode::OK,
532 body: Bytes::from("Hello world!"),
533 max_age: Some(3600),
534 created_at: Some(0),
535 cache_private: Some(true),
536 headers: Some(
537 convert_headers(&[
538 "Contont-Type: application/json".to_string(),
539 "Content-Encoding: gzip".to_string(),
540 ])
541 .unwrap(),
542 ),
543 };
544
545 let mut header = resp.new_response_header().unwrap();
546 assert_eq!(true, !header.headers.get("Age").unwrap().is_empty());
547 header.remove_header("Age").unwrap();
548
549 assert_eq!(
550 r###"ResponseHeader { base: Parts { status: 200, version: HTTP/1.1, headers: {"content-length": "12", "cache-control": "private, max-age=3600", "content-encoding": "gzip", "contont-type": "application/json"} }, header_name_map: Some({"content-length": CaseHeaderName(b"Content-Length"), "cache-control": CaseHeaderName(b"Cache-Control"), "content-encoding": CaseHeaderName(b"Content-Encoding"), "contont-type": CaseHeaderName(b"contont-type")}), reason_phrase: None }"###,
551 format!("{header:?}")
552 );
553 }
554
555 #[tokio::test]
556 async fn test_http_chunk_response() {
557 let file = include_bytes!("http_response.rs");
558 let mut f = NamedTempFile::new().unwrap();
559 f.write_all(file).unwrap();
560 let mut f = fs::OpenOptions::new().read(true).open(f).await.unwrap();
561 let mut resp = HttpChunkResponse::new(&mut f);
562 resp.max_age = Some(3600);
563 resp.cache_private = Some(false);
564 resp.headers = Some(
565 convert_headers(&["Contont-Type: text/html".to_string()]).unwrap(),
566 );
567 let header = resp.get_response_header().unwrap();
568 assert_eq!(
569 r###"ResponseHeader { base: Parts { status: 200, version: HTTP/1.1, headers: {"contont-type": "text/html", "transfer-encoding": "chunked", "cache-control": "public, max-age=3600"} }, header_name_map: Some({"contont-type": CaseHeaderName(b"contont-type"), "transfer-encoding": CaseHeaderName(b"Transfer-Encoding"), "cache-control": CaseHeaderName(b"Cache-Control")}), reason_phrase: None }"###,
570 format!("{header:?}")
571 );
572 }
573
574 #[tokio::test]
575 async fn test_http_chunk_response_short_reads() {
576 let mut reader = tokio_test::io::Builder::new()
579 .read(b"abc")
580 .read(b"defgh")
581 .build();
582 let mut resp = HttpChunkResponse::new(&mut reader);
583 resp.chunk_size = 1024;
584 let mut buffer = BytesMut::new();
585 assert_eq!(
586 Some(Bytes::from_static(b"abc")),
587 resp.next_chunk(&mut buffer).await.unwrap()
588 );
589 assert_eq!(
590 Some(Bytes::from_static(b"defgh")),
591 resp.next_chunk(&mut buffer).await.unwrap()
592 );
593 assert_eq!(None, resp.next_chunk(&mut buffer).await.unwrap());
594 }
595
596 #[test]
597 fn test_new_cache_control_header_logic() {
598 let (name, value) = new_cache_control_header(Some(3600), Some(true));
600 assert_eq!(name, header::CACHE_CONTROL);
601 assert_eq!(value.to_str().unwrap(), "private, max-age=3600");
602
603 let (name, value) = new_cache_control_header(Some(3600), Some(false));
604 assert_eq!(name, header::CACHE_CONTROL);
605 assert_eq!(value.to_str().unwrap(), "public, max-age=3600");
606
607 let (name, value) = new_cache_control_header(Some(3600), None);
609 assert_eq!(name, header::CACHE_CONTROL);
610 assert_eq!(value.to_str().unwrap(), "public, max-age=3600");
611
612 let (name, value) = new_cache_control_header(Some(0), Some(true));
614 assert_eq!(name, header::CACHE_CONTROL);
615 assert_eq!(value, HTTP_HEADER_NO_CACHE.clone().1);
616
617 let (name, value) = new_cache_control_header(None, Some(false));
619 assert_eq!(name, header::CACHE_CONTROL);
620 assert_eq!(value, HTTP_HEADER_NO_CACHE.clone().1);
621 }
622
623 #[test]
624 fn test_http_response_builder_pattern() {
625 let etag_header = (header::ETAG, HeaderValue::from_static("\"12345\""));
627 let server_header =
628 (header::SERVER, HeaderValue::from_static("MyTestServer"));
629
630 let response = HttpResponse::builder(StatusCode::OK)
631 .body("Test Body")
632 .header(etag_header.clone())
633 .headers(vec![server_header.clone()])
634 .max_age(60, true) .finish();
636
637 assert_eq!(response.status, StatusCode::OK);
638 assert_eq!(response.body, Bytes::from("Test Body"));
639 assert_eq!(response.max_age, Some(60));
640 assert_eq!(response.cache_private, Some(true));
641
642 let headers = response.headers.unwrap();
644 assert_eq!(headers.len(), 2);
645 assert!(headers.contains(&etag_header));
646 assert!(headers.contains(&server_header));
647
648 let no_store_response = HttpResponse::builder(StatusCode::ACCEPTED)
650 .no_store()
651 .finish();
652
653 assert_eq!(no_store_response.status, StatusCode::ACCEPTED);
654 assert!(
655 no_store_response
656 .headers
657 .unwrap()
658 .contains(&HTTP_HEADER_NO_STORE.clone())
659 );
660 }
661
662 #[test]
663 fn test_http_response_error_cases() {
664 let invalid_location = "http://example.com/\0";
667 let result = HttpResponse::redirect(invalid_location);
668 assert!(result.is_err());
669 }
670
671 #[test]
672 fn test_new_response_header_generation() {
673 let resp = HttpResponse {
674 status: StatusCode::OK,
675 body: Bytes::from("Hello world!"),
676 max_age: Some(3600),
677 created_at: Some(get_super_ts().saturating_sub(10)), cache_private: Some(true),
679 headers: Some(vec![(
680 header::CONTENT_ENCODING,
681 HeaderValue::from_static("gzip"),
682 )]),
683 };
684
685 let header = resp.new_response_header().unwrap();
686 let headers_map: std::collections::HashMap<_, _> =
687 header.headers.iter().collect();
688
689 assert_eq!(header.status, StatusCode::OK);
691 assert_eq!(
692 headers_map
693 .get(&header::CONTENT_LENGTH)
694 .unwrap()
695 .to_str()
696 .unwrap(),
697 "12"
698 );
699 assert_eq!(
700 headers_map
701 .get(&header::CACHE_CONTROL)
702 .unwrap()
703 .to_str()
704 .unwrap(),
705 "private, max-age=3600"
706 );
707 assert_eq!(
708 headers_map
709 .get(&header::CONTENT_ENCODING)
710 .unwrap()
711 .to_str()
712 .unwrap(),
713 "gzip"
714 );
715 }
716}