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}
18
19impl RequestHead {
20    pub fn new(method: Method, target: impl Into<String>) -> Self {
21        Self {
22            method,
23            target: target.into(),
24            headers: HeaderMap::new(),
25        }
26    }
27
28    /// Insert a header. `expect`-free: an invalid name or value is a programming
29    /// error in the caller's own literal, and is reported as `false` rather than
30    /// panicking.
31    pub fn with_header(mut self, name: impl AsRef<str>, value: impl AsRef<str>) -> Self {
32        if let (Ok(name), Ok(value)) = (
33            http::header::HeaderName::try_from(name.as_ref()),
34            http::header::HeaderValue::try_from(value.as_ref()),
35        ) {
36            self.headers.insert(name, value);
37        }
38        self
39    }
40}
41
42/// How the response body is framed.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum BodyKind {
45    /// No body at all: `HEAD`, `1xx`, `204`, `304`.
46    Empty,
47    /// Exactly this many bytes.
48    Length(u64),
49    /// `Transfer-Encoding: chunked`.
50    Chunked,
51    /// Delimited by connection close (no length, no chunking).
52    UntilClose,
53}
54
55/// A parsed response head.
56#[derive(Debug, Clone)]
57pub struct ResponseHead {
58    pub version: Version,
59    pub status: StatusCode,
60    pub headers: HeaderMap,
61    pub body: BodyKind,
62}
63
64impl ResponseHead {
65    /// `true` when the status carries no body by definition.
66    pub fn status_forbids_body(status: StatusCode) -> bool {
67        status.is_informational()
68            || status == StatusCode::NO_CONTENT
69            || status == StatusCode::NOT_MODIFIED
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn request_head_writes_headers_it_was_given() {
79        let head = RequestHead::new(Method::POST, "/v1/chat/completions")
80            .with_header("content-type", "application/json")
81            .with_header("authorization", "Bearer secret");
82        assert_eq!(head.target, "/v1/chat/completions");
83        assert_eq!(
84            head.headers
85                .get("content-type")
86                .map(|v| v.to_str().unwrap()),
87            Some("application/json")
88        );
89    }
90
91    #[test]
92    fn no_body_statuses_are_recognized() {
93        assert!(ResponseHead::status_forbids_body(StatusCode::NO_CONTENT));
94        assert!(ResponseHead::status_forbids_body(StatusCode::NOT_MODIFIED));
95        assert!(ResponseHead::status_forbids_body(StatusCode::CONTINUE));
96        assert!(!ResponseHead::status_forbids_body(StatusCode::OK));
97        assert!(!ResponseHead::status_forbids_body(StatusCode::BAD_REQUEST));
98    }
99}