we (web engine): Experimental web browser project to understand the limits of Claude
at main 1161 lines 37 kB view raw
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(&param["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}