1use bytes::{BufMut, Bytes, BytesMut};
39use std::borrow::Cow;
40use std::collections::HashMap;
41use std::io::IoSlice;
42
43pub const MAX_IO_SLICES: usize = 16;
47
48static STATUS_200: &[u8] = b"HTTP/1.1 200 OK\r\n";
50static STATUS_201: &[u8] = b"HTTP/1.1 201 Created\r\n";
51static STATUS_204: &[u8] = b"HTTP/1.1 204 No Content\r\n";
52static STATUS_301: &[u8] = b"HTTP/1.1 301 Moved Permanently\r\n";
53static STATUS_302: &[u8] = b"HTTP/1.1 302 Found\r\n";
54static STATUS_304: &[u8] = b"HTTP/1.1 304 Not Modified\r\n";
55static STATUS_400: &[u8] = b"HTTP/1.1 400 Bad Request\r\n";
56static STATUS_401: &[u8] = b"HTTP/1.1 401 Unauthorized\r\n";
57static STATUS_403: &[u8] = b"HTTP/1.1 403 Forbidden\r\n";
58static STATUS_404: &[u8] = b"HTTP/1.1 404 Not Found\r\n";
59static STATUS_405: &[u8] = b"HTTP/1.1 405 Method Not Allowed\r\n";
60static STATUS_500: &[u8] = b"HTTP/1.1 500 Internal Server Error\r\n";
61static STATUS_502: &[u8] = b"HTTP/1.1 502 Bad Gateway\r\n";
62static STATUS_503: &[u8] = b"HTTP/1.1 503 Service Unavailable\r\n";
63
64static HEADER_SEP: &[u8] = b": ";
66static CRLF: &[u8] = b"\r\n";
68
69#[inline]
74pub fn status_line(status: u16) -> Cow<'static, [u8]> {
75 match status {
76 200 => Cow::Borrowed(STATUS_200),
77 201 => Cow::Borrowed(STATUS_201),
78 204 => Cow::Borrowed(STATUS_204),
79 301 => Cow::Borrowed(STATUS_301),
80 302 => Cow::Borrowed(STATUS_302),
81 304 => Cow::Borrowed(STATUS_304),
82 400 => Cow::Borrowed(STATUS_400),
83 401 => Cow::Borrowed(STATUS_401),
84 403 => Cow::Borrowed(STATUS_403),
85 404 => Cow::Borrowed(STATUS_404),
86 405 => Cow::Borrowed(STATUS_405),
87 500 => Cow::Borrowed(STATUS_500),
88 502 => Cow::Borrowed(STATUS_502),
89 503 => Cow::Borrowed(STATUS_503),
90 _ => {
91 let mut buf = BytesMut::with_capacity(32);
92 format_status_line(status, &mut buf);
93 Cow::Owned(buf.to_vec())
94 }
95 }
96}
97
98#[inline]
100pub fn has_precomputed_status(status: u16) -> bool {
101 matches!(
102 status,
103 200 | 201 | 204 | 301 | 302 | 304 | 400 | 401 | 403 | 404 | 405 | 500 | 502 | 503
104 )
105}
106
107#[inline]
109pub fn format_status_line(status: u16, buf: &mut BytesMut) {
110 buf.extend_from_slice(b"HTTP/1.1 ");
111 let mut n = status;
113 let d2 = (n % 10) as u8 + b'0';
114 n /= 10;
115 let d1 = (n % 10) as u8 + b'0';
116 n /= 10;
117 let d0 = n as u8 + b'0';
118 buf.put_u8(d0);
119 buf.put_u8(d1);
120 buf.put_u8(d2);
121 buf.extend_from_slice(b" ");
122 buf.extend_from_slice(status_reason(status).as_bytes());
123 buf.extend_from_slice(CRLF);
124}
125
126#[inline]
128fn status_reason(status: u16) -> &'static str {
129 match status {
130 100 => "Continue",
131 101 => "Switching Protocols",
132 200 => "OK",
133 201 => "Created",
134 202 => "Accepted",
135 204 => "No Content",
136 206 => "Partial Content",
137 301 => "Moved Permanently",
138 302 => "Found",
139 303 => "See Other",
140 304 => "Not Modified",
141 307 => "Temporary Redirect",
142 308 => "Permanent Redirect",
143 400 => "Bad Request",
144 401 => "Unauthorized",
145 403 => "Forbidden",
146 404 => "Not Found",
147 405 => "Method Not Allowed",
148 408 => "Request Timeout",
149 409 => "Conflict",
150 410 => "Gone",
151 413 => "Payload Too Large",
152 415 => "Unsupported Media Type",
153 422 => "Unprocessable Entity",
154 429 => "Too Many Requests",
155 500 => "Internal Server Error",
156 501 => "Not Implemented",
157 502 => "Bad Gateway",
158 503 => "Service Unavailable",
159 504 => "Gateway Timeout",
160 _ => "Unknown",
161 }
162}
163
164#[derive(Debug)]
169pub struct ResponseChunks {
170 status_line: StatusLine,
172 headers: BytesMut,
174 header_end: &'static [u8],
176 body: Bytes,
178}
179
180#[derive(Debug)]
181enum StatusLine {
182 Static(&'static [u8]),
183 Dynamic(BytesMut),
184}
185
186impl StatusLine {
187 fn as_slice(&self) -> &[u8] {
188 match self {
189 StatusLine::Static(s) => s,
190 StatusLine::Dynamic(b) => b,
191 }
192 }
193}
194
195impl ResponseChunks {
196 pub fn new(status: u16, headers: &HashMap<String, String>, body: Bytes) -> Self {
198 Self::with_cookies(status, headers, &[], body)
199 }
200
201 pub fn with_cookies(
202 status: u16,
203 headers: &HashMap<String, String>,
204 cookies: &[String],
205 body: Bytes,
206 ) -> Self {
207 let status_line = match status_line(status) {
209 Cow::Borrowed(s) => StatusLine::Static(s),
210 Cow::Owned(s) => StatusLine::Dynamic(BytesMut::from(&s[..])),
211 };
212
213 let mut headers_buf = BytesMut::with_capacity((headers.len() + cookies.len()) * 30 + 32);
215 for (name, value) in headers {
216 headers_buf.extend_from_slice(name.as_bytes());
217 headers_buf.extend_from_slice(HEADER_SEP);
218 headers_buf.extend_from_slice(value.as_bytes());
219 headers_buf.extend_from_slice(CRLF);
220 }
221 for cookie_value in cookies {
222 headers_buf.extend_from_slice(b"Set-Cookie");
223 headers_buf.extend_from_slice(HEADER_SEP);
224 headers_buf.extend_from_slice(cookie_value.as_bytes());
225 headers_buf.extend_from_slice(CRLF);
226 }
227
228 let caller_set_content_length = headers
237 .keys()
238 .any(|k| k.eq_ignore_ascii_case("content-length"));
239 if caller_set_content_length {
240 } else if !body.is_empty() {
242 headers_buf.extend_from_slice(b"Content-Length: ");
243 let len = body.len();
245 let mut num_buf = [0u8; 20];
246 let num_str = format_usize(len, &mut num_buf);
247 headers_buf.extend_from_slice(num_str);
248 headers_buf.extend_from_slice(CRLF);
249 } else if status != 204 && status != 304 && !(100..200).contains(&status) {
250 headers_buf.extend_from_slice(b"Content-Length: 0\r\n");
251 }
252
253 Self {
254 status_line,
255 headers: headers_buf,
256 header_end: CRLF,
257 body,
258 }
259 }
260
261 #[inline]
263 pub fn total_len(&self) -> usize {
264 self.status_line.as_slice().len()
265 + self.headers.len()
266 + self.header_end.len()
267 + self.body.len()
268 }
269
270 #[inline]
274 pub fn as_io_slices(&self) -> [IoSlice<'_>; 4] {
275 [
276 IoSlice::new(self.status_line.as_slice()),
277 IoSlice::new(&self.headers),
278 IoSlice::new(self.header_end),
279 IoSlice::new(&self.body),
280 ]
281 }
282
283 #[inline]
285 pub fn chunk_count(&self) -> usize {
286 if self.body.is_empty() { 3 } else { 4 }
287 }
288
289 pub fn to_bytes(&self) -> Bytes {
293 let mut buf = BytesMut::with_capacity(self.total_len());
294 buf.extend_from_slice(self.status_line.as_slice());
295 buf.extend_from_slice(&self.headers);
296 buf.extend_from_slice(self.header_end);
297 buf.extend_from_slice(&self.body);
298 buf.freeze()
299 }
300}
301
302#[inline]
304fn format_usize(n: usize, buf: &mut [u8; 20]) -> &[u8] {
305 if n == 0 {
306 buf[19] = b'0';
307 return &buf[19..];
308 }
309
310 let mut n = n;
311 let mut pos = 20;
312 while n > 0 && pos > 0 {
313 pos -= 1;
314 buf[pos] = (n % 10) as u8 + b'0';
315 n /= 10;
316 }
317 &buf[pos..]
318}
319
320#[derive(Debug)]
324pub struct VectoredResponse {
325 status: u16,
326 headers: Vec<(String, String)>,
327 body: Option<Bytes>,
328}
329
330impl VectoredResponse {
331 #[inline]
333 pub fn new(status: u16) -> Self {
334 Self {
335 status,
336 headers: Vec::with_capacity(8),
337 body: None,
338 }
339 }
340
341 #[inline]
343 pub fn ok() -> Self {
344 Self::new(200)
345 }
346
347 #[inline]
349 pub fn status(mut self, status: u16) -> Self {
350 self.status = status;
351 self
352 }
353
354 #[inline]
356 pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
357 self.headers.push((name.into(), value.into()));
358 self
359 }
360
361 #[inline]
363 pub fn content_type(self, value: impl Into<String>) -> Self {
364 self.header("Content-Type", value)
365 }
366
367 #[inline]
369 pub fn body(mut self, body: impl Into<Bytes>) -> Self {
370 self.body = Some(body.into());
371 self
372 }
373
374 #[inline]
376 pub fn body_json<T: serde::Serialize>(self, value: &T) -> Result<Self, crate::Error> {
377 let json =
378 crate::json::to_vec(value).map_err(|e| crate::Error::Serialization(e.to_string()))?;
379 Ok(self
380 .content_type("application/json")
381 .body(Bytes::from(json)))
382 }
383
384 #[inline]
386 pub fn build(self) -> ResponseChunks {
387 let headers: HashMap<String, String> = self.headers.into_iter().collect();
388 ResponseChunks::new(self.status, &headers, self.body.unwrap_or_default())
389 }
390
391 #[inline]
393 pub fn build_bytes(self) -> Bytes {
394 self.build().to_bytes()
395 }
396}
397
398impl Default for VectoredResponse {
399 fn default() -> Self {
400 Self::ok()
401 }
402}
403
404impl From<crate::HttpResponse> for ResponseChunks {
409 fn from(response: crate::HttpResponse) -> Self {
410 let status = response.status;
411 let headers = response.headers.to_hashmap();
412 let cookies = response.cookies.clone();
413 let body = response.into_body_bytes();
414 Self::with_cookies(status, &headers, &cookies, body)
415 }
416}
417
418impl crate::HttpResponse {
419 #[inline]
424 pub fn into_chunks(self) -> ResponseChunks {
425 ResponseChunks::from(self)
426 }
427
428 #[inline]
433 pub fn to_vectored(&self) -> ResponseChunks {
434 let headers = self.headers.to_hashmap();
435 ResponseChunks::with_cookies(self.status, &headers, &self.cookies, self.body_bytes())
436 }
437}
438
439use std::sync::atomic::{AtomicU64, Ordering};
444
445#[derive(Debug, Default)]
447pub struct VectoredIoStats {
448 writes: AtomicU64,
450 bytes_written: AtomicU64,
452 precomputed_status: AtomicU64,
454 dynamic_status: AtomicU64,
456}
457
458impl VectoredIoStats {
459 pub fn new() -> Self {
461 Self::default()
462 }
463
464 #[inline]
466 pub fn record_write(&self, bytes: usize, precomputed: bool) {
467 self.writes.fetch_add(1, Ordering::Relaxed);
468 self.bytes_written
469 .fetch_add(bytes as u64, Ordering::Relaxed);
470 if precomputed {
471 self.precomputed_status.fetch_add(1, Ordering::Relaxed);
472 } else {
473 self.dynamic_status.fetch_add(1, Ordering::Relaxed);
474 }
475 }
476
477 pub fn writes(&self) -> u64 {
479 self.writes.load(Ordering::Relaxed)
480 }
481
482 pub fn bytes_written(&self) -> u64 {
484 self.bytes_written.load(Ordering::Relaxed)
485 }
486
487 pub fn precomputed_percentage(&self) -> f64 {
489 let total = self.writes();
490 if total == 0 {
491 return 0.0;
492 }
493 (self.precomputed_status.load(Ordering::Relaxed) as f64 / total as f64) * 100.0
494 }
495}
496
497static VECTORED_STATS: VectoredIoStats = VectoredIoStats {
499 writes: AtomicU64::new(0),
500 bytes_written: AtomicU64::new(0),
501 precomputed_status: AtomicU64::new(0),
502 dynamic_status: AtomicU64::new(0),
503};
504
505pub fn vectored_stats() -> &'static VectoredIoStats {
507 &VECTORED_STATS
508}
509
510#[cfg(test)]
515mod tests {
516 use super::*;
517
518 #[test]
519 fn test_status_line_precomputed() {
520 assert_eq!(status_line(200).as_ref(), b"HTTP/1.1 200 OK\r\n");
521 assert_eq!(status_line(404).as_ref(), b"HTTP/1.1 404 Not Found\r\n");
522 assert_eq!(
523 status_line(500).as_ref(),
524 b"HTTP/1.1 500 Internal Server Error\r\n"
525 );
526 }
527
528 #[test]
529 fn test_status_line_unlisted_is_formatted() {
530 assert_eq!(
532 status_line(429).as_ref(),
533 b"HTTP/1.1 429 Too Many Requests\r\n"
534 );
535 assert!(status_line(418).starts_with(b"HTTP/1.1 418"));
536 }
537
538 #[test]
539 fn test_format_status_line() {
540 let mut buf = BytesMut::with_capacity(64);
541 format_status_line(418, &mut buf);
542 assert!(buf.starts_with(b"HTTP/1.1 418"));
543 }
544
545 #[test]
546 fn test_format_usize() {
547 let mut buf = [0u8; 20];
548 assert_eq!(format_usize(0, &mut buf), b"0");
549 assert_eq!(format_usize(123, &mut buf), b"123");
550 assert_eq!(format_usize(1000000, &mut buf), b"1000000");
551 }
552
553 #[test]
554 fn test_response_chunks_basic() {
555 let mut headers = HashMap::new();
556 headers.insert("Content-Type".to_string(), "text/plain".to_string());
557
558 let chunks = ResponseChunks::new(200, &headers, Bytes::from_static(b"Hello"));
559
560 assert!(chunks.total_len() > 0);
561 assert_eq!(chunks.chunk_count(), 4);
562 }
563
564 #[test]
565 fn test_response_chunks_io_slices() {
566 let mut headers = HashMap::new();
567 headers.insert("X-Test".to_string(), "value".to_string());
568
569 let chunks = ResponseChunks::new(200, &headers, Bytes::from_static(b"body"));
570 let slices = chunks.as_io_slices();
571
572 assert_eq!(slices.len(), 4);
573 assert!(!slices[0].is_empty()); assert!(!slices[1].is_empty()); }
576
577 #[test]
578 fn test_empty_body_gets_content_length_zero() {
579 let headers = HashMap::new();
581 let chunks = ResponseChunks::new(200, &headers, Bytes::new());
582 let s = String::from_utf8_lossy(&chunks.to_bytes()).to_string();
583 assert!(s.contains("Content-Length: 0\r\n"), "response was: {s:?}");
584
585 for status in [204, 304, 100, 101] {
587 let chunks = ResponseChunks::new(status, &headers, Bytes::new());
588 let s = String::from_utf8_lossy(&chunks.to_bytes()).to_string();
589 assert!(
590 !s.contains("Content-Length"),
591 "status {status} must not have Content-Length: {s:?}"
592 );
593 }
594 }
595
596 #[test]
597 fn test_unlisted_status_response_has_correct_status_line() {
598 let headers = HashMap::new();
599 let chunks = ResponseChunks::new(429, &headers, Bytes::new());
600 let s = String::from_utf8_lossy(&chunks.to_bytes()).to_string();
601 assert!(s.starts_with("HTTP/1.1 429 Too Many Requests\r\n"));
602 }
603
604 #[test]
605 fn test_response_chunks_to_bytes() {
606 let mut headers = HashMap::new();
607 headers.insert("Content-Type".to_string(), "text/plain".to_string());
608
609 let chunks = ResponseChunks::new(200, &headers, Bytes::from_static(b"Hello"));
610 let bytes = chunks.to_bytes();
611
612 let s = String::from_utf8_lossy(&bytes);
613 assert!(s.contains("HTTP/1.1 200 OK"));
614 assert!(s.contains("Content-Type: text/plain"));
615 assert!(s.contains("Hello"));
616 }
617
618 #[test]
619 fn test_vectored_response_builder() {
620 let chunks = VectoredResponse::ok()
621 .header("X-Custom", "test")
622 .body(Bytes::from_static(b"body data"))
623 .build();
624
625 let bytes = chunks.to_bytes();
626 let s = String::from_utf8_lossy(&bytes);
627 assert!(s.contains("X-Custom: test"));
628 assert!(s.contains("body data"));
629 }
630
631 #[test]
632 fn test_vectored_response_json() {
633 #[derive(serde::Serialize)]
634 struct Data {
635 status: &'static str,
636 }
637
638 let chunks = VectoredResponse::ok()
639 .body_json(&Data { status: "ok" })
640 .unwrap()
641 .build();
642
643 let bytes = chunks.to_bytes();
644 let s = String::from_utf8_lossy(&bytes);
645 assert!(s.contains("application/json"));
646 assert!(s.contains(r#""status":"ok""#));
647 }
648
649 #[test]
650 fn test_http_response_to_chunks() {
651 let response = crate::HttpResponse::ok()
652 .with_header("X-Test".to_string(), "value".to_string())
653 .with_body(b"test body".to_vec());
654
655 let chunks = response.into_chunks();
656 let bytes = chunks.to_bytes();
657 let s = String::from_utf8_lossy(&bytes);
658
659 assert!(s.contains("HTTP/1.1 200 OK"));
660 assert!(s.contains("X-Test: value"));
661 assert!(s.contains("test body"));
662 }
663
664 #[test]
665 fn test_empty_body() {
666 let chunks = ResponseChunks::new(204, &HashMap::new(), Bytes::new());
667 assert_eq!(chunks.chunk_count(), 3); }
669
670 #[test]
671 fn test_caller_content_length_not_duplicated() {
672 for (name, body) in [
676 ("Content-Length", Bytes::from_static(b"12345")),
677 ("content-length", Bytes::from_static(b"12345")),
678 ("Content-Length", Bytes::new()),
679 ] {
680 let mut headers = HashMap::new();
681 headers.insert(name.to_string(), "5".to_string());
682 let chunks = ResponseChunks::new(200, &headers, body);
683 let s = String::from_utf8_lossy(&chunks.to_bytes()).to_lowercase();
684 assert_eq!(
685 s.matches("content-length:").count(),
686 1,
687 "exactly one Content-Length expected, got: {s:?}"
688 );
689 }
690 }
691
692 #[test]
693 fn test_content_length_header() {
694 let chunks = ResponseChunks::new(200, &HashMap::new(), Bytes::from_static(b"12345"));
695 let bytes = chunks.to_bytes();
696 let s = String::from_utf8_lossy(&bytes);
697 assert!(s.contains("Content-Length: 5"));
698 }
699}