we (web engine): Experimental web browser project to understand the limits of Claude
1//! HTTP/1.1 message parser and request serializer (RFC 7230, 7231).
2//!
3//! Pure parsing — no I/O. Provides:
4//! - Request serialization (GET, POST, etc.)
5//! - Response parsing: status line, headers, body
6//! - Chunked transfer-encoding decoding
7//! - Content-Length body reading
8//! - Case-insensitive header collection
9//! - Content-Type parsing (MIME type + charset)
10
11use std::fmt;
12
13// ---------------------------------------------------------------------------
14// Constants
15// ---------------------------------------------------------------------------
16
17const HTTP_VERSION: &str = "HTTP/1.1";
18const CRLF: &str = "\r\n";
19const USER_AGENT: &str = "we-browser/0.1";
20
21// ---------------------------------------------------------------------------
22// HTTP Method
23// ---------------------------------------------------------------------------
24
25/// HTTP request methods.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum Method {
28 Get,
29 Post,
30 Head,
31 Put,
32 Delete,
33 Options,
34 Patch,
35}
36
37impl Method {
38 /// Return the method name as a string.
39 pub fn as_str(&self) -> &'static str {
40 match self {
41 Self::Get => "GET",
42 Self::Post => "POST",
43 Self::Head => "HEAD",
44 Self::Put => "PUT",
45 Self::Delete => "DELETE",
46 Self::Options => "OPTIONS",
47 Self::Patch => "PATCH",
48 }
49 }
50}
51
52impl fmt::Display for Method {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 f.write_str(self.as_str())
55 }
56}
57
58// ---------------------------------------------------------------------------
59// Headers
60// ---------------------------------------------------------------------------
61
62/// A collection of HTTP headers with case-insensitive name lookup.
63#[derive(Debug, Clone)]
64pub struct Headers {
65 /// Stored as (original_name, value) pairs.
66 entries: Vec<(String, String)>,
67}
68
69impl Headers {
70 /// Create an empty header collection.
71 pub fn new() -> Self {
72 Self {
73 entries: Vec::new(),
74 }
75 }
76
77 /// Add a header. Does not replace existing headers with the same name.
78 pub fn add(&mut self, name: &str, value: &str) {
79 self.entries
80 .push((name.to_string(), value.trim().to_string()));
81 }
82
83 /// Set a header, replacing any existing header with the same name.
84 pub fn set(&mut self, name: &str, value: &str) {
85 let lower = name.to_ascii_lowercase();
86 self.entries
87 .retain(|(n, _)| n.to_ascii_lowercase() != lower);
88 self.entries
89 .push((name.to_string(), value.trim().to_string()));
90 }
91
92 /// Get the first value for a header name (case-insensitive).
93 pub fn get(&self, name: &str) -> Option<&str> {
94 let lower = name.to_ascii_lowercase();
95 self.entries
96 .iter()
97 .find(|(n, _)| n.to_ascii_lowercase() == lower)
98 .map(|(_, v)| v.as_str())
99 }
100
101 /// Get all values for a header name (case-insensitive).
102 pub fn get_all(&self, name: &str) -> Vec<&str> {
103 let lower = name.to_ascii_lowercase();
104 self.entries
105 .iter()
106 .filter(|(n, _)| n.to_ascii_lowercase() == lower)
107 .map(|(_, v)| v.as_str())
108 .collect()
109 }
110
111 /// Check if a header exists (case-insensitive).
112 pub fn contains(&self, name: &str) -> bool {
113 let lower = name.to_ascii_lowercase();
114 self.entries
115 .iter()
116 .any(|(n, _)| n.to_ascii_lowercase() == lower)
117 }
118
119 /// Remove all headers with the given name (case-insensitive).
120 pub fn remove(&mut self, name: &str) {
121 let lower = name.to_ascii_lowercase();
122 self.entries
123 .retain(|(n, _)| n.to_ascii_lowercase() != lower);
124 }
125
126 /// Return an iterator over all (name, value) pairs.
127 pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
128 self.entries.iter().map(|(n, v)| (n.as_str(), v.as_str()))
129 }
130
131 /// Return the number of header entries.
132 pub fn len(&self) -> usize {
133 self.entries.len()
134 }
135
136 /// Check if the collection is empty.
137 pub fn is_empty(&self) -> bool {
138 self.entries.is_empty()
139 }
140}
141
142impl Default for Headers {
143 fn default() -> Self {
144 Self::new()
145 }
146}
147
148// ---------------------------------------------------------------------------
149// Error types
150// ---------------------------------------------------------------------------
151
152/// HTTP parsing errors.
153#[derive(Debug)]
154pub enum HttpError {
155 /// Response status line is malformed.
156 MalformedStatusLine(String),
157 /// Header line is malformed.
158 MalformedHeader(String),
159 /// Chunked encoding is malformed.
160 MalformedChunkedEncoding(String),
161 /// Content-Length value is invalid.
162 InvalidContentLength(String),
163 /// Response is incomplete (not enough data).
164 Incomplete,
165 /// A generic parse error.
166 Parse(String),
167}
168
169impl fmt::Display for HttpError {
170 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171 match self {
172 Self::MalformedStatusLine(s) => write!(f, "malformed status line: {s}"),
173 Self::MalformedHeader(s) => write!(f, "malformed header: {s}"),
174 Self::MalformedChunkedEncoding(s) => write!(f, "malformed chunked encoding: {s}"),
175 Self::InvalidContentLength(s) => write!(f, "invalid Content-Length: {s}"),
176 Self::Incomplete => write!(f, "incomplete HTTP response"),
177 Self::Parse(s) => write!(f, "HTTP parse error: {s}"),
178 }
179 }
180}
181
182pub type Result<T> = std::result::Result<T, HttpError>;
183
184// ---------------------------------------------------------------------------
185// Content-Type
186// ---------------------------------------------------------------------------
187
188/// Parsed Content-Type header value.
189#[derive(Debug, Clone, PartialEq, Eq)]
190pub struct ContentType {
191 /// The MIME type (e.g. "text/html").
192 pub mime_type: String,
193 /// The charset parameter, if present (e.g. "utf-8").
194 pub charset: Option<String>,
195}
196
197impl ContentType {
198 /// Parse a Content-Type header value.
199 ///
200 /// Handles formats like:
201 /// - `text/html`
202 /// - `text/html; charset=utf-8`
203 /// - `text/html; charset="UTF-8"`
204 pub fn parse(value: &str) -> Self {
205 let mut parts = value.splitn(2, ';');
206 let mime_type = parts.next().unwrap_or("").trim().to_ascii_lowercase();
207
208 let charset = parts.next().and_then(|params| {
209 // Look for charset= parameter
210 for param in params.split(';') {
211 let param = param.trim();
212 if let Some(val) = param
213 .strip_prefix("charset=")
214 .or_else(|| param.strip_prefix("CHARSET="))
215 .or_else(|| {
216 // Case-insensitive match
217 let lower = param.to_ascii_lowercase();
218 if lower.starts_with("charset=") {
219 Some(¶m["charset=".len()..])
220 } else {
221 None
222 }
223 })
224 {
225 let val = val.trim();
226 // Remove surrounding quotes if present
227 let val = if val.starts_with('"') && val.ends_with('"') && val.len() >= 2 {
228 &val[1..val.len() - 1]
229 } else {
230 val
231 };
232 return Some(val.to_ascii_lowercase());
233 }
234 }
235 None
236 });
237
238 ContentType { mime_type, charset }
239 }
240}
241
242// ---------------------------------------------------------------------------
243// HTTP Request serialization
244// ---------------------------------------------------------------------------
245
246/// Serialize an HTTP/1.1 request to bytes.
247///
248/// The caller provides the method, path, host, optional extra headers, and
249/// optional body. Required headers (Host, User-Agent, Connection) are added
250/// automatically if not present in the provided headers.
251pub fn serialize_request(
252 method: Method,
253 path: &str,
254 host: &str,
255 headers: &Headers,
256 body: Option<&[u8]>,
257) -> Vec<u8> {
258 let mut buf = Vec::with_capacity(256);
259
260 // Request line
261 buf.extend_from_slice(method.as_str().as_bytes());
262 buf.push(b' ');
263 buf.extend_from_slice(path.as_bytes());
264 buf.push(b' ');
265 buf.extend_from_slice(HTTP_VERSION.as_bytes());
266 buf.extend_from_slice(CRLF.as_bytes());
267
268 // Host header (required)
269 if !headers.contains("Host") {
270 buf.extend_from_slice(b"Host: ");
271 buf.extend_from_slice(host.as_bytes());
272 buf.extend_from_slice(CRLF.as_bytes());
273 }
274
275 // User-Agent
276 if !headers.contains("User-Agent") {
277 buf.extend_from_slice(b"User-Agent: ");
278 buf.extend_from_slice(USER_AGENT.as_bytes());
279 buf.extend_from_slice(CRLF.as_bytes());
280 }
281
282 // Accept
283 if !headers.contains("Accept") {
284 buf.extend_from_slice(b"Accept: */*");
285 buf.extend_from_slice(CRLF.as_bytes());
286 }
287
288 // Connection
289 if !headers.contains("Connection") {
290 buf.extend_from_slice(b"Connection: keep-alive");
291 buf.extend_from_slice(CRLF.as_bytes());
292 }
293
294 // Content-Length for requests with a body
295 if let Some(body_data) = body {
296 if !headers.contains("Content-Length") {
297 let len_str = body_data.len().to_string();
298 buf.extend_from_slice(b"Content-Length: ");
299 buf.extend_from_slice(len_str.as_bytes());
300 buf.extend_from_slice(CRLF.as_bytes());
301 }
302 }
303
304 // User-provided headers
305 for (name, value) in headers.iter() {
306 buf.extend_from_slice(name.as_bytes());
307 buf.extend_from_slice(b": ");
308 buf.extend_from_slice(value.as_bytes());
309 buf.extend_from_slice(CRLF.as_bytes());
310 }
311
312 // End of headers
313 buf.extend_from_slice(CRLF.as_bytes());
314
315 // Body
316 if let Some(body_data) = body {
317 buf.extend_from_slice(body_data);
318 }
319
320 buf
321}
322
323// ---------------------------------------------------------------------------
324// HTTP Response
325// ---------------------------------------------------------------------------
326
327/// A parsed HTTP/1.1 response.
328#[derive(Debug, Clone)]
329pub struct HttpResponse {
330 /// HTTP version string (e.g. "HTTP/1.1").
331 pub version: String,
332 /// Status code (e.g. 200, 404).
333 pub status_code: u16,
334 /// Reason phrase (e.g. "OK", "Not Found").
335 pub reason: String,
336 /// Response headers.
337 pub headers: Headers,
338 /// Response body bytes.
339 pub body: Vec<u8>,
340}
341
342impl HttpResponse {
343 /// Get the Content-Type header, parsed.
344 pub fn content_type(&self) -> Option<ContentType> {
345 self.headers.get("Content-Type").map(ContentType::parse)
346 }
347
348 /// Check if the response indicates the connection should be closed.
349 pub fn connection_close(&self) -> bool {
350 self.headers
351 .get("Connection")
352 .map(|v| v.eq_ignore_ascii_case("close"))
353 .unwrap_or(false)
354 }
355
356 /// Check if the response has no body (per RFC 7230).
357 /// 1xx, 204, and 304 responses have no message body.
358 pub fn has_no_body(&self) -> bool {
359 self.status_code < 200 || self.status_code == 204 || self.status_code == 304
360 }
361}
362
363// ---------------------------------------------------------------------------
364// Response parsing
365// ---------------------------------------------------------------------------
366
367/// Parse an HTTP/1.1 response from raw bytes.
368///
369/// Parses the status line, headers, and body (handling both Content-Length
370/// and chunked Transfer-Encoding).
371pub fn parse_response(data: &[u8]) -> Result<HttpResponse> {
372 let (header_section, body_start) = split_header_body(data)?;
373 let header_str = std::str::from_utf8(header_section)
374 .map_err(|_| HttpError::Parse("invalid UTF-8 in headers".to_string()))?;
375
376 let mut lines = header_str.split("\r\n");
377
378 // Parse status line
379 let status_line = lines.next().ok_or(HttpError::Incomplete)?;
380 let (version, status_code, reason) = parse_status_line(status_line)?;
381
382 // Parse headers
383 let mut headers = Headers::new();
384 for line in lines {
385 if line.is_empty() {
386 break;
387 }
388 let (name, value) = parse_header_line(line)?;
389 headers.add(name, value);
390 }
391
392 let body_data = &data[body_start..];
393
394 // Determine body based on status code
395 let body = if status_code < 200 || status_code == 204 || status_code == 304 {
396 Vec::new()
397 } else {
398 decode_body(body_data, &headers)?
399 };
400
401 Ok(HttpResponse {
402 version: version.to_string(),
403 status_code,
404 reason: reason.to_string(),
405 headers,
406 body,
407 })
408}
409
410/// Split the header section from the body, returning (header_bytes, body_start_offset).
411fn split_header_body(data: &[u8]) -> Result<(&[u8], usize)> {
412 // Look for \r\n\r\n
413 for i in 0..data.len().saturating_sub(3) {
414 if &data[i..i + 4] == b"\r\n\r\n" {
415 return Ok((&data[..i], i + 4));
416 }
417 }
418 Err(HttpError::Incomplete)
419}
420
421/// Parse a status line like "HTTP/1.1 200 OK".
422fn parse_status_line(line: &str) -> Result<(&str, u16, &str)> {
423 // Split: version SP status-code SP reason-phrase
424 let mut parts = line.splitn(3, ' ');
425
426 let version = parts
427 .next()
428 .ok_or_else(|| HttpError::MalformedStatusLine(line.to_string()))?;
429
430 if !version.starts_with("HTTP/") {
431 return Err(HttpError::MalformedStatusLine(line.to_string()));
432 }
433
434 let status_str = parts
435 .next()
436 .ok_or_else(|| HttpError::MalformedStatusLine(line.to_string()))?;
437
438 let status_code: u16 = status_str
439 .parse()
440 .map_err(|_| HttpError::MalformedStatusLine(line.to_string()))?;
441
442 // Reason phrase may be empty (but usually isn't)
443 let reason = parts.next().unwrap_or("");
444
445 Ok((version, status_code, reason))
446}
447
448/// Parse a single header line like "Content-Type: text/html".
449fn parse_header_line(line: &str) -> Result<(&str, &str)> {
450 let colon_pos = line
451 .find(':')
452 .ok_or_else(|| HttpError::MalformedHeader(line.to_string()))?;
453
454 let name = &line[..colon_pos];
455 let value = line[colon_pos + 1..].trim();
456
457 // Header field names must not contain whitespace
458 if name.contains(' ') || name.contains('\t') || name.is_empty() {
459 return Err(HttpError::MalformedHeader(line.to_string()));
460 }
461
462 Ok((name, value))
463}
464
465/// Decode the response body based on Transfer-Encoding and Content-Length headers.
466fn decode_body(body_data: &[u8], headers: &Headers) -> Result<Vec<u8>> {
467 // Check for chunked transfer encoding
468 if let Some(te) = headers.get("Transfer-Encoding") {
469 if te.eq_ignore_ascii_case("chunked") {
470 return decode_chunked(body_data);
471 }
472 }
473
474 // Check for Content-Length
475 if let Some(cl) = headers.get("Content-Length") {
476 let length: usize = cl
477 .trim()
478 .parse()
479 .map_err(|_| HttpError::InvalidContentLength(cl.to_string()))?;
480
481 if body_data.len() < length {
482 return Err(HttpError::Incomplete);
483 }
484
485 return Ok(body_data[..length].to_vec());
486 }
487
488 // No Content-Length and not chunked: read all available data
489 // (connection close signals end of body)
490 Ok(body_data.to_vec())
491}
492
493/// Decode a chunked transfer-encoded body.
494///
495/// Format per RFC 7230 §4.1:
496/// ```text
497/// chunk-size (hex) CRLF
498/// chunk-data CRLF
499/// ...
500/// 0 CRLF
501/// CRLF
502/// ```
503pub fn decode_chunked(data: &[u8]) -> Result<Vec<u8>> {
504 let mut result = Vec::new();
505 let mut pos = 0;
506
507 loop {
508 // Find the end of the chunk-size line
509 let line_end = find_crlf(data, pos).ok_or_else(|| {
510 HttpError::MalformedChunkedEncoding("missing CRLF after chunk size".to_string())
511 })?;
512
513 let size_line = std::str::from_utf8(&data[pos..line_end]).map_err(|_| {
514 HttpError::MalformedChunkedEncoding("invalid UTF-8 in chunk size".to_string())
515 })?;
516
517 // Chunk size may be followed by chunk-extensions (semicolon-delimited);
518 // we only care about the hex size before any extension.
519 let size_str = size_line.split(';').next().unwrap_or("").trim();
520
521 let chunk_size = usize::from_str_radix(size_str, 16).map_err(|_| {
522 HttpError::MalformedChunkedEncoding(format!("invalid chunk size: {size_str}"))
523 })?;
524
525 pos = line_end + 2; // skip past CRLF
526
527 if chunk_size == 0 {
528 // Last chunk. Optional trailers follow, then final CRLF.
529 break;
530 }
531
532 // Read chunk data
533 if pos + chunk_size > data.len() {
534 return Err(HttpError::MalformedChunkedEncoding(
535 "chunk data truncated".to_string(),
536 ));
537 }
538
539 result.extend_from_slice(&data[pos..pos + chunk_size]);
540 pos += chunk_size;
541
542 // Skip trailing CRLF after chunk data
543 if pos + 2 > data.len() || data[pos] != b'\r' || data[pos + 1] != b'\n' {
544 return Err(HttpError::MalformedChunkedEncoding(
545 "missing CRLF after chunk data".to_string(),
546 ));
547 }
548 pos += 2;
549 }
550
551 Ok(result)
552}
553
554/// Find the position of `\r\n` starting from `offset`.
555fn find_crlf(data: &[u8], offset: usize) -> Option<usize> {
556 (offset..data.len().saturating_sub(1)).find(|&i| data[i] == b'\r' && data[i + 1] == b'\n')
557}
558
559// ---------------------------------------------------------------------------
560// Tests
561// ---------------------------------------------------------------------------
562
563#[cfg(test)]
564mod tests {
565 use super::*;
566
567 // -- Method tests --
568
569 #[test]
570 fn method_as_str() {
571 assert_eq!(Method::Get.as_str(), "GET");
572 assert_eq!(Method::Post.as_str(), "POST");
573 assert_eq!(Method::Head.as_str(), "HEAD");
574 assert_eq!(Method::Put.as_str(), "PUT");
575 assert_eq!(Method::Delete.as_str(), "DELETE");
576 assert_eq!(Method::Options.as_str(), "OPTIONS");
577 assert_eq!(Method::Patch.as_str(), "PATCH");
578 }
579
580 #[test]
581 fn method_display() {
582 assert_eq!(format!("{}", Method::Get), "GET");
583 assert_eq!(format!("{}", Method::Post), "POST");
584 }
585
586 // -- Headers tests --
587
588 #[test]
589 fn headers_add_and_get() {
590 let mut h = Headers::new();
591 h.add("Content-Type", "text/html");
592 assert_eq!(h.get("Content-Type"), Some("text/html"));
593 assert_eq!(h.get("content-type"), Some("text/html"));
594 assert_eq!(h.get("CONTENT-TYPE"), Some("text/html"));
595 }
596
597 #[test]
598 fn headers_set_replaces() {
599 let mut h = Headers::new();
600 h.add("X-Foo", "bar");
601 h.set("X-Foo", "baz");
602 assert_eq!(h.get("X-Foo"), Some("baz"));
603 assert_eq!(h.len(), 1);
604 }
605
606 #[test]
607 fn headers_get_all() {
608 let mut h = Headers::new();
609 h.add("Set-Cookie", "a=1");
610 h.add("Set-Cookie", "b=2");
611 let vals = h.get_all("Set-Cookie");
612 assert_eq!(vals, vec!["a=1", "b=2"]);
613 }
614
615 #[test]
616 fn headers_contains() {
617 let mut h = Headers::new();
618 h.add("Host", "example.com");
619 assert!(h.contains("Host"));
620 assert!(h.contains("host"));
621 assert!(!h.contains("Accept"));
622 }
623
624 #[test]
625 fn headers_remove() {
626 let mut h = Headers::new();
627 h.add("X-Foo", "bar");
628 h.add("X-Foo", "baz");
629 h.add("X-Other", "val");
630 h.remove("X-Foo");
631 assert!(!h.contains("X-Foo"));
632 assert_eq!(h.len(), 1);
633 }
634
635 #[test]
636 fn headers_is_empty() {
637 let h = Headers::new();
638 assert!(h.is_empty());
639 }
640
641 #[test]
642 fn headers_iter() {
643 let mut h = Headers::new();
644 h.add("A", "1");
645 h.add("B", "2");
646 let pairs: Vec<_> = h.iter().collect();
647 assert_eq!(pairs, vec![("A", "1"), ("B", "2")]);
648 }
649
650 #[test]
651 fn headers_missing_returns_none() {
652 let h = Headers::new();
653 assert_eq!(h.get("NonExistent"), None);
654 }
655
656 #[test]
657 fn headers_get_all_empty() {
658 let h = Headers::new();
659 let vals = h.get_all("X-Missing");
660 assert!(vals.is_empty());
661 }
662
663 #[test]
664 fn headers_trims_values() {
665 let mut h = Headers::new();
666 h.add("X-Foo", " bar ");
667 assert_eq!(h.get("X-Foo"), Some("bar"));
668 }
669
670 // -- Content-Type parsing tests --
671
672 #[test]
673 fn content_type_simple() {
674 let ct = ContentType::parse("text/html");
675 assert_eq!(ct.mime_type, "text/html");
676 assert_eq!(ct.charset, None);
677 }
678
679 #[test]
680 fn content_type_with_charset() {
681 let ct = ContentType::parse("text/html; charset=utf-8");
682 assert_eq!(ct.mime_type, "text/html");
683 assert_eq!(ct.charset, Some("utf-8".to_string()));
684 }
685
686 #[test]
687 fn content_type_charset_quoted() {
688 let ct = ContentType::parse("text/html; charset=\"UTF-8\"");
689 assert_eq!(ct.mime_type, "text/html");
690 assert_eq!(ct.charset, Some("utf-8".to_string()));
691 }
692
693 #[test]
694 fn content_type_case_insensitive() {
695 let ct = ContentType::parse("Text/HTML; Charset=ISO-8859-1");
696 assert_eq!(ct.mime_type, "text/html");
697 assert_eq!(ct.charset, Some("iso-8859-1".to_string()));
698 }
699
700 #[test]
701 fn content_type_application_json() {
702 let ct = ContentType::parse("application/json");
703 assert_eq!(ct.mime_type, "application/json");
704 assert_eq!(ct.charset, None);
705 }
706
707 #[test]
708 fn content_type_extra_params() {
709 let ct = ContentType::parse("text/html; charset=utf-8; boundary=something");
710 assert_eq!(ct.mime_type, "text/html");
711 assert_eq!(ct.charset, Some("utf-8".to_string()));
712 }
713
714 // -- Request serialization tests --
715
716 #[test]
717 fn serialize_get_request() {
718 let headers = Headers::new();
719 let req = serialize_request(Method::Get, "/", "example.com", &headers, None);
720 let req_str = String::from_utf8(req).unwrap();
721
722 assert!(req_str.starts_with("GET / HTTP/1.1\r\n"));
723 assert!(req_str.contains("Host: example.com\r\n"));
724 assert!(req_str.contains("User-Agent: we-browser/0.1\r\n"));
725 assert!(req_str.contains("Accept: */*\r\n"));
726 assert!(req_str.contains("Connection: keep-alive\r\n"));
727 assert!(req_str.ends_with("\r\n\r\n"));
728 }
729
730 #[test]
731 fn serialize_post_request_with_body() {
732 let mut headers = Headers::new();
733 headers.add("Content-Type", "application/json");
734 let body = b"{\"key\": \"value\"}";
735 let req = serialize_request(
736 Method::Post,
737 "/api",
738 "api.example.com",
739 &headers,
740 Some(body),
741 );
742 let req_str = String::from_utf8(req).unwrap();
743
744 assert!(req_str.starts_with("POST /api HTTP/1.1\r\n"));
745 assert!(req_str.contains("Host: api.example.com\r\n"));
746 assert!(req_str.contains("Content-Length: 16\r\n"));
747 assert!(req_str.contains("Content-Type: application/json\r\n"));
748 assert!(req_str.ends_with("{\"key\": \"value\"}"));
749 }
750
751 #[test]
752 fn serialize_request_with_path() {
753 let headers = Headers::new();
754 let req = serialize_request(
755 Method::Get,
756 "/path/to/resource?query=1",
757 "example.com",
758 &headers,
759 None,
760 );
761 let req_str = String::from_utf8(req).unwrap();
762 assert!(req_str.starts_with("GET /path/to/resource?query=1 HTTP/1.1\r\n"));
763 }
764
765 #[test]
766 fn serialize_request_custom_headers_override() {
767 let mut headers = Headers::new();
768 headers.add("Host", "custom.example.com");
769 headers.add("User-Agent", "custom-agent/1.0");
770 headers.add("Accept", "text/html");
771 headers.add("Connection", "close");
772
773 let req = serialize_request(Method::Get, "/", "example.com", &headers, None);
774 let req_str = String::from_utf8(req).unwrap();
775
776 // Should use custom headers, not add defaults
777 assert!(req_str.contains("Host: custom.example.com\r\n"));
778 assert!(req_str.contains("User-Agent: custom-agent/1.0\r\n"));
779 assert!(!req_str.contains("we-browser"));
780 }
781
782 #[test]
783 fn serialize_head_request() {
784 let headers = Headers::new();
785 let req = serialize_request(Method::Head, "/", "example.com", &headers, None);
786 let req_str = String::from_utf8(req).unwrap();
787 assert!(req_str.starts_with("HEAD / HTTP/1.1\r\n"));
788 }
789
790 #[test]
791 fn serialize_post_empty_body() {
792 let mut headers = Headers::new();
793 headers.add("Content-Type", "text/plain");
794 let req = serialize_request(Method::Post, "/submit", "example.com", &headers, Some(b""));
795 let req_str = String::from_utf8(req).unwrap();
796 assert!(req_str.contains("Content-Length: 0\r\n"));
797 }
798
799 // -- Response parsing tests --
800
801 fn make_response(status: u16, reason: &str, headers: &[(&str, &str)], body: &[u8]) -> Vec<u8> {
802 let mut buf = Vec::new();
803 buf.extend_from_slice(format!("HTTP/1.1 {status} {reason}\r\n").as_bytes());
804 for &(name, value) in headers {
805 buf.extend_from_slice(format!("{name}: {value}\r\n").as_bytes());
806 }
807 // Add Content-Length if not already present and not chunked
808 let has_cl = headers
809 .iter()
810 .any(|(n, _)| n.eq_ignore_ascii_case("Content-Length"));
811 let has_te = headers.iter().any(|(n, v)| {
812 n.eq_ignore_ascii_case("Transfer-Encoding") && v.eq_ignore_ascii_case("chunked")
813 });
814 if !has_cl && !has_te && !body.is_empty() {
815 buf.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
816 }
817 buf.extend_from_slice(b"\r\n");
818 buf.extend_from_slice(body);
819 buf
820 }
821
822 #[test]
823 fn parse_simple_200() {
824 let data = make_response(
825 200,
826 "OK",
827 &[("Content-Type", "text/html")],
828 b"<html></html>",
829 );
830 let resp = parse_response(&data).unwrap();
831 assert_eq!(resp.version, "HTTP/1.1");
832 assert_eq!(resp.status_code, 200);
833 assert_eq!(resp.reason, "OK");
834 assert_eq!(resp.headers.get("Content-Type"), Some("text/html"));
835 assert_eq!(resp.body, b"<html></html>");
836 }
837
838 #[test]
839 fn parse_404_not_found() {
840 let data = make_response(404, "Not Found", &[], b"not found");
841 let resp = parse_response(&data).unwrap();
842 assert_eq!(resp.status_code, 404);
843 assert_eq!(resp.reason, "Not Found");
844 assert_eq!(resp.body, b"not found");
845 }
846
847 #[test]
848 fn parse_204_no_content() {
849 let data = b"HTTP/1.1 204 No Content\r\nConnection: keep-alive\r\n\r\n";
850 let resp = parse_response(data).unwrap();
851 assert_eq!(resp.status_code, 204);
852 assert!(resp.body.is_empty());
853 assert!(resp.has_no_body());
854 }
855
856 #[test]
857 fn parse_304_not_modified() {
858 let data = b"HTTP/1.1 304 Not Modified\r\nETag: \"abc\"\r\n\r\n";
859 let resp = parse_response(data).unwrap();
860 assert_eq!(resp.status_code, 304);
861 assert!(resp.body.is_empty());
862 assert!(resp.has_no_body());
863 }
864
865 #[test]
866 fn parse_100_continue() {
867 let data = b"HTTP/1.1 100 Continue\r\n\r\n";
868 let resp = parse_response(data).unwrap();
869 assert_eq!(resp.status_code, 100);
870 assert!(resp.body.is_empty());
871 assert!(resp.has_no_body());
872 }
873
874 #[test]
875 fn parse_content_length_body() {
876 let body = b"Hello, World!";
877 let data = make_response(200, "OK", &[("Content-Length", "13")], body);
878 let resp = parse_response(&data).unwrap();
879 assert_eq!(resp.body, body);
880 }
881
882 #[test]
883 fn parse_zero_content_length() {
884 let data = make_response(200, "OK", &[("Content-Length", "0")], b"");
885 let resp = parse_response(&data).unwrap();
886 assert!(resp.body.is_empty());
887 }
888
889 #[test]
890 fn parse_chunked_body() {
891 let mut data = Vec::new();
892 data.extend_from_slice(b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n");
893 data.extend_from_slice(b"5\r\nHello\r\n");
894 data.extend_from_slice(b"7\r\n, World\r\n");
895 data.extend_from_slice(b"0\r\n\r\n");
896
897 let resp = parse_response(&data).unwrap();
898 assert_eq!(resp.body, b"Hello, World");
899 }
900
901 #[test]
902 fn parse_chunked_with_extensions() {
903 let mut data = Vec::new();
904 data.extend_from_slice(b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n");
905 data.extend_from_slice(b"5;ext=val\r\nHello\r\n");
906 data.extend_from_slice(b"0\r\n\r\n");
907
908 let resp = parse_response(&data).unwrap();
909 assert_eq!(resp.body, b"Hello");
910 }
911
912 #[test]
913 fn parse_multiple_headers_same_name() {
914 let data =
915 b"HTTP/1.1 200 OK\r\nSet-Cookie: a=1\r\nSet-Cookie: b=2\r\nContent-Length: 0\r\n\r\n";
916 let resp = parse_response(data).unwrap();
917 let cookies = resp.headers.get_all("Set-Cookie");
918 assert_eq!(cookies, vec!["a=1", "b=2"]);
919 }
920
921 #[test]
922 fn parse_case_insensitive_headers() {
923 let data = make_response(200, "OK", &[("content-type", "text/plain")], b"ok");
924 let resp = parse_response(&data).unwrap();
925 assert_eq!(resp.headers.get("Content-Type"), Some("text/plain"));
926 assert_eq!(resp.headers.get("CONTENT-TYPE"), Some("text/plain"));
927 }
928
929 #[test]
930 fn parse_connection_close() {
931 let data = make_response(200, "OK", &[("Connection", "close")], b"bye");
932 let resp = parse_response(&data).unwrap();
933 assert!(resp.connection_close());
934 }
935
936 #[test]
937 fn parse_connection_keep_alive() {
938 let data = make_response(200, "OK", &[("Connection", "keep-alive")], b"hi");
939 let resp = parse_response(&data).unwrap();
940 assert!(!resp.connection_close());
941 }
942
943 #[test]
944 fn parse_content_type_method() {
945 let data = make_response(
946 200,
947 "OK",
948 &[("Content-Type", "text/html; charset=utf-8")],
949 b"<html></html>",
950 );
951 let resp = parse_response(&data).unwrap();
952 let ct = resp.content_type().unwrap();
953 assert_eq!(ct.mime_type, "text/html");
954 assert_eq!(ct.charset, Some("utf-8".to_string()));
955 }
956
957 #[test]
958 fn parse_no_content_type() {
959 let data = make_response(200, "OK", &[], b"data");
960 let resp = parse_response(&data).unwrap();
961 assert!(resp.content_type().is_none());
962 }
963
964 // -- Status line edge cases --
965
966 #[test]
967 fn parse_status_line_no_reason() {
968 // Some servers send status lines without a reason phrase
969 let data = b"HTTP/1.1 200 \r\nContent-Length: 0\r\n\r\n";
970 let resp = parse_response(data).unwrap();
971 assert_eq!(resp.status_code, 200);
972 }
973
974 #[test]
975 fn parse_http10_response() {
976 let data = b"HTTP/1.0 200 OK\r\nContent-Length: 2\r\n\r\nhi";
977 let resp = parse_response(data).unwrap();
978 assert_eq!(resp.version, "HTTP/1.0");
979 assert_eq!(resp.status_code, 200);
980 assert_eq!(resp.body, b"hi");
981 }
982
983 // -- Error cases --
984
985 #[test]
986 fn parse_malformed_status_line() {
987 let data = b"INVALID\r\n\r\n";
988 let result = parse_response(data);
989 assert!(result.is_err());
990 }
991
992 #[test]
993 fn parse_malformed_status_code() {
994 let data = b"HTTP/1.1 abc OK\r\n\r\n";
995 let result = parse_response(data);
996 assert!(result.is_err());
997 }
998
999 #[test]
1000 fn parse_incomplete_response() {
1001 // No \r\n\r\n terminator
1002 let data = b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n";
1003 let result = parse_response(data);
1004 assert!(matches!(result, Err(HttpError::Incomplete)));
1005 }
1006
1007 #[test]
1008 fn parse_invalid_content_length() {
1009 let data = b"HTTP/1.1 200 OK\r\nContent-Length: abc\r\n\r\ndata";
1010 let result = parse_response(data);
1011 assert!(matches!(result, Err(HttpError::InvalidContentLength(_))));
1012 }
1013
1014 #[test]
1015 fn parse_malformed_header() {
1016 let data = b"HTTP/1.1 200 OK\r\n bad header\r\n\r\n";
1017 let result = parse_response(data);
1018 assert!(matches!(result, Err(HttpError::MalformedHeader(_))));
1019 }
1020
1021 // -- Chunked encoding edge cases --
1022
1023 #[test]
1024 fn decode_chunked_empty() {
1025 let data = b"0\r\n\r\n";
1026 let result = decode_chunked(data).unwrap();
1027 assert!(result.is_empty());
1028 }
1029
1030 #[test]
1031 fn decode_chunked_single_chunk() {
1032 let data = b"a\r\n0123456789\r\n0\r\n\r\n";
1033 let result = decode_chunked(data).unwrap();
1034 assert_eq!(result, b"0123456789");
1035 }
1036
1037 #[test]
1038 fn decode_chunked_multiple_chunks() {
1039 let data = b"3\r\nabc\r\n3\r\ndef\r\n0\r\n\r\n";
1040 let result = decode_chunked(data).unwrap();
1041 assert_eq!(result, b"abcdef");
1042 }
1043
1044 #[test]
1045 fn decode_chunked_hex_upper() {
1046 let data = b"A\r\n0123456789\r\n0\r\n\r\n";
1047 let result = decode_chunked(data).unwrap();
1048 assert_eq!(result, b"0123456789");
1049 }
1050
1051 #[test]
1052 fn decode_chunked_invalid_size() {
1053 let data = b"xyz\r\ndata\r\n0\r\n\r\n";
1054 let result = decode_chunked(data);
1055 assert!(result.is_err());
1056 }
1057
1058 #[test]
1059 fn decode_chunked_truncated() {
1060 // Chunk says 10 bytes, but only 3 are provided
1061 let data = b"a\r\nabc";
1062 let result = decode_chunked(data);
1063 assert!(result.is_err());
1064 }
1065
1066 // -- Error display tests --
1067
1068 #[test]
1069 fn error_display_malformed_status() {
1070 let e = HttpError::MalformedStatusLine("bad".to_string());
1071 assert_eq!(e.to_string(), "malformed status line: bad");
1072 }
1073
1074 #[test]
1075 fn error_display_malformed_header() {
1076 let e = HttpError::MalformedHeader("no colon".to_string());
1077 assert_eq!(e.to_string(), "malformed header: no colon");
1078 }
1079
1080 #[test]
1081 fn error_display_malformed_chunked() {
1082 let e = HttpError::MalformedChunkedEncoding("bad chunk".to_string());
1083 assert_eq!(e.to_string(), "malformed chunked encoding: bad chunk");
1084 }
1085
1086 #[test]
1087 fn error_display_invalid_content_length() {
1088 let e = HttpError::InvalidContentLength("abc".to_string());
1089 assert_eq!(e.to_string(), "invalid Content-Length: abc");
1090 }
1091
1092 #[test]
1093 fn error_display_incomplete() {
1094 let e = HttpError::Incomplete;
1095 assert_eq!(e.to_string(), "incomplete HTTP response");
1096 }
1097
1098 #[test]
1099 fn error_display_parse() {
1100 let e = HttpError::Parse("something went wrong".to_string());
1101 assert_eq!(e.to_string(), "HTTP parse error: something went wrong");
1102 }
1103
1104 // -- Body without Content-Length or chunked --
1105
1106 #[test]
1107 fn parse_body_no_content_length_no_chunked() {
1108 // When neither Content-Length nor chunked is present, read all remaining data
1109 let data = b"HTTP/1.1 200 OK\r\nConnection: close\r\n\r\nall the data";
1110 let resp = parse_response(data).unwrap();
1111 assert_eq!(resp.body, b"all the data");
1112 }
1113
1114 // -- Large chunk size --
1115
1116 #[test]
1117 fn decode_chunked_large_hex() {
1118 // Use hex ff (255)
1119 let mut data = Vec::new();
1120 data.extend_from_slice(b"ff\r\n");
1121 data.extend_from_slice(&[b'X'; 255]);
1122 data.extend_from_slice(b"\r\n0\r\n\r\n");
1123 let result = decode_chunked(&data).unwrap();
1124 assert_eq!(result.len(), 255);
1125 assert!(result.iter().all(|&b| b == b'X'));
1126 }
1127
1128 // -- HttpResponse methods --
1129
1130 #[test]
1131 fn response_has_no_body_1xx() {
1132 let resp = HttpResponse {
1133 version: "HTTP/1.1".to_string(),
1134 status_code: 101,
1135 reason: "Switching Protocols".to_string(),
1136 headers: Headers::new(),
1137 body: Vec::new(),
1138 };
1139 assert!(resp.has_no_body());
1140 }
1141
1142 #[test]
1143 fn response_has_body_200() {
1144 let resp = HttpResponse {
1145 version: "HTTP/1.1".to_string(),
1146 status_code: 200,
1147 reason: "OK".to_string(),
1148 headers: Headers::new(),
1149 body: Vec::new(),
1150 };
1151 assert!(!resp.has_no_body());
1152 }
1153
1154 // -- Default trait --
1155
1156 #[test]
1157 fn headers_default() {
1158 let h = Headers::default();
1159 assert!(h.is_empty());
1160 }
1161}