Skip to main content

netune_http1/
head.rs

1//! Request and response message heads.
2
3use http::{HeaderMap, Method, StatusCode, Version};
4
5/// A request head: method, origin-form target and headers.
6///
7/// The body is passed separately to [`crate::write_request`] so the caller
8/// controls its length; a `Content-Length` header is written for a present body
9/// and never for an absent one.
10#[derive(Debug, Clone)]
11pub struct RequestHead {
12    pub method: Method,
13    /// Origin-form target (`/v1/chat/completions`); the authority lives in the
14    /// connection, not the message.
15    pub target: String,
16    pub headers: HeaderMap,
17    /// Explicit raw wire headers preserving caller-defined casing and order.
18    /// When `Some`, [`crate::write_request`] serializes these exact bytes
19    /// instead of lowercasing through [`http::HeaderMap`].
20    pub raw_headers: Option<Vec<(String, String)>>,
21}
22
23impl RequestHead {
24    pub fn new(method: Method, target: impl Into<String>) -> Self {
25        Self {
26            method,
27            target: target.into(),
28            headers: HeaderMap::new(),
29            raw_headers: None,
30        }
31    }
32
33    /// Insert a header. `expect`-free: an invalid name or value is a programming
34    /// error in the caller's own literal, and is reported as `false` rather than
35    /// panicking.
36    pub fn with_header(mut self, name: impl AsRef<str>, value: impl AsRef<str>) -> Self {
37        if let (Ok(name), Ok(value)) = (
38            http::header::HeaderName::try_from(name.as_ref()),
39            http::header::HeaderValue::try_from(value.as_ref()),
40        ) {
41            self.headers.insert(name, value);
42        }
43        self
44    }
45
46    /// Append a raw header preserving caller's exact name casing and duplicates.
47    ///
48    /// This directly addresses HTTP-layer fingerprinting (JA4H) and WAF/DPI evasion:
49    /// wire casing (e.g. `User-Agent`, `Host`, `Sec-CH-UA`) and multi-value
50    /// repetition (e.g. repeated `Cookie` or `Set-Cookie`) are emitted byte-for-byte.
51    pub fn with_raw_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
52        let name_str = name.into();
53        let value_str = value.into();
54        if let (Ok(hname), Ok(hval)) = (
55            http::header::HeaderName::try_from(name_str.as_str()),
56            http::header::HeaderValue::try_from(value_str.as_str()),
57        ) {
58            self.headers.append(hname, hval);
59        }
60        let raw = self.raw_headers.get_or_insert_with(Vec::new);
61        raw.push((name_str, value_str));
62        self
63    }
64}
65
66/// How the response body is framed.
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum BodyKind {
69    /// No body at all: `HEAD`, `1xx`, `204`, `304`.
70    Empty,
71    /// Exactly this many bytes.
72    Length(u64),
73    /// `Transfer-Encoding: chunked`.
74    Chunked,
75    /// Delimited by connection close (no length, no chunking).
76    UntilClose,
77}
78
79/// A parsed response head.
80#[derive(Debug, Clone)]
81pub struct ResponseHead {
82    pub version: Version,
83    pub status: StatusCode,
84    pub headers: HeaderMap,
85    pub body: BodyKind,
86}
87
88impl ResponseHead {
89    /// `true` when the status carries no body by definition.
90    pub fn status_forbids_body(status: StatusCode) -> bool {
91        status.is_informational()
92            || status == StatusCode::NO_CONTENT
93            || status == StatusCode::NOT_MODIFIED
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn request_head_writes_headers_it_was_given() {
103        let head = RequestHead::new(Method::POST, "/v1/chat/completions")
104            .with_header("content-type", "application/json")
105            .with_header("authorization", "Bearer secret");
106        assert_eq!(head.target, "/v1/chat/completions");
107        assert_eq!(
108            head.headers
109                .get("content-type")
110                .map(|v| v.to_str().unwrap()),
111            Some("application/json")
112        );
113    }
114
115    #[test]
116    fn no_body_statuses_are_recognized() {
117        assert!(ResponseHead::status_forbids_body(StatusCode::NO_CONTENT));
118        assert!(ResponseHead::status_forbids_body(StatusCode::NOT_MODIFIED));
119        assert!(ResponseHead::status_forbids_body(StatusCode::CONTINUE));
120        assert!(!ResponseHead::status_forbids_body(StatusCode::OK));
121        assert!(!ResponseHead::status_forbids_body(StatusCode::BAD_REQUEST));
122    }
123}