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 for (name, value) in head.headers.iter() {
28 if name == http::header::CONTENT_LENGTH {
29 wrote_length = true;
30 }
31 out.put_slice(name.as_str().as_bytes());
32 out.put_slice(b": ");
33 out.put_slice(value.as_bytes());
34 out.put_slice(b"\r\n");
35 }
36 if !wrote_length {
37 let length = body.map_or(0, <[u8]>::len);
38 out.put_slice(b"content-length: ");
39 out.put_slice(length.to_string().as_bytes());
40 out.put_slice(b"\r\n");
41 }
42 out.put_slice(b"\r\n");
43 if let Some(body) = body {
44 out.put_slice(body);
45 }
46
47 writer.write_all(&out).await?;
48 writer.flush().await?;
49 Ok(())
50}
51
52#[cfg(test)]
53mod tests {
54 use super::*;
55 use http::Method;
56
57 async fn encode(head: &RequestHead, body: Option<&[u8]>) -> String {
58 let mut out = Vec::new();
59 write_request(&mut out, head, body).await.expect("write");
60 String::from_utf8(out).expect("utf-8")
61 }
62
63 #[tokio::test]
64 async fn a_request_is_self_delimiting() {
65 let head = RequestHead::new(Method::POST, "/v1/chat/completions")
66 .with_header("content-type", "application/json")
67 .with_header("authorization", "Bearer secret");
68 let text = encode(&head, Some(b"{\"stream\":true}")).await;
69 assert!(text.starts_with("POST /v1/chat/completions HTTP/1.1\r\n"));
70 assert!(text.contains("content-type: application/json\r\n"));
71 assert!(text.contains("authorization: Bearer secret\r\n"));
72 assert!(text.contains("content-length: 15\r\n"));
73 assert!(text.ends_with("\r\n\r\n{\"stream\":true}"));
74 }
75
76 #[tokio::test]
77 async fn an_absent_body_still_declares_zero_length() {
78 let head = RequestHead::new(Method::GET, "/models");
79 let text = encode(&head, None).await;
80 assert!(text.contains("content-length: 0\r\n"));
81 assert!(text.ends_with("\r\n\r\n"));
82 }
83
84 #[tokio::test]
85 async fn a_caller_supplied_length_is_not_duplicated() {
86 let head = RequestHead::new(Method::POST, "/x").with_header("content-length", "3");
87 let text = encode(&head, Some(b"abc")).await;
88 assert_eq!(text.matches("content-length").count(), 1);
89 }
90
91 #[tokio::test]
101 async fn header_order_is_what_the_caller_inserted() {
102 let head = RequestHead::new(Method::GET, "/models")
103 .with_header("user-agent", "netune/0.1")
104 .with_header("accept", "application/json")
105 .with_header("authorization", "Bearer t");
106 let text = encode(&head, None).await;
107 let body_start = text.find("\r\n\r\n").expect("head terminator");
108 let head_text = &text[..body_start];
109 let ua = head_text.find("user-agent").expect("user-agent");
110 let accept = head_text.find("accept").expect("accept");
111 let auth = head_text.find("authorization").expect("authorization");
112 assert!(
113 ua < accept && accept < auth,
114 "insertion order is wire order:\n{head_text}"
115 );
116 }
117
118 #[tokio::test]
119 async fn header_casing_is_lowercased_by_the_header_map() {
120 let head = RequestHead::new(Method::GET, "/x").with_header("X-API-Key", "k");
129 let text = encode(&head, None).await;
130 assert!(
131 text.contains("x-api-key: k\r\n"),
132 "HeaderMap lowercases; the writer does not re-case:\n{text}"
133 );
134 }
135
136 #[tokio::test]
137 async fn duplicate_headers_replace_their_predecessor_on_insert() {
138 let head = RequestHead::new(Method::GET, "/x")
145 .with_header("accept", "text/html")
146 .with_header("accept", "application/json");
147 let text = encode(&head, None).await;
148 assert!(text.contains("accept: application/json\r\n"));
149 assert!(
150 !text.contains("accept: text/html"),
151 "insert replaced the earlier value:\n{text}"
152 );
153 }
154
155 #[tokio::test]
156 async fn the_content_length_fills_in_after_the_callers_headers() {
157 let head = RequestHead::new(Method::POST, "/x")
162 .with_header("user-agent", "netune/0.1")
163 .with_header("accept", "application/json");
164 let text = encode(&head, Some(b"{}")).await;
165 let head_text = &text[..text.find("\r\n\r\n").expect("terminator")];
166 let user_agent = head_text.find("user-agent").expect("user-agent");
167 let accept = head_text.find("accept").expect("accept");
168 let length = head_text.find("content-length").expect("content-length");
169 assert!(
170 user_agent < accept && accept < length,
171 "the synthesized length closes the header block, after the caller's:\n{head_text}"
172 );
173 }
174}