use crate::body::{BodyConfig, BodyError, parse_body_with_consumed};
use fastapi_core::{Body, HttpVersion, Method, Request};
use std::borrow::Cow;
#[derive(Debug)]
pub enum ParseError {
InvalidRequestLine,
InvalidMethod,
InvalidHeader,
InvalidHeaderName,
InvalidHeaderBytes,
RequestLineTooLong,
HeaderLineTooLong,
TooManyHeaders,
HeadersTooLarge,
InvalidTransferEncoding,
AmbiguousBodyLength,
TooLarge,
Incomplete,
}
impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidRequestLine => write!(f, "invalid request line"),
Self::InvalidMethod => write!(f, "invalid HTTP method"),
Self::InvalidHeader => write!(f, "invalid header"),
Self::InvalidHeaderName => write!(f, "invalid header name"),
Self::InvalidHeaderBytes => write!(f, "invalid header bytes"),
Self::RequestLineTooLong => write!(f, "request line too long"),
Self::HeaderLineTooLong => write!(f, "header line too long"),
Self::TooManyHeaders => write!(f, "too many headers"),
Self::HeadersTooLarge => write!(f, "headers too large"),
Self::InvalidTransferEncoding => write!(f, "invalid transfer-encoding"),
Self::AmbiguousBodyLength => write!(f, "ambiguous body length"),
Self::TooLarge => write!(f, "request too large"),
Self::Incomplete => write!(f, "incomplete request"),
}
}
}
impl std::error::Error for ParseError {}
#[derive(Debug, Clone)]
pub struct ParseLimits {
pub max_request_size: usize,
pub max_request_line_len: usize,
pub max_header_count: usize,
pub max_header_line_len: usize,
pub max_headers_size: usize,
}
impl Default for ParseLimits {
fn default() -> Self {
Self {
max_request_size: 1024 * 1024, max_request_line_len: 8 * 1024, max_header_count: 100,
max_header_line_len: 8 * 1024, max_headers_size: 64 * 1024, }
}
}
fn has_invalid_request_line_bytes(line: &[u8]) -> bool {
line.iter().any(|&b| b == 0 || b == b'\r' || b == b'\n')
}
#[derive(Debug, Clone, Copy)]
pub struct RequestLine<'a> {
method: Method,
uri: &'a str,
path: &'a str,
query: Option<&'a str>,
version: &'a str,
}
impl<'a> RequestLine<'a> {
pub fn parse(buffer: &'a [u8]) -> Result<Self, ParseError> {
let line_end = buffer
.windows(2)
.position(|w| w == b"\r\n")
.unwrap_or(buffer.len());
let line = &buffer[..line_end];
if has_invalid_request_line_bytes(line) {
return Err(ParseError::InvalidRequestLine);
}
let mut parts = line.splitn(3, |&b| b == b' ');
let method_bytes = parts.next().ok_or(ParseError::InvalidRequestLine)?;
let method = Method::from_bytes(method_bytes).ok_or(ParseError::InvalidMethod)?;
let uri_bytes = parts.next().ok_or(ParseError::InvalidRequestLine)?;
if uri_bytes.is_empty() {
return Err(ParseError::InvalidRequestLine);
}
let uri = std::str::from_utf8(uri_bytes).map_err(|_| ParseError::InvalidRequestLine)?;
let version_bytes = parts.next().ok_or(ParseError::InvalidRequestLine)?;
let version =
std::str::from_utf8(version_bytes).map_err(|_| ParseError::InvalidRequestLine)?;
let (path, query) = if let Some(q_pos) = uri.find('?') {
(&uri[..q_pos], Some(&uri[q_pos + 1..]))
} else {
(uri, None)
};
Ok(Self {
method,
uri,
path,
query,
version,
})
}
pub fn parse_with_len(buffer: &'a [u8]) -> Result<(Self, usize), ParseError> {
let line_end = buffer
.windows(2)
.position(|w| w == b"\r\n")
.ok_or(ParseError::Incomplete)?;
let line = Self::parse(&buffer[..line_end])?;
Ok((line, line_end + 2))
}
#[inline]
#[must_use]
pub fn method(&self) -> Method {
self.method
}
#[inline]
#[must_use]
pub fn path(&self) -> &'a str {
self.path
}
#[inline]
#[must_use]
pub fn query(&self) -> Option<&'a str> {
self.query
}
#[must_use]
pub fn uri(&self) -> &'a str {
self.uri
}
#[inline]
#[must_use]
pub fn version(&self) -> &'a str {
self.version
}
#[inline]
#[must_use]
pub fn is_http11(&self) -> bool {
self.version == "HTTP/1.1"
}
#[inline]
#[must_use]
pub fn is_http10(&self) -> bool {
self.version == "HTTP/1.0"
}
}
fn is_token_char(b: u8) -> bool {
matches!(
b,
b'!' | b'#' | b'$' | b'%' | b'&' | b'\'' | b'*' | b'+' | b'-' | b'.' | b'^' | b'_' | b'`'
| b'|' | b'~' | b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z'
)
}
fn is_valid_header_name(bytes: &[u8]) -> bool {
!bytes.is_empty() && bytes.iter().all(|&b| is_token_char(b))
}
fn has_invalid_header_value_bytes(value: &[u8]) -> bool {
value
.iter()
.any(|&b| b == 0 || b == 0x7f || (b < 0x20 && b != b'\t' && b != b' '))
}
fn has_invalid_header_line_bytes(line: &[u8]) -> bool {
line.iter().any(|&b| b == 0)
}
#[derive(Debug, Clone, Copy)]
pub struct Header<'a> {
name: &'a str,
name_bytes: &'a [u8],
value: &'a [u8],
}
impl<'a> Header<'a> {
#[inline]
#[must_use]
pub fn name(&self) -> &'a str {
self.name
}
#[inline]
#[must_use]
pub fn name_bytes(&self) -> &'a [u8] {
self.name_bytes
}
#[inline]
#[must_use]
pub fn as_bytes_pair(&self) -> (&'a [u8], &'a [u8]) {
(self.name_bytes, self.value)
}
#[inline]
#[must_use]
pub fn value(&self) -> &'a [u8] {
self.value
}
#[must_use]
pub fn value_str(&self) -> Option<&'a str> {
std::str::from_utf8(self.value).ok()
}
#[must_use]
pub fn name_eq_ignore_case(&self, other: &str) -> bool {
self.name.eq_ignore_ascii_case(other)
}
#[inline]
#[must_use]
pub fn is_content_length(&self) -> bool {
self.name_eq_ignore_case("content-length")
}
#[inline]
#[must_use]
pub fn is_transfer_encoding(&self) -> bool {
self.name_eq_ignore_case("transfer-encoding")
}
#[must_use]
pub fn as_content_length(&self) -> Option<usize> {
if !self.is_content_length() {
return None;
}
self.value_str()?.trim().parse().ok()
}
#[must_use]
pub fn is_chunked_encoding(&self) -> bool {
if !self.is_transfer_encoding() {
return false;
}
self.value_str()
.is_some_and(|v| v.trim().eq_ignore_ascii_case("chunked"))
}
}
pub struct HeadersIter<'a> {
remaining: &'a [u8],
}
impl<'a> HeadersIter<'a> {
#[must_use]
pub fn new(buffer: &'a [u8]) -> Self {
Self { remaining: buffer }
}
fn parse_header(line: &'a [u8]) -> Result<Header<'a>, ParseError> {
if has_invalid_header_line_bytes(line) {
return Err(ParseError::InvalidHeaderBytes);
}
let colon_pos = line
.iter()
.position(|&b| b == b':')
.ok_or(ParseError::InvalidHeader)?;
let name_bytes = &line[..colon_pos];
if !is_valid_header_name(name_bytes) {
return Err(ParseError::InvalidHeaderName);
}
let name = std::str::from_utf8(name_bytes).map_err(|_| ParseError::InvalidHeader)?;
let value_start = line[colon_pos + 1..]
.iter()
.position(|&b| b != b' ' && b != b'\t')
.map_or(colon_pos + 1, |p| colon_pos + 1 + p);
let value = &line[value_start..];
if has_invalid_header_value_bytes(value) {
return Err(ParseError::InvalidHeaderBytes);
}
let trimmed_name = name.trim();
Ok(Header {
name: trimmed_name,
name_bytes,
value,
})
}
}
impl<'a> Iterator for HeadersIter<'a> {
type Item = Result<Header<'a>, ParseError>;
fn next(&mut self) -> Option<Self::Item> {
if self.remaining.is_empty() {
return None;
}
let line_end = self
.remaining
.windows(2)
.position(|w| w == b"\r\n")
.unwrap_or(self.remaining.len());
if line_end == 0 {
self.remaining = &[];
return None;
}
let line = &self.remaining[..line_end];
self.remaining = if line_end + 2 <= self.remaining.len() {
&self.remaining[line_end + 2..]
} else {
&[]
};
Some(Self::parse_header(line))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BodyLength {
ContentLength(usize),
Chunked,
None,
Conflicting,
}
pub struct HeadersParser<'a> {
buffer: &'a [u8],
bytes_consumed: usize,
content_length: Option<usize>,
is_chunked: bool,
}
impl<'a> HeadersParser<'a> {
pub fn parse(buffer: &'a [u8]) -> Result<Self, ParseError> {
Self::parse_with_limits(buffer, &ParseLimits::default())
}
pub fn parse_with_limits(buffer: &'a [u8], limits: &ParseLimits) -> Result<Self, ParseError> {
let header_end = buffer.windows(4).position(|w| w == b"\r\n\r\n");
let header_block_len = header_end.map_or(buffer.len(), |pos| pos + 4);
if header_block_len > limits.max_headers_size {
return Err(ParseError::HeadersTooLarge);
}
let mut content_length = None;
let mut saw_transfer_encoding = false;
let mut is_chunked = false;
let mut header_count = 0usize;
let mut remaining = &buffer[..header_block_len];
while !remaining.is_empty() {
let line_end = remaining
.windows(2)
.position(|w| w == b"\r\n")
.unwrap_or(remaining.len());
if line_end == 0 {
break;
}
if line_end > limits.max_header_line_len {
return Err(ParseError::HeaderLineTooLong);
}
let line = &remaining[..line_end];
if matches!(line.first(), Some(b' ' | b'\t')) {
return Err(ParseError::InvalidHeader);
}
let header = HeadersIter::parse_header(line)?;
header_count += 1;
if header_count > limits.max_header_count {
return Err(ParseError::TooManyHeaders);
}
if header.is_content_length() {
let len = header
.as_content_length()
.ok_or(ParseError::InvalidHeader)?;
if content_length.is_some() && content_length != Some(len) {
return Err(ParseError::InvalidHeader);
}
content_length = Some(len);
}
if header.is_transfer_encoding() {
saw_transfer_encoding = true;
if header.is_chunked_encoding() {
is_chunked = true;
} else {
return Err(ParseError::InvalidTransferEncoding);
}
}
remaining = if line_end + 2 <= remaining.len() {
&remaining[line_end + 2..]
} else {
&[]
};
}
if saw_transfer_encoding && content_length.is_some() {
return Err(ParseError::AmbiguousBodyLength);
}
let bytes_consumed = header_block_len;
Ok(Self {
buffer,
bytes_consumed,
content_length,
is_chunked,
})
}
#[must_use]
pub fn body_length(&self) -> BodyLength {
if self.is_chunked {
if self.content_length.is_some() {
return BodyLength::Conflicting;
}
return BodyLength::Chunked;
}
if let Some(len) = self.content_length {
return BodyLength::ContentLength(len);
}
BodyLength::None
}
#[must_use]
pub fn content_length(&self) -> Option<usize> {
self.content_length
}
#[must_use]
pub fn is_chunked(&self) -> bool {
self.is_chunked
}
#[must_use]
pub fn bytes_consumed(&self) -> usize {
self.bytes_consumed
}
#[must_use]
pub fn iter(&self) -> HeadersIter<'a> {
HeadersIter::new(self.buffer)
}
#[must_use]
pub fn get(&self, name: &str) -> Option<Header<'a>> {
self.iter()
.filter_map(Result::ok)
.find(|h| h.name_eq_ignore_case(name))
}
pub fn get_all<'b>(&'b self, name: &'b str) -> impl Iterator<Item = Header<'a>> + 'b {
self.iter()
.filter_map(Result::ok)
.filter(move |h| h.name_eq_ignore_case(name))
}
}
pub struct Parser {
limits: ParseLimits,
}
impl Parser {
#[must_use]
pub fn new() -> Self {
Self {
limits: ParseLimits::default(),
}
}
#[must_use]
pub fn with_max_size(mut self, size: usize) -> Self {
self.limits.max_request_size = size;
self
}
#[must_use]
pub fn with_limits(mut self, limits: ParseLimits) -> Self {
self.limits = limits;
self
}
pub fn parse(&self, buffer: &[u8]) -> Result<Request, ParseError> {
if buffer.len() > self.limits.max_request_size {
return Err(ParseError::TooLarge);
}
let header_end = find_header_end(buffer).ok_or(ParseError::Incomplete)?;
let header_bytes = &buffer[..header_end];
let body_bytes = &buffer[header_end + 4..];
let first_line_end = header_bytes
.windows(2)
.position(|w| w == b"\r\n")
.ok_or(ParseError::InvalidRequestLine)?;
if first_line_end > self.limits.max_request_line_len {
return Err(ParseError::RequestLineTooLong);
}
let request_line = &header_bytes[..first_line_end];
let (method, path, query, http_version) = parse_request_line(request_line)?;
let header_start = first_line_end + 2;
let header_block_len = header_end + 4 - header_start;
if header_block_len > self.limits.max_headers_size {
return Err(ParseError::HeadersTooLarge);
}
let headers =
HeadersParser::parse_with_limits(&buffer[header_start..header_end + 4], &self.limits)?;
let mut request = Request::with_version(method, path, http_version);
request.set_query(query);
for header in headers.iter() {
let header = header?;
request
.headers_mut()
.insert_from_slice(header.name(), header.value());
}
if !body_bytes.is_empty() {
request.set_body(Body::Bytes(body_bytes.to_vec()));
}
Ok(request)
}
}
impl Default for Parser {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum ParseStatus {
Complete { request: Request, consumed: usize },
Incomplete,
}
#[derive(Debug)]
enum ParseState {
RequestLine,
Headers {
method: Method,
path: String,
query: Option<String>,
http_version: HttpVersion,
header_start: usize,
},
Body {
request: Request,
body_length: BodyLength,
body_start: usize,
},
}
pub struct StatefulParser {
limits: ParseLimits,
body_config: BodyConfig,
buffer: Vec<u8>,
state: ParseState,
}
impl StatefulParser {
#[must_use]
pub fn new() -> Self {
Self {
limits: ParseLimits::default(),
body_config: BodyConfig::default(),
buffer: Vec::new(),
state: ParseState::RequestLine,
}
}
#[must_use]
pub fn with_max_size(mut self, size: usize) -> Self {
self.limits.max_request_size = size;
self
}
#[must_use]
pub fn with_limits(mut self, limits: ParseLimits) -> Self {
self.limits = limits;
self
}
#[must_use]
pub fn with_body_config(mut self, config: BodyConfig) -> Self {
self.body_config = config;
self
}
#[must_use]
pub fn buffered_len(&self) -> usize {
self.buffer.len()
}
#[must_use]
pub fn take_buffered(&mut self) -> Vec<u8> {
std::mem::take(&mut self.buffer)
}
pub fn clear(&mut self) {
self.buffer.clear();
self.state = ParseState::RequestLine;
}
pub fn feed(&mut self, bytes: &[u8]) -> Result<ParseStatus, ParseError> {
if !bytes.is_empty() {
self.buffer.extend_from_slice(bytes);
}
if self.buffer.len() > self.limits.max_request_size {
return Err(ParseError::TooLarge);
}
loop {
let state = std::mem::replace(&mut self.state, ParseState::RequestLine);
match state {
ParseState::RequestLine => match parse_request_line_with_len_limit(
&self.buffer,
self.limits.max_request_line_len,
) {
Ok((method, path, query, http_version, header_start)) => {
self.state = ParseState::Headers {
method,
path,
query,
http_version,
header_start,
};
}
Err(ParseError::Incomplete) => {
if self.buffer.len() > self.limits.max_request_line_len {
self.state = ParseState::RequestLine;
return Err(ParseError::RequestLineTooLong);
}
self.state = ParseState::RequestLine;
return Ok(ParseStatus::Incomplete);
}
Err(err) => return Err(err),
},
ParseState::Headers {
method,
path,
query,
http_version,
header_start,
} => {
let header_end = match find_header_end_from(&self.buffer, header_start) {
Some(pos) => pos,
None => {
if self.buffer.len().saturating_sub(header_start)
> self.limits.max_headers_size
{
self.state = ParseState::Headers {
method,
path,
query,
http_version,
header_start,
};
return Err(ParseError::HeadersTooLarge);
}
self.state = ParseState::Headers {
method,
path,
query,
http_version,
header_start,
};
return Ok(ParseStatus::Incomplete);
}
};
let body_start = header_end + 4;
let header_block_len = body_start - header_start;
if header_block_len > self.limits.max_headers_size {
return Err(ParseError::HeadersTooLarge);
}
let header_slice = &self.buffer[header_start..body_start];
let headers = HeadersParser::parse_with_limits(header_slice, &self.limits)?;
let mut request = Request::with_version(method, path, http_version);
request.set_query(query);
for header in headers.iter() {
let header = header?;
request
.headers_mut()
.insert_from_slice(header.name(), header.value());
}
let body_length = headers.body_length();
if matches!(body_length, BodyLength::None) {
let consumed = body_start;
self.consume(consumed);
return Ok(ParseStatus::Complete { request, consumed });
}
self.state = ParseState::Body {
request,
body_length,
body_start,
};
}
ParseState::Body {
mut request,
body_length,
body_start,
} => {
let body_slice = &self.buffer[body_start..];
match parse_body_with_consumed(body_slice, body_length, &self.body_config) {
Ok((body, body_consumed)) => {
if let Some(body) = body {
request.set_body(Body::Bytes(body));
}
let consumed = body_start + body_consumed;
self.consume(consumed);
return Ok(ParseStatus::Complete { request, consumed });
}
Err(err) => {
let mapped = map_body_error(err);
if matches!(mapped, ParseError::Incomplete) {
self.state = ParseState::Body {
request,
body_length,
body_start,
};
return Ok(ParseStatus::Incomplete);
}
return Err(mapped);
}
}
}
}
}
}
fn consume(&mut self, consumed: usize) {
if consumed >= self.buffer.len() {
self.buffer.clear();
} else {
self.buffer.drain(..consumed);
}
self.state = ParseState::RequestLine;
}
}
impl Default for StatefulParser {
fn default() -> Self {
Self::new()
}
}
fn find_header_end(buffer: &[u8]) -> Option<usize> {
buffer.windows(4).position(|w| w == b"\r\n\r\n")
}
fn find_header_end_from(buffer: &[u8], start: usize) -> Option<usize> {
find_header_end(&buffer[start..]).map(|pos| start + pos)
}
fn parse_request_line_with_len_limit(
buffer: &[u8],
max_len: usize,
) -> Result<(Method, String, Option<String>, HttpVersion, usize), ParseError> {
let line_end = buffer
.windows(2)
.position(|w| w == b"\r\n")
.ok_or(ParseError::Incomplete)?;
if line_end > max_len {
return Err(ParseError::RequestLineTooLong);
}
let (method, path, query, http_version) = parse_request_line(&buffer[..line_end])?;
Ok((method, path, query, http_version, line_end + 2))
}
fn map_body_error(error: BodyError) -> ParseError {
match error {
BodyError::TooLarge { .. } => ParseError::TooLarge,
BodyError::Incomplete { .. } | BodyError::UnexpectedEof => ParseError::Incomplete,
BodyError::Parse(err) => err,
BodyError::InvalidChunkedEncoding { .. } => ParseError::InvalidHeader,
}
}
fn parse_request_line(
line: &[u8],
) -> Result<(Method, String, Option<String>, HttpVersion), ParseError> {
if has_invalid_request_line_bytes(line) {
return Err(ParseError::InvalidRequestLine);
}
let mut parts = line.split(|&b| b == b' ');
let method_bytes = parts.next().ok_or(ParseError::InvalidRequestLine)?;
let method = Method::from_bytes(method_bytes).ok_or(ParseError::InvalidMethod)?;
let uri_bytes = parts.next().ok_or(ParseError::InvalidRequestLine)?;
let uri = std::str::from_utf8(uri_bytes).map_err(|_| ParseError::InvalidRequestLine)?;
let version_bytes = parts.next().ok_or(ParseError::InvalidRequestLine)?;
let version_str =
std::str::from_utf8(version_bytes).map_err(|_| ParseError::InvalidRequestLine)?;
let http_version = HttpVersion::parse(version_str).ok_or(ParseError::InvalidRequestLine)?;
if parts.next().is_some() {
return Err(ParseError::InvalidRequestLine);
}
let (path, query) = if let Some(q_pos) = uri.find('?') {
(
percent_decode_path(&uri[..q_pos])?,
Some(uri[q_pos + 1..].to_string()),
)
} else {
(percent_decode_path(uri)?, None)
};
let path = match path {
Cow::Borrowed(borrowed) => borrowed.to_string(),
Cow::Owned(owned) => owned,
};
Ok((method, path, query, http_version))
}
fn percent_decode_path(s: &str) -> Result<Cow<'_, str>, ParseError> {
if !s.contains('%') {
return Ok(Cow::Borrowed(s));
}
let mut result = Vec::with_capacity(s.len());
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'%' if i + 2 < bytes.len() => {
if let (Some(hi), Some(lo)) = (hex_digit(bytes[i + 1]), hex_digit(bytes[i + 2])) {
result.push(hi << 4 | lo);
i += 3;
} else {
result.push(b'%');
i += 1;
}
}
b => {
result.push(b);
i += 1;
}
}
}
String::from_utf8(result)
.map(Cow::Owned)
.map_err(|_| ParseError::InvalidRequestLine)
}
fn hex_digit(b: u8) -> Option<u8> {
match b {
b'0'..=b'9' => Some(b - b'0'),
b'a'..=b'f' => Some(b - b'a' + 10),
b'A'..=b'F' => Some(b - b'A' + 10),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn percent_decode_path_no_encoding() {
let decoded = percent_decode_path("/simple/path").unwrap();
assert!(matches!(decoded, Cow::Borrowed(_)));
assert_eq!(&*decoded, "/simple/path");
}
#[test]
fn percent_decode_path_simple() {
assert_eq!(
&*percent_decode_path("/hello%20world").unwrap(),
"/hello world"
);
assert_eq!(&*percent_decode_path("%2F").unwrap(), "/");
}
#[test]
fn percent_decode_path_utf8() {
assert_eq!(&*percent_decode_path("/caf%C3%A9").unwrap(), "/café");
}
#[test]
fn percent_decode_path_plus_preserved() {
assert_eq!(&*percent_decode_path("/a+b").unwrap(), "/a+b");
}
#[test]
fn percent_decode_path_rejects_invalid_utf8() {
assert!(percent_decode_path("/%C0%AF").is_err());
}
#[test]
fn parse_request_line_decodes_path() {
let line = b"GET /hello%20world HTTP/1.1";
let (_method, path, _query, _version) =
parse_request_line(line).expect("parse request line");
assert_eq!(path, "/hello world");
}
#[test]
fn request_line_simple_get() {
let buffer = b"GET /path HTTP/1.1\r\n";
let line = RequestLine::parse(buffer).unwrap();
assert_eq!(line.method(), Method::Get);
assert_eq!(line.path(), "/path");
assert_eq!(line.query(), None);
assert_eq!(line.version(), "HTTP/1.1");
assert!(line.is_http11());
assert!(!line.is_http10());
}
#[test]
fn request_line_with_query() {
let buffer = b"GET /items?q=test&page=1 HTTP/1.1\r\n";
let line = RequestLine::parse(buffer).unwrap();
assert_eq!(line.method(), Method::Get);
assert_eq!(line.uri(), "/items?q=test&page=1");
assert_eq!(line.path(), "/items");
assert_eq!(line.query(), Some("q=test&page=1"));
assert_eq!(line.version(), "HTTP/1.1");
}
#[test]
fn request_line_uri_without_query_matches_path() {
let buffer = b"GET /path HTTP/1.1\r\n";
let line = RequestLine::parse(buffer).unwrap();
assert_eq!(line.uri(), "/path");
assert_eq!(line.path(), "/path");
assert_eq!(line.query(), None);
}
#[test]
fn request_line_post() {
let buffer = b"POST /api/users HTTP/1.1\r\n";
let line = RequestLine::parse(buffer).unwrap();
assert_eq!(line.method(), Method::Post);
assert_eq!(line.path(), "/api/users");
}
#[test]
fn request_line_all_methods() {
let methods = [
("GET", Method::Get),
("POST", Method::Post),
("PUT", Method::Put),
("DELETE", Method::Delete),
("PATCH", Method::Patch),
("OPTIONS", Method::Options),
("HEAD", Method::Head),
("TRACE", Method::Trace),
];
for (method_str, expected_method) in methods {
let buffer = format!("{method_str} /path HTTP/1.1\r\n");
let line = RequestLine::parse(buffer.as_bytes()).unwrap();
assert_eq!(line.method(), expected_method, "Failed for {method_str}");
}
}
#[test]
fn request_line_http10() {
let buffer = b"GET /legacy HTTP/1.0\r\n";
let line = RequestLine::parse(buffer).unwrap();
assert_eq!(line.version(), "HTTP/1.0");
assert!(line.is_http10());
assert!(!line.is_http11());
}
#[test]
fn request_line_without_crlf() {
let buffer = b"GET /path HTTP/1.1";
let line = RequestLine::parse(buffer).unwrap();
assert_eq!(line.method(), Method::Get);
assert_eq!(line.path(), "/path");
}
#[test]
fn request_line_with_len() {
let buffer = b"GET /path HTTP/1.1\r\nHost: example.com\r\n";
let (line, consumed) = RequestLine::parse_with_len(buffer).unwrap();
assert_eq!(line.method(), Method::Get);
assert_eq!(line.path(), "/path");
assert_eq!(consumed, 20); }
#[test]
fn request_line_invalid_method() {
let buffer = b"INVALID /path HTTP/1.1\r\n";
let result = RequestLine::parse(buffer);
assert!(matches!(result, Err(ParseError::InvalidMethod)));
}
#[test]
fn request_line_missing_path() {
let buffer = b"GET\r\n";
let result = RequestLine::parse(buffer);
assert!(matches!(result, Err(ParseError::InvalidRequestLine)));
}
#[test]
fn request_line_missing_version() {
let buffer = b"GET /path\r\n";
let result = RequestLine::parse(buffer);
assert!(matches!(result, Err(ParseError::InvalidRequestLine)));
}
#[test]
fn request_line_complex_path() {
let buffer = b"GET /api/v1/users/123/posts HTTP/1.1\r\n";
let line = RequestLine::parse(buffer).unwrap();
assert_eq!(line.path(), "/api/v1/users/123/posts");
}
#[test]
fn request_line_query_with_special_chars() {
let buffer = b"GET /search?q=hello%20world&filter=a%3Db HTTP/1.1\r\n";
let line = RequestLine::parse(buffer).unwrap();
assert_eq!(line.path(), "/search");
assert_eq!(line.query(), Some("q=hello%20world&filter=a%3Db"));
}
#[test]
fn header_simple() {
let buffer = b"Host: example.com\r\n";
let mut iter = HeadersIter::new(buffer);
let header = iter.next().unwrap().unwrap();
assert_eq!(header.name(), "Host");
assert_eq!(header.value(), b"example.com");
assert_eq!(header.value_str(), Some("example.com"));
assert!(iter.next().is_none());
}
#[test]
fn headers_multiple() {
let buffer =
b"Host: example.com\r\nContent-Type: application/json\r\nContent-Length: 42\r\n";
let headers: Vec<_> = HeadersIter::new(buffer).collect();
assert_eq!(headers.len(), 3);
let h0 = headers[0].as_ref().unwrap();
assert_eq!(h0.name(), "Host");
assert_eq!(h0.value_str(), Some("example.com"));
let h1 = headers[1].as_ref().unwrap();
assert_eq!(h1.name(), "Content-Type");
assert_eq!(h1.value_str(), Some("application/json"));
let h2 = headers[2].as_ref().unwrap();
assert_eq!(h2.name(), "Content-Length");
assert_eq!(h2.value_str(), Some("42"));
}
#[test]
fn headers_with_empty_line() {
let buffer = b"Host: example.com\r\n\r\n";
let headers: Vec<_> = HeadersIter::new(buffer).collect();
assert_eq!(headers.len(), 1);
}
#[test]
fn header_with_leading_space() {
let buffer = b"Host: example.com\r\n";
let mut iter = HeadersIter::new(buffer);
let header = iter.next().unwrap().unwrap();
assert_eq!(header.name(), "Host");
assert_eq!(header.value_str(), Some("example.com"));
}
#[test]
fn header_binary_value() {
let buffer = b"X-Binary: \xff\xfe\r\n";
let mut iter = HeadersIter::new(buffer);
let header = iter.next().unwrap().unwrap();
assert_eq!(header.name(), "X-Binary");
assert_eq!(header.value(), b"\xff\xfe");
assert!(header.value_str().is_none()); }
#[test]
fn header_missing_colon() {
let buffer = b"InvalidHeader\r\n";
let mut iter = HeadersIter::new(buffer);
let result = iter.next().unwrap();
assert!(matches!(result, Err(ParseError::InvalidHeader)));
}
#[test]
fn request_line_borrows_from_buffer() {
let buffer = b"GET /borrowed/path HTTP/1.1\r\n";
let line = RequestLine::parse(buffer).unwrap();
let buffer_range = buffer.as_ptr_range();
let path_ptr = line.path().as_ptr();
assert!(buffer_range.contains(&path_ptr));
}
#[test]
fn header_borrows_from_buffer() {
let buffer = b"Host: borrowed.example.com\r\n";
let mut iter = HeadersIter::new(buffer);
let header = iter.next().unwrap().unwrap();
let buffer_range = buffer.as_ptr_range();
let value_ptr = header.value().as_ptr();
assert!(buffer_range.contains(&value_ptr));
}
#[test]
fn header_as_bytes_pair() {
let buffer = b"Content-Type: application/json\r\n";
let mut iter = HeadersIter::new(buffer);
let header = iter.next().unwrap().unwrap();
let (name, value) = header.as_bytes_pair();
assert_eq!(name, b"Content-Type");
assert_eq!(value, b"application/json");
}
#[test]
fn header_name_bytes() {
let buffer = b"X-Custom-Header: value\r\n";
let mut iter = HeadersIter::new(buffer);
let header = iter.next().unwrap().unwrap();
assert_eq!(header.name_bytes(), b"X-Custom-Header");
assert_eq!(header.name(), "X-Custom-Header");
}
#[test]
fn header_case_insensitive_match() {
let buffer = b"content-type: text/html\r\n";
let mut iter = HeadersIter::new(buffer);
let header = iter.next().unwrap().unwrap();
assert!(header.name_eq_ignore_case("Content-Type"));
assert!(header.name_eq_ignore_case("CONTENT-TYPE"));
assert!(header.name_eq_ignore_case("content-type"));
assert!(!header.name_eq_ignore_case("Content-Length"));
}
#[test]
fn header_content_length_detection() {
let buffer = b"Content-Length: 1234\r\n";
let mut iter = HeadersIter::new(buffer);
let header = iter.next().unwrap().unwrap();
assert!(header.is_content_length());
assert!(!header.is_transfer_encoding());
assert_eq!(header.as_content_length(), Some(1234));
}
#[test]
fn header_content_length_case_insensitive() {
let buffer = b"content-length: 5678\r\n";
let mut iter = HeadersIter::new(buffer);
let header = iter.next().unwrap().unwrap();
assert!(header.is_content_length());
assert_eq!(header.as_content_length(), Some(5678));
}
#[test]
fn header_transfer_encoding_chunked() {
let buffer = b"Transfer-Encoding: chunked\r\n";
let mut iter = HeadersIter::new(buffer);
let header = iter.next().unwrap().unwrap();
assert!(header.is_transfer_encoding());
assert!(header.is_chunked_encoding());
assert!(!header.is_content_length());
}
#[test]
fn header_transfer_encoding_gzip_chunked_is_not_chunked() {
let buffer = b"Transfer-Encoding: gzip, chunked\r\n";
let mut iter = HeadersIter::new(buffer);
let header = iter.next().unwrap().unwrap();
assert!(header.is_transfer_encoding());
assert!(!header.is_chunked_encoding());
}
#[test]
fn header_transfer_encoding_not_chunked() {
let buffer = b"Transfer-Encoding: gzip\r\n";
let mut iter = HeadersIter::new(buffer);
let header = iter.next().unwrap().unwrap();
assert!(header.is_transfer_encoding());
assert!(!header.is_chunked_encoding());
}
#[test]
fn headers_parser_content_length() {
let buffer = b"Host: example.com\r\nContent-Length: 42\r\n\r\n";
let parser = HeadersParser::parse(buffer).unwrap();
assert_eq!(parser.content_length(), Some(42));
assert!(!parser.is_chunked());
assert_eq!(parser.body_length(), BodyLength::ContentLength(42));
}
#[test]
fn headers_parser_chunked() {
let buffer = b"Host: example.com\r\nTransfer-Encoding: chunked\r\n\r\n";
let parser = HeadersParser::parse(buffer).unwrap();
assert_eq!(parser.content_length(), None);
assert!(parser.is_chunked());
assert_eq!(parser.body_length(), BodyLength::Chunked);
}
#[test]
fn headers_parser_no_body() {
let buffer = b"Host: example.com\r\nAccept: */*\r\n\r\n";
let parser = HeadersParser::parse(buffer).unwrap();
assert_eq!(parser.content_length(), None);
assert!(!parser.is_chunked());
assert_eq!(parser.body_length(), BodyLength::None);
}
#[test]
fn headers_parser_chunked_takes_precedence() {
let buffer =
b"Host: example.com\r\nContent-Length: 100\r\nTransfer-Encoding: chunked\r\n\r\n";
let result = HeadersParser::parse(buffer);
assert!(matches!(result, Err(ParseError::AmbiguousBodyLength)));
}
#[test]
fn headers_parser_invalid_transfer_encoding() {
let buffer = b"Host: example.com\r\nTransfer-Encoding: gzip\r\n\r\n";
let result = HeadersParser::parse(buffer);
assert!(matches!(result, Err(ParseError::InvalidTransferEncoding)));
}
#[test]
fn headers_parser_bytes_consumed() {
let buffer = b"Host: example.com\r\nContent-Length: 5\r\n\r\nHello";
let parser = HeadersParser::parse(buffer).unwrap();
assert_eq!(parser.bytes_consumed(), 40);
}
#[test]
fn headers_parser_get_header() {
let buffer = b"Host: example.com\r\nContent-Type: application/json\r\n\r\n";
let parser = HeadersParser::parse(buffer).unwrap();
let host = parser.get("Host").unwrap();
assert_eq!(host.value_str(), Some("example.com"));
let ct = parser.get("content-type").unwrap();
assert_eq!(ct.value_str(), Some("application/json"));
assert!(parser.get("X-Missing").is_none());
}
#[test]
fn headers_parser_get_all() {
let buffer = b"Set-Cookie: a=1\r\nSet-Cookie: b=2\r\nHost: example.com\r\n\r\n";
let parser = HeadersParser::parse(buffer).unwrap();
let cookies: Vec<_> = parser.get_all("Set-Cookie").collect();
assert_eq!(cookies.len(), 2);
assert_eq!(cookies[0].value_str(), Some("a=1"));
assert_eq!(cookies[1].value_str(), Some("b=2"));
}
#[test]
fn headers_parser_iter() {
let buffer = b"A: 1\r\nB: 2\r\nC: 3\r\n\r\n";
let parser = HeadersParser::parse(buffer).unwrap();
let headers: Vec<_> = parser.iter().collect();
assert_eq!(headers.len(), 3);
}
#[test]
fn headers_parser_conflicting_content_length() {
let buffer = b"Content-Length: 10\r\nContent-Length: 20\r\n\r\n";
let result = HeadersParser::parse(buffer);
assert!(matches!(result, Err(ParseError::InvalidHeader)));
}
#[test]
fn headers_parser_duplicate_same_content_length() {
let buffer = b"Content-Length: 42\r\nContent-Length: 42\r\n\r\n";
let parser = HeadersParser::parse(buffer).unwrap();
assert_eq!(parser.content_length(), Some(42));
}
#[test]
fn headers_parser_too_many_headers() {
let mut limits = ParseLimits::default();
limits.max_header_count = 1;
let buffer = b"A: 1\r\nB: 2\r\n\r\n";
let result = HeadersParser::parse_with_limits(buffer, &limits);
assert!(matches!(result, Err(ParseError::TooManyHeaders)));
}
#[test]
fn headers_parser_header_line_too_long() {
let mut limits = ParseLimits::default();
limits.max_header_line_len = 8;
let buffer = b"Long-Header: 123\r\n\r\n";
let result = HeadersParser::parse_with_limits(buffer, &limits);
assert!(matches!(result, Err(ParseError::HeaderLineTooLong)));
}
#[test]
fn headers_parser_headers_too_large() {
let mut limits = ParseLimits::default();
limits.max_headers_size = 7;
let buffer = b"A: 1\r\n\r\n";
let result = HeadersParser::parse_with_limits(buffer, &limits);
assert!(matches!(result, Err(ParseError::HeadersTooLarge)));
}
#[test]
fn headers_parser_invalid_header_name() {
let buffer = b"Bad Header: value\r\n\r\n";
let result = HeadersParser::parse(buffer);
assert!(matches!(result, Err(ParseError::InvalidHeaderName)));
}
#[test]
fn headers_parser_invalid_header_bytes() {
let buffer = b"X-Test: hi\0there\r\n\r\n";
let result = HeadersParser::parse(buffer);
assert!(matches!(result, Err(ParseError::InvalidHeaderBytes)));
}
#[test]
fn request_line_too_long() {
let mut limits = ParseLimits::default();
limits.max_request_line_len = 8;
let mut parser = StatefulParser::new().with_limits(limits);
let result = parser.feed(b"GET /toolong HTTP/1.1\r\n\r\n");
assert!(matches!(result, Err(ParseError::RequestLineTooLong)));
}
#[test]
fn stateful_parser_content_length_partial() {
let mut parser = StatefulParser::new();
let full = b"GET /hello HTTP/1.1\r\nHost: example.com\r\nContent-Length: 5\r\n\r\nHello";
let status = parser.feed(&full[..full.len() - 3]).unwrap();
assert!(matches!(status, ParseStatus::Incomplete));
let status = parser.feed(&full[full.len() - 3..]).unwrap();
let (request, consumed) = match status {
ParseStatus::Complete { request, consumed } => (request, consumed),
ParseStatus::Incomplete => panic!("expected complete request"),
};
assert_eq!(consumed, full.len());
assert_eq!(request.method(), Method::Get);
assert_eq!(request.path(), "/hello");
assert!(request.query().is_none());
match request.body() {
Body::Bytes(bytes) => assert_eq!(bytes, b"Hello"),
_ => panic!("expected bytes body"),
}
assert_eq!(parser.buffered_len(), 0);
}
#[test]
fn stateful_parser_headers_partial() {
let mut parser = StatefulParser::new();
let part1 = b"GET /x HTTP/1.1\r\nHost: example.com\r\nContent-Length: 5\r\n";
let part2 = b"\r\nHello";
let status = parser.feed(part1).unwrap();
assert!(matches!(status, ParseStatus::Incomplete));
let status = parser.feed(part2).unwrap();
let (request, consumed) = match status {
ParseStatus::Complete { request, consumed } => (request, consumed),
ParseStatus::Incomplete => panic!("expected complete request"),
};
assert_eq!(consumed, part1.len() + part2.len());
assert_eq!(request.path(), "/x");
match request.body() {
Body::Bytes(bytes) => assert_eq!(bytes, b"Hello"),
_ => panic!("expected bytes body"),
}
}
#[test]
fn stateful_parser_chunked_body() {
let mut parser = StatefulParser::new();
let full = b"GET /chunk HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n\
4\r\nWiki\r\n5\r\npedia\r\n0\r\n\r\n";
let status = parser.feed(full).unwrap();
let (request, consumed) = match status {
ParseStatus::Complete { request, consumed } => (request, consumed),
ParseStatus::Incomplete => panic!("expected complete request"),
};
assert_eq!(consumed, full.len());
assert_eq!(request.path(), "/chunk");
match request.body() {
Body::Bytes(bytes) => assert_eq!(bytes, b"Wikipedia"),
_ => panic!("expected bytes body"),
}
}
#[test]
fn stateful_parser_pipelined_requests() {
let mut parser = StatefulParser::new();
let req1 = b"GET /a HTTP/1.1\r\nContent-Length: 1\r\n\r\na";
let req2 = b"GET /b HTTP/1.1\r\nContent-Length: 1\r\n\r\nb";
let mut combined = Vec::new();
combined.extend_from_slice(req1);
combined.extend_from_slice(req2);
let status = parser.feed(&combined).unwrap();
let (request, consumed) = match status {
ParseStatus::Complete { request, consumed } => (request, consumed),
ParseStatus::Incomplete => panic!("expected complete request"),
};
assert_eq!(consumed, req1.len());
assert_eq!(request.path(), "/a");
assert_eq!(parser.buffered_len(), req2.len());
let status = parser.feed(&[]).unwrap();
let (request, consumed) = match status {
ParseStatus::Complete { request, consumed } => (request, consumed),
ParseStatus::Incomplete => panic!("expected second request"),
};
assert_eq!(consumed, req2.len());
assert_eq!(request.path(), "/b");
assert_eq!(parser.buffered_len(), 0);
}
#[test]
fn security_rejects_cl_te_smuggling_attempt() {
let buffer =
b"POST /admin HTTP/1.1\r\nContent-Length: 13\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\nSMUGGLED";
let parser = Parser::new();
let result = parser.parse(buffer);
assert!(
result.is_err() || {
let req = result.unwrap();
!matches!(req.body(), Body::Bytes(b) if b == b"SMUGGLED")
}
);
}
#[test]
fn security_ambiguous_body_length_rejected() {
let buffer = b"Content-Length: 100\r\nTransfer-Encoding: chunked\r\n\r\n";
let result = HeadersParser::parse(buffer);
assert!(matches!(result, Err(ParseError::AmbiguousBodyLength)));
}
#[test]
fn security_crlf_injection_in_path_rejected() {
let buffer = b"GET /path\r\nX-Injected: evil HTTP/1.1\r\nHost: example.com\r\n\r\n";
let result = RequestLine::parse(buffer);
match result {
Err(_) => {} Ok(line) => {
assert!(!line.path().contains('\r'));
assert!(!line.path().contains('\n'));
}
}
}
#[test]
fn security_null_byte_in_request_line_rejected() {
let buffer = b"GET /path\x00evil HTTP/1.1\r\n";
let result = RequestLine::parse(buffer);
assert!(matches!(result, Err(ParseError::InvalidRequestLine)));
}
#[test]
fn security_null_byte_in_header_name_rejected() {
let buffer = b"X-Test\x00Header: value\r\n\r\n";
let result = HeadersParser::parse(buffer);
assert!(result.is_err());
}
#[test]
fn security_header_injection_via_value() {
let buffer = b"X-Test: value\r\nX-Injected: evil\r\n\r\n";
let parser = HeadersParser::parse(buffer).unwrap();
assert!(parser.get("X-Test").is_some());
assert!(parser.get("X-Injected").is_some());
}
#[test]
fn security_oversized_chunk_size_rejected() {
let mut parser = StatefulParser::new();
let buffer = b"GET /chunk HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\nFFFFFFFFFFFFFFFFF\r\n";
let result = parser.feed(buffer);
assert!(matches!(result, Err(ParseError::InvalidHeader)));
}
#[test]
fn security_negative_content_length_rejected() {
let buffer = b"Content-Length: -1\r\n\r\n";
let result = HeadersParser::parse(buffer);
assert!(matches!(result, Err(ParseError::InvalidHeader)));
}
#[test]
fn security_content_length_overflow() {
let buffer = b"Content-Length: 99999999999999999999999999\r\n\r\n";
let result = HeadersParser::parse(buffer);
assert!(matches!(result, Err(ParseError::InvalidHeader)));
}
#[test]
fn security_request_line_space_injection() {
let buffer = b"GET /path HTTP/1.1\r\n";
let result = RequestLine::parse(buffer);
match result {
Ok(line) => {
let _ = line.path();
let _ = line.version();
}
Err(_) => {} }
}
#[test]
fn security_obs_fold_header_rejected() {
let buffer = b"X-Test: value\r\n continuation\r\n\r\n";
let result = HeadersParser::parse(buffer);
assert!(matches!(result, Err(ParseError::InvalidHeader)));
}
#[test]
fn security_duplicate_transfer_encoding() {
let buffer = b"Transfer-Encoding: chunked\r\nTransfer-Encoding: chunked\r\n\r\n";
let parser = HeadersParser::parse(buffer).unwrap();
assert!(parser.is_chunked());
}
#[test]
fn edge_root_path() {
let buffer = b"GET / HTTP/1.1\r\n";
let line = RequestLine::parse(buffer).unwrap();
assert_eq!(line.path(), "/");
assert_eq!(line.query(), None);
}
#[test]
fn edge_root_path_with_query() {
let buffer = b"GET /?key=value HTTP/1.1\r\n";
let line = RequestLine::parse(buffer).unwrap();
assert_eq!(line.path(), "/");
assert_eq!(line.query(), Some("key=value"));
}
#[test]
fn edge_empty_query_string() {
let buffer = b"GET /path? HTTP/1.1\r\n";
let line = RequestLine::parse(buffer).unwrap();
assert_eq!(line.path(), "/path");
assert_eq!(line.query(), Some(""));
}
#[test]
fn edge_double_slashes_in_path() {
let buffer = b"GET //api//v1//users HTTP/1.1\r\n";
let line = RequestLine::parse(buffer).unwrap();
assert_eq!(line.path(), "//api//v1//users");
}
#[test]
fn edge_dot_segments_in_path() {
let buffer = b"GET /api/../admin/./config HTTP/1.1\r\n";
let line = RequestLine::parse(buffer).unwrap();
assert_eq!(line.path(), "/api/../admin/./config");
}
#[test]
fn edge_percent_encoded_slash() {
let buffer = b"GET /path%2Fwith%2Fslashes HTTP/1.1\r\n";
let line = RequestLine::parse(buffer).unwrap();
assert!(line.path().contains("%2F") || line.path().contains("/with/"));
}
#[test]
fn edge_very_long_path() {
let long_path = "/".to_string() + &"a".repeat(4000);
let buffer = format!("GET {} HTTP/1.1\r\n", long_path);
let line = RequestLine::parse(buffer.as_bytes()).unwrap();
assert_eq!(line.path().len(), 4001);
}
#[test]
fn edge_unicode_in_path() {
let buffer = b"GET /caf\xc3\xa9 HTTP/1.1\r\n";
let result = RequestLine::parse(buffer);
match result {
Ok(line) => assert!(line.path().len() > 0),
Err(_) => {} }
}
#[test]
fn edge_http_version_http10() {
let buffer = b"GET /path HTTP/1.0\r\n";
let line = RequestLine::parse(buffer).unwrap();
assert!(line.is_http10());
assert!(!line.is_http11());
}
#[test]
fn edge_http_version_unknown() {
let buffer = b"GET /path HTTP/2.0\r\n";
let line = RequestLine::parse(buffer).unwrap();
assert_eq!(line.version(), "HTTP/2.0");
assert!(!line.is_http10());
assert!(!line.is_http11());
}
#[test]
fn edge_lowercase_method_rejected() {
let buffer = b"get /path HTTP/1.1\r\n";
let result = RequestLine::parse(buffer);
assert!(matches!(result, Err(ParseError::InvalidMethod)));
}
#[test]
fn edge_mixed_case_method_rejected() {
let buffer = b"Get /path HTTP/1.1\r\n";
let result = RequestLine::parse(buffer);
assert!(matches!(result, Err(ParseError::InvalidMethod)));
}
#[test]
fn edge_connect_method() {
let buffer = b"CONNECT example.com:443 HTTP/1.1\r\n";
let result = RequestLine::parse(buffer);
match result {
Ok(line) => assert_eq!(line.path(), "example.com:443"),
Err(ParseError::InvalidMethod) => {} Err(_) => panic!("unexpected error"),
}
}
#[test]
fn edge_empty_header_value() {
let buffer = b"X-Empty:\r\n\r\n";
let parser = HeadersParser::parse(buffer).unwrap();
let header = parser.get("X-Empty").unwrap();
assert_eq!(header.value(), b"");
}
#[test]
fn edge_header_with_only_spaces() {
let buffer = b"X-Spaces: \r\n\r\n";
let parser = HeadersParser::parse(buffer).unwrap();
let header = parser.get("X-Spaces").unwrap();
assert!(header.value().is_empty() || header.value() == b" ");
}
#[test]
fn edge_very_long_header_value() {
let long_value = "x".repeat(7000);
let buffer = format!("X-Long: {}\r\n\r\n", long_value);
let parser = HeadersParser::parse(buffer.as_bytes()).unwrap();
let header = parser.get("X-Long").unwrap();
assert_eq!(header.value().len(), 7000);
}
#[test]
fn edge_header_with_colon_in_value() {
let buffer = b"X-Time: 12:30:45\r\n\r\n";
let parser = HeadersParser::parse(buffer).unwrap();
let header = parser.get("X-Time").unwrap();
assert_eq!(header.value_str(), Some("12:30:45"));
}
#[test]
fn edge_header_name_with_numbers() {
let buffer = b"X-Header-123: value\r\n\r\n";
let parser = HeadersParser::parse(buffer).unwrap();
assert!(parser.get("X-Header-123").is_some());
}
#[test]
fn edge_header_value_with_utf8() {
let buffer = "X-Message: Hello, 世界!\r\n\r\n".as_bytes();
let parser = HeadersParser::parse(buffer).unwrap();
let header = parser.get("X-Message").unwrap();
assert!(header.value_str().is_some());
}
#[test]
fn edge_many_small_headers() {
let mut buffer = String::new();
for i in 0..50 {
buffer.push_str(&format!("X-H{}: v{}\r\n", i, i));
}
buffer.push_str("\r\n");
let parser = HeadersParser::parse(buffer.as_bytes()).unwrap();
assert!(parser.get("X-H0").is_some());
assert!(parser.get("X-H49").is_some());
}
#[test]
fn edge_content_length_with_leading_zeros() {
let buffer = b"Content-Length: 00042\r\n\r\n";
let parser = HeadersParser::parse(buffer).unwrap();
assert_eq!(parser.content_length(), Some(42));
}
#[test]
fn edge_content_length_with_whitespace() {
let buffer = b"Content-Length: 42 \r\n\r\n";
let parser = HeadersParser::parse(buffer).unwrap();
assert_eq!(parser.content_length(), Some(42));
}
#[test]
fn parser_simple_get() {
let parser = Parser::new();
let buffer = b"GET /api/users HTTP/1.1\r\nHost: example.com\r\n\r\n";
let request = parser.parse(buffer).unwrap();
assert_eq!(request.method(), Method::Get);
assert_eq!(request.path(), "/api/users");
assert!(request.query().is_none());
}
#[test]
fn parser_post_with_json_body() {
let parser = Parser::new();
let buffer = b"POST /api/items HTTP/1.1\r\nHost: example.com\r\nContent-Type: application/json\r\nContent-Length: 13\r\n\r\n{\"id\": \"123\"}";
let request = parser.parse(buffer).unwrap();
assert_eq!(request.method(), Method::Post);
assert_eq!(request.path(), "/api/items");
match request.body() {
Body::Bytes(bytes) => assert_eq!(bytes, b"{\"id\": \"123\"}"),
_ => panic!("expected bytes body"),
}
}
#[test]
fn parser_request_with_query() {
let parser = Parser::new();
let buffer = b"GET /search?q=rust&limit=10 HTTP/1.1\r\nHost: example.com\r\n\r\n";
let request = parser.parse(buffer).unwrap();
assert_eq!(request.path(), "/search");
assert_eq!(
request.query(),
Some("q=rust&limit=10".to_string()).as_deref()
);
}
#[test]
fn parser_max_size_respected() {
let parser = Parser::new().with_max_size(50);
let buffer = b"GET /path HTTP/1.1\r\nHost: example.com\r\nX-Long: this is a very long header that exceeds the limit\r\n\r\n";
let result = parser.parse(buffer);
assert!(matches!(result, Err(ParseError::TooLarge)));
}
#[test]
fn parser_custom_limits() {
let limits = ParseLimits {
max_request_size: 1024,
max_request_line_len: 100,
max_header_count: 10,
max_header_line_len: 200,
max_headers_size: 500,
};
let parser = Parser::new().with_limits(limits);
let buffer = b"GET /path HTTP/1.1\r\nHost: example.com\r\n\r\n";
let request = parser.parse(buffer).unwrap();
assert_eq!(request.method(), Method::Get);
}
#[test]
fn parser_incomplete_request() {
let parser = Parser::new();
let buffer = b"GET /path HTTP/1.1\r\nHost: example.com\r\n";
let result = parser.parse(buffer);
assert!(matches!(result, Err(ParseError::Incomplete)));
}
#[test]
fn parser_preserves_headers() {
let parser = Parser::new();
let buffer =
b"GET /path HTTP/1.1\r\nHost: example.com\r\nX-Custom: my-value\r\nAccept: */*\r\n\r\n";
let request = parser.parse(buffer).unwrap();
assert!(request.headers().get("Host").is_some());
assert!(request.headers().get("X-Custom").is_some());
assert!(request.headers().get("Accept").is_some());
}
#[test]
fn malformed_empty_input() {
let parser = Parser::new();
let result = parser.parse(b"");
assert!(matches!(result, Err(ParseError::Incomplete)));
}
#[test]
fn malformed_only_crlf() {
let parser = Parser::new();
let result = parser.parse(b"\r\n\r\n");
assert!(result.is_err());
}
#[test]
fn malformed_no_method() {
let buffer = b"/path HTTP/1.1\r\n\r\n";
let result = RequestLine::parse(buffer);
assert!(result.is_err());
}
#[test]
fn malformed_garbage_input() {
let parser = Parser::new();
let result = parser.parse(b"not a valid http request at all");
assert!(result.is_err());
}
#[test]
fn malformed_binary_garbage() {
let parser = Parser::new();
let result = parser.parse(b"\x00\x01\x02\x03\x04\x05\r\n\r\n");
assert!(result.is_err());
}
#[test]
fn malformed_tab_instead_of_space() {
let buffer = b"GET\t/path\tHTTP/1.1\r\n\r\n";
let result = RequestLine::parse(buffer);
assert!(result.is_err());
}
#[test]
fn malformed_lf_only_line_ending() {
let buffer = b"GET /path HTTP/1.1\nHost: example.com\n\n";
let parser = Parser::new();
let result = parser.parse(buffer);
assert!(result.is_err());
}
#[test]
fn malformed_cr_only_line_ending() {
let buffer = b"GET /path HTTP/1.1\rHost: example.com\r\r";
let parser = Parser::new();
let result = parser.parse(buffer);
assert!(result.is_err());
}
#[test]
fn stateful_parser_clear_resets_state() {
let mut parser = StatefulParser::new();
parser.feed(b"GET /partial").unwrap();
assert!(parser.buffered_len() > 0);
parser.clear();
assert_eq!(parser.buffered_len(), 0);
let result = parser
.feed(b"GET /new HTTP/1.1\r\nHost: example.com\r\n\r\n")
.unwrap();
assert!(matches!(result, ParseStatus::Complete { .. }));
}
#[test]
fn stateful_parser_byte_at_a_time() {
let mut parser = StatefulParser::new();
let request = b"GET /path HTTP/1.1\r\nHost: x\r\n\r\n";
for (i, byte) in request.iter().enumerate() {
let result = parser.feed(&[*byte]).unwrap();
if i == request.len() - 1 {
assert!(matches!(result, ParseStatus::Complete { .. }));
} else {
assert!(matches!(result, ParseStatus::Incomplete));
}
}
}
#[test]
fn stateful_parser_with_body_config() {
use crate::body::BodyConfig;
let config = BodyConfig::new().with_max_size(10);
let mut parser = StatefulParser::new().with_body_config(config);
let result = parser.feed(b"GET /path HTTP/1.1\r\nContent-Length: 5\r\n\r\nHello");
assert!(result.is_ok());
}
#[test]
fn stateful_parser_pipelining_two_requests_in_one_buffer() {
let mut parser = StatefulParser::new();
let pipelined = b"GET /first HTTP/1.1\r\nHost: localhost\r\n\r\nGET /second HTTP/1.1\r\nHost: localhost\r\n\r\n";
let result = parser.feed(pipelined).unwrap();
match result {
ParseStatus::Complete { request, .. } => {
assert_eq!(request.path(), "/first");
}
ParseStatus::Incomplete => panic!("expected first request"),
}
let result = parser.feed(&[]).unwrap();
match result {
ParseStatus::Complete { request, .. } => {
assert_eq!(request.path(), "/second");
}
ParseStatus::Incomplete => panic!("expected second pipelined request"),
}
}
#[test]
fn stateful_parser_pipelining_three_requests() {
let mut parser = StatefulParser::new();
let pipelined = b"GET /a HTTP/1.1\r\nHost: h\r\n\r\n\
GET /b HTTP/1.1\r\nHost: h\r\n\r\n\
GET /c HTTP/1.1\r\nHost: h\r\n\r\n";
let r1 = parser.feed(pipelined).unwrap();
assert!(matches!(r1, ParseStatus::Complete { .. }));
let r2 = parser.feed(&[]).unwrap();
assert!(matches!(r2, ParseStatus::Complete { .. }));
let r3 = parser.feed(&[]).unwrap();
assert!(matches!(r3, ParseStatus::Complete { .. }));
let r4 = parser.feed(&[]).unwrap();
assert!(matches!(r4, ParseStatus::Incomplete));
}
}