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.
268        let mut resp = ResponseHeader::build(self.status, None)?;
269
270        // A local helper closure to simplify adding headers and handling potential errors.
271        let mut add_header =
272            |name: &HeaderName, value: &HeaderValue| -> pingora::Result<()> {
273                resp.insert_header(name, value)?;
274                Ok(())
275            };
276
277        // Add the Content-Length header based on the body size.
278        add_header(
279            &header::CONTENT_LENGTH,
280            &HeaderValue::from(self.body.len()),
281        )?;
282
283        // Generate and add the Cache-Control header.
284        let (name, value) =
285            new_cache_control_header(self.max_age, self.cache_private);
286        add_header(&name, &value)?;
287
288        // If a creation timestamp is provided, calculate and add the Age header.
289        if let Some(created_at) = self.created_at {
290            let secs = get_super_ts().saturating_sub(created_at);
291            add_header(&header::AGE, &HeaderValue::from(secs))?;
292        }
293
294        // Add all custom headers from the `headers` field.
295        if let Some(headers) = &self.headers {
296            for (name, value) in headers {
297                add_header(name, value)?;
298            }
299        }
300        Ok(resp)
301    }
302
303    /// Sends the entire HTTP response (header and body) to the client via the session.
304    pub async fn send(self, session: &mut Session) -> pingora::Result<usize> {
305        // First, build the response header.
306        let header = self.new_response_header()?;
307        let size = self.body.len();
308        // Write the header to the session.
309        session
310            .write_response_header(Box::new(header), false)
311            .await?;
312        // Write the body to the session. `end_stream` is true because this is the final body part.
313        session.write_response_body(Some(self.body), true).await?;
314        // Finalize the response stream.
315        session.finish_body().await?;
316        Ok(size)
317    }
318}
319
320/// Represents a chunked HTTP response for streaming large bodies of data.
321///
322/// This is used when the response body is too large to fit in memory or is generated on-the-fly.
323pub struct HttpChunkResponse<'r, R> {
324    /// A pinned, mutable reference to an async reader that provides the body data.
325    pub reader: Pin<&'r mut R>,
326    /// The suggested size for each data chunk. Defaults to `DEFAULT_BUF_SIZE`.
327    pub chunk_size: usize,
328    /// Cache control `max-age` setting for the response.
329    pub max_age: Option<u32>,
330    /// Cache control privacy setting for the response.
331    pub cache_private: Option<bool>,
332    /// Additional headers to include in the response.
333    pub headers: Option<Vec<HttpHeader>>,
334}
335
336/// The default buffer size (8KB) for chunked responses.
337const DEFAULT_BUF_SIZE: usize = 8 * 1024;
338
339impl<'r, R> HttpChunkResponse<'r, R>
340where
341    // The reader must implement `AsyncRead` and be `Unpin`.
342    R: tokio::io::AsyncRead + std::marker::Unpin,
343{
344    /// Creates a new `HttpChunkResponse` with a given reader and default settings.
345    pub fn new(r: &'r mut R) -> Self {
346        Self {
347            reader: Pin::new(r),
348            chunk_size: DEFAULT_BUF_SIZE,
349            max_age: None,
350            headers: None,
351            cache_private: None,
352        }
353    }
354
355    /// Builds the `ResponseHeader` for the chunked response.
356    ///
357    /// This will include a `Transfer-Encoding: chunked` header.
358    pub fn get_response_header(&self) -> pingora::Result<ResponseHeader> {
359        // Start building a 200 OK response header.
360        let mut resp = ResponseHeader::build(StatusCode::OK, Some(4))?;
361        // Add any custom headers.
362        if let Some(headers) = &self.headers {
363            for (name, value) in headers {
364                resp.insert_header(name.to_owned(), value)?;
365            }
366        }
367
368        // Add the mandatory `Transfer-Encoding: chunked` header.
369        let chunked = HTTP_HEADER_TRANSFER_CHUNKED.clone();
370        resp.insert_header(chunked.0, chunked.1)?;
371
372        // Add the `Cache-Control` header.
373        let cache_control =
374            new_cache_control_header(self.max_age, self.cache_private);
375        resp.insert_header(cache_control.0, cache_control.1)?;
376        Ok(resp)
377    }
378
379    /// Streams the data from the reader to the client as a chunked response.
380    ///
381    /// Reads data from the `reader` in chunks and sends each chunk to the client until
382    /// the reader is exhausted.
383    pub async fn send(
384        mut self,
385        session: &mut Session,
386    ) -> pingora::Result<usize> {
387        // First, build and send the response headers. `end_stream` is false because a body will follow.
388        let header = self.get_response_header()?;
389        session
390            .write_response_header(Box::new(header), false)
391            .await?;
392
393        let mut sent = 0;
394        // Ensure the chunk size is not too small.
395        let chunk_size = self.chunk_size.max(512);
396        // Create a reusable buffer for reading data into.
397        let mut buffer = vec![0; chunk_size];
398        loop {
399            // Read a chunk of data from the source reader.
400            let size = self.reader.read(&mut buffer).await.map_err(|e| {
401                error!(error = e.to_string(), "read data fail");
402                new_internal_error(400, e)
403            })?;
404            // Determine if this is the final chunk.
405            let end = size < chunk_size;
406            // Write the chunk to the response body.
407            session
408                .write_response_body(
409                    // `copy_from_slice` is necessary because `write_response_body` takes an `Option<Bytes>`.
410                    Some(Bytes::copy_from_slice(&buffer[..size])),
411                    end,
412                )
413                .await?;
414            sent += size;
415            // If it was the last chunk, exit the loop.
416            if end {
417                break;
418            }
419        }
420        // Finalize the response stream.
421        session.finish_body().await?;
422
423        Ok(sent)
424    }
425}
426#[cfg(test)]
427mod tests {
428    use super::*;
429    use crate::convert_headers;
430    use bytes::Bytes;
431    use http::StatusCode;
432    use pretty_assertions::assert_eq;
433    use serde::Serialize;
434    use std::io::Write;
435    use tempfile::NamedTempFile;
436    use tokio::fs;
437    #[test]
438    fn test_new_cache_control_header() {
439        assert_eq!(
440            r###"("cache-control", "private, max-age=3600")"###,
441            format!("{:?}", new_cache_control_header(Some(3600), Some(true)))
442        );
443        assert_eq!(
444            r###"("cache-control", "public, max-age=3600")"###,
445            format!("{:?}", new_cache_control_header(Some(3600), None))
446        );
447        assert_eq!(
448            r###"("cache-control", "private, no-cache")"###,
449            format!("{:?}", new_cache_control_header(Some(0), Some(false)))
450        );
451        assert_eq!(
452            r###"("cache-control", "private, no-cache")"###,
453            format!("{:?}", new_cache_control_header(None, None))
454        );
455    }
456
457    #[test]
458    fn test_http_response() {
459        assert_eq!(
460            r###"HttpResponse { status: 204, body: b"", max_age: None, created_at: None, cache_private: None, headers: Some([("cache-control", "private, no-store")]) }"###,
461            format!("{:?}", HttpResponse::no_content())
462        );
463        assert_eq!(
464            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")]) }"###,
465            format!("{:?}", HttpResponse::not_found("Not Found"))
466        );
467        assert_eq!(
468            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")]) }"###,
469            format!("{:?}", HttpResponse::unknown_error("Unknown Error"))
470        );
471
472        assert_eq!(
473            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")]) }"###,
474            format!("{:?}", HttpResponse::bad_request("Bad Request"))
475        );
476
477        assert_eq!(
478            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")]) }"###,
479            format!("{:?}", HttpResponse::html("<p>Pingap</p>"))
480        );
481
482        assert_eq!(
483            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")]) }"###,
484            format!(
485                "{:?}",
486                HttpResponse::redirect("http://example.com/").unwrap()
487            )
488        );
489
490        assert_eq!(
491            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")]) }"###,
492            format!("{:?}", HttpResponse::text("Hello World!"))
493        );
494
495        #[derive(Serialize)]
496        struct Data {
497            message: String,
498        }
499        let resp = HttpResponse::try_from_json_status(
500            &Data {
501                message: "Hello World!".to_string(),
502            },
503            StatusCode::BAD_REQUEST,
504        )
505        .unwrap();
506        assert_eq!(
507            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")]) }"###,
508            format!("{resp:?}")
509        );
510        let resp = HttpResponse::try_from_json(&Data {
511            message: "Hello World!".to_string(),
512        })
513        .unwrap();
514        assert_eq!(
515            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")]) }"###,
516            format!("{resp:?}")
517        );
518
519        let resp = HttpResponse {
520            status: StatusCode::OK,
521            body: Bytes::from("Hello world!"),
522            max_age: Some(3600),
523            created_at: Some(0),
524            cache_private: Some(true),
525            headers: Some(
526                convert_headers(&[
527                    "Contont-Type: application/json".to_string(),
528                    "Content-Encoding: gzip".to_string(),
529                ])
530                .unwrap(),
531            ),
532        };
533
534        let mut header = resp.new_response_header().unwrap();
535        assert_eq!(true, !header.headers.get("Age").unwrap().is_empty());
536        header.remove_header("Age").unwrap();
537
538        assert_eq!(
539            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 }"###,
540            format!("{header:?}")
541        );
542    }
543
544    #[tokio::test]
545    async fn test_http_chunk_response() {
546        let file = include_bytes!("../../error.html");
547        let mut f = NamedTempFile::new().unwrap();
548        f.write_all(file).unwrap();
549        let mut f = fs::OpenOptions::new().read(true).open(f).await.unwrap();
550        let mut resp = HttpChunkResponse::new(&mut f);
551        resp.max_age = Some(3600);
552        resp.cache_private = Some(false);
553        resp.headers = Some(
554            convert_headers(&["Contont-Type: text/html".to_string()]).unwrap(),
555        );
556        let header = resp.get_response_header().unwrap();
557        assert_eq!(
558            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 }"###,
559            format!("{header:?}")
560        );
561    }
562
563    #[test]
564    fn test_new_cache_control_header_logic() {
565        // Test max_age > 0 case
566        let (name, value) = new_cache_control_header(Some(3600), Some(true));
567        assert_eq!(name, header::CACHE_CONTROL);
568        assert_eq!(value.to_str().unwrap(), "private, max-age=3600");
569
570        let (name, value) = new_cache_control_header(Some(3600), Some(false));
571        assert_eq!(name, header::CACHE_CONTROL);
572        assert_eq!(value.to_str().unwrap(), "public, max-age=3600");
573
574        // Test None is equivalent to public
575        let (name, value) = new_cache_control_header(Some(3600), None);
576        assert_eq!(name, header::CACHE_CONTROL);
577        assert_eq!(value.to_str().unwrap(), "public, max-age=3600");
578
579        // Test max_age = 0, should return no-cache
580        let (name, value) = new_cache_control_header(Some(0), Some(true));
581        assert_eq!(name, header::CACHE_CONTROL);
582        assert_eq!(value, HTTP_HEADER_NO_CACHE.clone().1);
583
584        // Test max_age = None, should return no-cache
585        let (name, value) = new_cache_control_header(None, Some(false));
586        assert_eq!(name, header::CACHE_CONTROL);
587        assert_eq!(value, HTTP_HEADER_NO_CACHE.clone().1);
588    }
589
590    #[test]
591    fn test_http_response_builder_pattern() {
592        // Create a custom Header
593        let etag_header = (header::ETAG, HeaderValue::from_static("\"12345\""));
594        let server_header =
595            (header::SERVER, HeaderValue::from_static("MyTestServer"));
596
597        let response = HttpResponse::builder(StatusCode::OK)
598            .body("Test Body")
599            .header(etag_header.clone())
600            .headers(vec![server_header.clone()])
601            .max_age(60, true) // 60 seconds, private
602            .finish();
603
604        assert_eq!(response.status, StatusCode::OK);
605        assert_eq!(response.body, Bytes::from("Test Body"));
606        assert_eq!(response.max_age, Some(60));
607        assert_eq!(response.cache_private, Some(true));
608
609        // Verify headers are correctly added
610        let headers = response.headers.unwrap();
611        assert_eq!(headers.len(), 2);
612        assert!(headers.contains(&etag_header));
613        assert!(headers.contains(&server_header));
614
615        // Test no_store in the chain call
616        let no_store_response = HttpResponse::builder(StatusCode::ACCEPTED)
617            .no_store()
618            .finish();
619
620        assert_eq!(no_store_response.status, StatusCode::ACCEPTED);
621        assert!(
622            no_store_response
623                .headers
624                .unwrap()
625                .contains(&HTTP_HEADER_NO_STORE.clone())
626        );
627    }
628
629    #[test]
630    fn test_http_response_error_cases() {
631        // Test redirect error cases (invalid location)
632        // String containing \0 characters is invalid HeaderValue
633        let invalid_location = "http://example.com/\0";
634        let result = HttpResponse::redirect(invalid_location);
635        assert!(result.is_err());
636    }
637
638    #[test]
639    fn test_new_response_header_generation() {
640        let resp = HttpResponse {
641            status: StatusCode::OK,
642            body: Bytes::from("Hello world!"),
643            max_age: Some(3600),
644            created_at: Some(get_super_ts().saturating_sub(10)), // 模拟10秒前创建
645            cache_private: Some(true),
646            headers: Some(vec![(
647                header::CONTENT_ENCODING,
648                HeaderValue::from_static("gzip"),
649            )]),
650        };
651
652        let header = resp.new_response_header().unwrap();
653        let headers_map: std::collections::HashMap<_, _> =
654            header.headers.iter().collect();
655
656        // 验证基本 headers
657        assert_eq!(header.status, StatusCode::OK);
658        assert_eq!(
659            headers_map
660                .get(&header::CONTENT_LENGTH)
661                .unwrap()
662                .to_str()
663                .unwrap(),
664            "12"
665        );
666        assert_eq!(
667            headers_map
668                .get(&header::CACHE_CONTROL)
669                .unwrap()
670                .to_str()
671                .unwrap(),
672            "private, max-age=3600"
673        );
674        assert_eq!(
675            headers_map
676                .get(&header::CONTENT_ENCODING)
677                .unwrap()
678                .to_str()
679                .unwrap(),
680            "gzip"
681        );
682    }
683}