1use bytes::{BufMut, BytesMut};
4use tokio::io::{AsyncWrite, AsyncWriteExt};
5
6use crate::error::HttpError;
7use crate::head::RequestHead;
8
9pub async fn write_request<W: AsyncWrite + Unpin>(
15 writer: &mut W,
16 head: &RequestHead,
17 body: Option<&[u8]>,
18) -> Result<(), HttpError> {
19 let mut out = BytesMut::with_capacity(512 + body.map_or(0, <[u8]>::len));
20
21 out.put_slice(head.method.as_str().as_bytes());
22 out.put_slice(b" ");
23 out.put_slice(head.target.as_bytes());
24 out.put_slice(b" HTTP/1.1\r\n");
25
26 let mut wrote_length = false;
27 if let Some(raw_headers) = &head.raw_headers {
28 for (name, value) in raw_headers {
29 if name.eq_ignore_ascii_case("content-length") {
30 wrote_length = true;
31 }
32 out.put_slice(name.as_bytes());
33 out.put_slice(b": ");
34 out.put_slice(value.as_bytes());
35 out.put_slice(b"\r\n");
36 }
37 } else {
38 for (name, value) in head.headers.iter() {
39 if name == http::header::CONTENT_LENGTH {
40 wrote_length = true;
41 }
42 out.put_slice(name.as_str().as_bytes());
43 out.put_slice(b": ");
44 out.put_slice(value.as_bytes());
45 out.put_slice(b"\r\n");
46 }
47 }
48 if !wrote_length {
49 let length = body.map_or(0, <[u8]>::len);
50 let cl_name = if head.raw_headers.is_some() {
51 b"Content-Length: ".as_slice()
52 } else {
53 b"content-length: ".as_slice()
54 };
55 out.put_slice(cl_name);
56 out.put_slice(length.to_string().as_bytes());
57 out.put_slice(b"\r\n");
58 }
59 out.put_slice(b"\r\n");
60 if let Some(body) = body {
61 out.put_slice(body);
62 }
63
64 writer.write_all(&out).await?;
65 writer.flush().await?;
66 Ok(())
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72 use http::Method;
73
74 async fn encode(head: &RequestHead, body: Option<&[u8]>) -> String {
75 let mut out = Vec::new();
76 write_request(&mut out, head, body).await.expect("write");
77 String::from_utf8(out).expect("utf-8")
78 }
79
80 #[tokio::test]
81 async fn a_request_is_self_delimiting() {
82 let head = RequestHead::new(Method::POST, "/v1/chat/completions")
83 .with_header("content-type", "application/json")
84 .with_header("authorization", "Bearer secret");
85 let text = encode(&head, Some(b"{\"stream\":true}")).await;
86 assert!(text.starts_with("POST /v1/chat/completions HTTP/1.1\r\n"));
87 assert!(text.contains("content-type: application/json\r\n"));
88 assert!(text.contains("authorization: Bearer secret\r\n"));
89 assert!(text.contains("content-length: 15\r\n"));
90 assert!(text.ends_with("\r\n\r\n{\"stream\":true}"));
91 }
92
93 #[tokio::test]
94 async fn an_absent_body_still_declares_zero_length() {
95 let head = RequestHead::new(Method::GET, "/models");
96 let text = encode(&head, None).await;
97 assert!(text.contains("content-length: 0\r\n"));
98 assert!(text.ends_with("\r\n\r\n"));
99 }
100
101 #[tokio::test]
102 async fn a_caller_supplied_length_is_not_duplicated() {
103 let head = RequestHead::new(Method::POST, "/x").with_header("content-length", "3");
104 let text = encode(&head, Some(b"abc")).await;
105 assert_eq!(text.matches("content-length").count(), 1);
106 }
107
108 #[tokio::test]
118 async fn header_order_is_what_the_caller_inserted() {
119 let head = RequestHead::new(Method::GET, "/models")
120 .with_header("user-agent", "netune/0.1")
121 .with_header("accept", "application/json")
122 .with_header("authorization", "Bearer t");
123 let text = encode(&head, None).await;
124 let body_start = text.find("\r\n\r\n").expect("head terminator");
125 let head_text = &text[..body_start];
126 let ua = head_text.find("user-agent").expect("user-agent");
127 let accept = head_text.find("accept").expect("accept");
128 let auth = head_text.find("authorization").expect("authorization");
129 assert!(
130 ua < accept && accept < auth,
131 "insertion order is wire order:\n{head_text}"
132 );
133 }
134
135 #[tokio::test]
136 async fn header_casing_is_lowercased_by_the_header_map() {
137 let head = RequestHead::new(Method::GET, "/x").with_header("X-API-Key", "k");
146 let text = encode(&head, None).await;
147 assert!(
148 text.contains("x-api-key: k\r\n"),
149 "HeaderMap lowercases; the writer does not re-case:\n{text}"
150 );
151 }
152
153 #[tokio::test]
154 async fn duplicate_headers_replace_their_predecessor_on_insert() {
155 let head = RequestHead::new(Method::GET, "/x")
162 .with_header("accept", "text/html")
163 .with_header("accept", "application/json");
164 let text = encode(&head, None).await;
165 assert!(text.contains("accept: application/json\r\n"));
166 assert!(
167 !text.contains("accept: text/html"),
168 "insert replaced the earlier value:\n{text}"
169 );
170 }
171
172 #[tokio::test]
173 async fn the_content_length_fills_in_after_the_callers_headers() {
174 let head = RequestHead::new(Method::POST, "/x")
179 .with_header("user-agent", "netune/0.1")
180 .with_header("accept", "application/json");
181 let text = encode(&head, Some(b"{}")).await;
182 let head_text = &text[..text.find("\r\n\r\n").expect("terminator")];
183 let user_agent = head_text.find("user-agent").expect("user-agent");
184 let accept = head_text.find("accept").expect("accept");
185 let length = head_text.find("content-length").expect("content-length");
186 assert!(
187 user_agent < accept && accept < length,
188 "the synthesized length closes the header block, after the caller's:\n{head_text}"
189 );
190 }
191
192 #[tokio::test]
193 async fn raw_header_casing_and_duplicates_reach_the_wire() {
194 let head = RequestHead::new(Method::GET, "/index.html")
197 .with_raw_header("User-Agent", "Mozilla/5.0")
198 .with_raw_header("Accept", "text/html")
199 .with_raw_header("Accept", "application/xhtml+xml")
200 .with_raw_header("X-Custom-Header", "Value123");
201 let text = encode(&head, None).await;
202 assert!(text.contains("User-Agent: Mozilla/5.0\r\n"));
203 assert!(text.contains("Accept: text/html\r\n"));
204 assert!(text.contains("Accept: application/xhtml+xml\r\n"));
205 assert!(text.contains("X-Custom-Header: Value123\r\n"));
206 assert!(text.contains("Content-Length: 0\r\n"));
207 }
208}