Skip to main content

pingap_core/
http_response.rs

1// Copyright 2024-2025 Tree xie.
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 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
32/// A helper function to generate a `Cache-Control` header.
33///
34/// It determines the appropriate `Cache-Control` value based on the provided
35/// `max_age` and `cache_private` settings. This is a performance-sensitive
36/// function, so it's optimized to avoid allocations where possible.
37///
38/// # Arguments
39/// * `max_age`: An `Option<u32>` specifying the max-age in seconds. If `Some(0)` or `None`,
40///   a "no-cache" header is returned.
41/// * `cache_private`: An `Option<bool>` indicating if the cache is private. Defaults to public.
42///
43/// # Returns
44/// An `HttpHeader` tuple `(HeaderName, HeaderValue)` for the `Cache-Control` header.
45fn new_cache_control_header(
46    max_age: Option<u32>,
47    cache_private: Option<bool>,
48) -> HttpHeader {
49    // Determine the max_age value. If it's 0 or not provided, return the static no-cache header immediately.
50    let max_age = match max_age {
51        Some(0) | None => return HTTP_HEADER_NO_CACHE.clone(),
52        Some(age) => age,
53    };
54
55    // Determine the cache visibility ("private" or "public").
56    let category: &[u8] = if cache_private.unwrap_or_default() {
57        b"private"
58    } else {
59        b"public"
60    };
61
62    // Use a pre-allocated buffer (`BytesMut`) and the `itoa` crate to efficiently build the
63    // header value without creating an intermediate `String` via `format!`.
64    // The capacity is estimated to prevent reallocations.
65    let mut buf = BytesMut::with_capacity(category.len() + 9 + 10); // e.g., "public, max-age=" + up to 10 digits for a u32
66    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    // Try to create a `HeaderValue` from the constructed bytes.
71    if let Ok(value) = HeaderValue::from_bytes(&buf) {
72        return (header::CACHE_CONTROL, value);
73    }
74    // If creation fails for any reason, fall back to the safe "no-cache" header.
75    HTTP_HEADER_NO_CACHE.clone()
76}
77
78/// A builder for creating `HttpResponse` instances fluently using the builder pattern.
79///
80/// This allows for chaining method calls to configure a response, improving readability.
81#[derive(Default, Debug)]
82pub struct HttpResponseBuilder {
83    /// The `HttpResponse` instance being built.
84    response: HttpResponse,
85}
86
87impl HttpResponseBuilder {
88    /// Creates a new builder with a given status code.
89    pub fn new(status: StatusCode) -> Self {
90        Self {
91            response: HttpResponse {
92                status,
93                ..Default::default()
94            },
95        }
96    }
97
98    /// Sets the response body.
99    ///
100    /// This method is generic over `impl Into<Bytes>`, allowing various types like
101    /// `Vec<u8>`, `String`, or `&'static str` to be passed as the body.
102    pub fn body(mut self, body: impl Into<Bytes>) -> Self {
103        self.response.body = body.into();
104        self
105    }
106
107    /// Adds a single HTTP header to the response.
108    ///
109    /// If the headers vector doesn't exist yet, it will be created.
110    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    /// Appends multiple HTTP headers to the response.
119    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    /// Sets the `Cache-Control` max-age and privacy directive.
128    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    /// A convenience method to add a "no-store" `Cache-Control` header.
135    pub fn no_store(self) -> Self {
136        self.header(HTTP_HEADER_NO_STORE.clone())
137    }
138
139    /// Consumes the builder and returns the final `HttpResponse` instance.
140    pub fn finish(self) -> HttpResponse {
141        self.response
142    }
143}
144
145/// Represents a complete HTTP response, including status, headers, and a body.
146/// This struct is used for responses where the entire body is known upfront.
147#[derive(Default, Clone, Debug)]
148pub struct HttpResponse {
149    /// The HTTP status code (e.g., 200 OK, 404 Not Found).
150    pub status: StatusCode,
151    /// The response body, stored as a `Bytes` object for efficiency.
152    pub body: Bytes,
153    /// The `max-age` directive for the `Cache-Control` header, in seconds.
154    pub max_age: Option<u32>,
155    /// The UNIX timestamp when the response content was created, used for the `Age` header.
156    pub created_at: Option<u32>,
157    /// A flag to indicate if the `Cache-Control` should be "private" (true) or "public" (false).
158    pub cache_private: Option<bool>,
159    /// A list of additional HTTP headers to include in the response.
160    pub headers: Option<Vec<HttpHeader>>,
161}
162
163impl HttpResponse {
164    /// Returns a new `HttpResponseBuilder` to start building a response.
165    pub fn builder(status: StatusCode) -> HttpResponseBuilder {
166        HttpResponseBuilder::new(status)
167    }
168
169    /// A convenience constructor for a 204 No Content response.
170    pub fn no_content() -> Self {
171        Self::builder(StatusCode::NO_CONTENT).no_store().finish()
172    }
173
174    /// A convenience constructor for a 400 Bad Request response.
175    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    /// A convenience constructor for a 404 Not Found response.
184    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    /// A convenience constructor for a 500 Internal Server Error response.
193    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    /// A convenience constructor for a 200 OK HTML response.
202    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    /// A convenience constructor for a 302 Temporary Redirect response.
211    pub fn redirect(location: &str) -> pingora::Result<Self> {
212        // Attempt to parse the location string into a valid HeaderValue.
213        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        // Build the redirect response.
218        Ok(Self::builder(StatusCode::TEMPORARY_REDIRECT)
219            .header((header::LOCATION, value))
220            .header(HTTP_HEADER_NO_CACHE.clone())
221            .finish())
222    }
223
224    /// A convenience constructor for a 200 OK plain text response.
225    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    /// Creates a 200 OK JSON response by serializing the given value.
234    pub fn try_from_json<T>(value: &T) -> pingora::Result<Self>
235    where
236        T: ?Sized + Serialize,
237    {
238        // Serialize the value to a JSON byte vector.
239        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        // Build the JSON response.
244        Ok(Self::builder(StatusCode::OK)
245            .body(buf)
246            .header(HTTP_HEADER_CONTENT_JSON.clone())
247            .finish())
248    }
249
250    /// Creates a JSON response with a specified status code by serializing the given value.
251    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        // First, create a standard 200 OK JSON response.
259        let mut resp = Self::try_from_json(value)?;
260        // Then, simply overwrite the status code.
261        resp.status = status;
262        Ok(resp)
263    }
264
265    /// Builds a `pingora::http::ResponseHeader` from the `HttpResponse`'s properties.
266    pub fn new_response_header(&self) -> pingora::Result<ResponseHeader> {
267        // Build the response header with the status code, sized for the
268        // headers added below: Content-Length, Cache-Control, Age, plus
269        // the custom ones.
270        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        // A local helper closure to simplify adding headers and handling potential errors.
274        let mut add_header =
275            |name: &HeaderName, value: &HeaderValue| -> pingora::Result<()> {
276                resp.insert_header(name, value)?;
277                Ok(())
278            };
279
280        // Add the Content-Length header based on the body size.
281        add_header(
282            &header::CONTENT_LENGTH,
283            &HeaderValue::from(self.body.len()),
284        )?;
285
286        // Generate and add the Cache-Control header.
287        let (name, value) =
288            new_cache_control_header(self.max_age, self.cache_private);
289        add_header(&name, &value)?;
290
291        // If a creation timestamp is provided, calculate and add the Age header.
292        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        // Add all custom headers from the `headers` field.
298        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    /// Sends the entire HTTP response (header and body) to the client via the session.
307    pub async fn send(self, session: &mut Session) -> pingora::Result<usize> {
308        // First, build the response header.
309        let header = self.new_response_header()?;
310        let size = self.body.len();
311        // Write the header to the session.
312        session
313            .write_response_header(Box::new(header), false)
314            .await?;
315        // Write the body to the session. `end_stream` is true because this is the final body part.
316        session.write_response_body(Some(self.body), true).await?;
317        // Finalize the response stream.
318        session.finish_body().await?;
319        Ok(size)
320    }
321}
322
323/// Represents a chunked HTTP response for streaming large bodies of data.
324///
325/// This is used when the response body is too large to fit in memory or is generated on-the-fly.
326pub struct HttpChunkResponse<'r, R> {
327    /// A pinned, mutable reference to an async reader that provides the body data.
328    pub reader: Pin<&'r mut R>,
329    /// The suggested size for each data chunk. Defaults to `DEFAULT_BUF_SIZE`.
330    pub chunk_size: usize,
331    /// Cache control `max-age` setting for the response.
332    pub max_age: Option<u32>,
333    /// Cache control privacy setting for the response.
334    pub cache_private: Option<bool>,
335    /// Additional headers to include in the response.
336    pub headers: Option<Vec<HttpHeader>>,
337}
338
339/// The default buffer size (8KB) for chunked responses.
340const DEFAULT_BUF_SIZE: usize = 8 * 1024;
341
342impl<'r, R> HttpChunkResponse<'r, R>
343where
344    // The reader must implement `AsyncRead` and be `Unpin`.
345    R: tokio::io::AsyncRead + std::marker::Unpin,
346{
347    /// Creates a new `HttpChunkResponse` with a given reader and default settings.
348    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    /// Builds the `ResponseHeader` for the chunked response.
359    ///
360    /// This will include a `Transfer-Encoding: chunked` header.
361    pub fn get_response_header(&self) -> pingora::Result<ResponseHeader> {
362        // Start building a 200 OK response header.
363        let mut resp = ResponseHeader::build(StatusCode::OK, Some(4))?;
364        // Add any custom headers.
365        if let Some(headers) = &self.headers {
366            for (name, value) in headers {
367                resp.insert_header(name.to_owned(), value)?;
368            }
369        }
370
371        // Add the mandatory `Transfer-Encoding: chunked` header.
372        let chunked = HTTP_HEADER_TRANSFER_CHUNKED.clone();
373        resp.insert_header(chunked.0, chunked.1)?;
374
375        // Add the `Cache-Control` header.
376        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    /// Reads the next chunk of the body. `None` only at the end of the
383    /// stream: a read that comes back short is a chunk like any other, not a
384    /// sign that the reader is done - `AsyncRead` never promised to fill the
385    /// buffer.
386    ///
387    /// The chunk is split off `buffer` rather than copied out of it; once
388    /// the session has written it and dropped it, `reserve` gets the
389    /// allocation back for the next read.
390    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    /// Streams the data from the reader to the client as a chunked response.
406    ///
407    /// Reads data from the `reader` in chunks and sends each chunk to the client until
408    /// the reader is exhausted.
409    pub async fn send(
410        mut self,
411        session: &mut Session,
412    ) -> pingora::Result<usize> {
413        // First, build and send the response headers. `end_stream` is false because a body will follow.
414        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        // One chunk of lookahead, so the last one goes out flagged as the end
422        // of the stream.
423        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        // Finalize the response stream.
432        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        // A reader that hands out less than a chunk at a time. Only an empty
577        // read ends the body; a short one used to, truncating the file.
578        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        // Test max_age > 0 case
599        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        // Test None is equivalent to public
608        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        // Test max_age = 0, should return no-cache
613        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        // Test max_age = None, should return no-cache
618        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        // Create a custom Header
626        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) // 60 seconds, private
635            .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        // Verify headers are correctly added
643        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        // Test no_store in the chain call
649        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        // Test redirect error cases (invalid location)
665        // String containing \0 characters is invalid HeaderValue
666        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)), // 模拟10秒前创建
678            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        // 验证基本 headers
690        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}