use super::*;
use crate::error::{Error, ParseErrorKind};
use crate::headers::RequestHeader;
#[test]
fn parse_simple_get() {
let raw = b"GET /some/path HTTP/1.1\r\nHost: localhost:8080\r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
assert_eq!(req.method(), Method::Get);
assert_eq!(req.version(), HttpVersion::Http11);
assert_eq!(req.path(), b"/some/path");
assert_eq!(req.header("Host"), Some(&b"localhost:8080"[..]));
assert_eq!(req.header_count(), 1);
}
#[test]
fn parse_root_path() {
let raw = b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
assert_eq!(req.path(), b"/");
}
#[test]
fn parse_path_with_query_string() {
let raw = b"GET /search?q=hello&lang=en HTTP/1.1\r\nHost: localhost\r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
assert_eq!(req.path(), b"/search?q=hello&lang=en");
}
#[test]
fn parse_path_with_fragment() {
let raw = b"GET /page#section HTTP/1.1\r\nHost: localhost\r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
assert_eq!(req.path(), b"/page#section");
}
#[test]
fn parse_path_with_percent_encoding() {
let raw = b"GET /hello%20world HTTP/1.1\r\nHost: localhost\r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
assert_eq!(req.path(), b"/hello%20world");
}
#[test]
fn parse_long_path() {
let path = "/articles/".to_string() + &"a".repeat(500);
let raw = format!("GET {path} HTTP/1.1\r\nHost: localhost\r\n\r\n");
let req: Request = Request::parse(raw.as_bytes()).unwrap();
assert_eq!(req.path(), path.as_bytes());
}
#[test]
fn parse_multiple_headers() {
let raw = b"GET / HTTP/1.1\r\n\
Host: localhost\r\n\
Accept: text/html\r\n\
Connection: keep-alive\r\n\
\r\n";
let req: Request = Request::parse(raw).unwrap();
assert_eq!(req.header_count(), 3);
assert_eq!(req.header("Host"), Some(&b"localhost"[..]));
assert_eq!(req.header("Accept"), Some(&b"text/html"[..]));
assert_eq!(req.header("Connection"), Some(&b"keep-alive"[..]));
}
#[test]
fn headers_case_insensitive() {
let raw = b"GET / HTTP/1.1\r\nHost: localhost\r\nX-Custom: value\r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
assert_eq!(req.header("x-custom"), Some(&b"value"[..]));
assert_eq!(req.header("X-CUSTOM"), Some(&b"value"[..]));
assert_eq!(req.header("X-Custom"), Some(&b"value"[..]));
}
#[test]
fn header_with_typed_enum() {
let raw = b"GET / HTTP/1.1\r\nHost: example.com\r\nUser-Agent: test/1.0\r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
assert_eq!(req.header(RequestHeader::Host), Some(&b"example.com"[..]));
assert_eq!(req.header(RequestHeader::UserAgent), Some(&b"test/1.0"[..]));
}
#[test]
fn missing_header_returns_none() {
let raw = b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
assert_eq!(req.header("X-Missing"), None);
assert_eq!(req.header(RequestHeader::Authorization), None);
}
#[test]
fn header_value_with_colon() {
let raw = b"GET / HTTP/1.1\r\nHost: localhost\r\nX-Url: http://example.com:8080/path\r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
assert_eq!(
req.header("X-Url"),
Some(&b"http://example.com:8080/path"[..])
);
}
#[test]
fn duplicate_unknown_headers_returns_first() {
let raw = b"GET / HTTP/1.1\r\nHost: localhost\r\nX-Dup: first\r\nX-Dup: second\r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
assert_eq!(req.header("X-Dup"), Some(&b"first"[..]));
assert_eq!(req.header_count(), 3);
}
#[test]
fn rejects_duplicate_host() {
let raw = b"GET / HTTP/1.1\r\nHost: a.com\r\nHost: b.com\r\n\r\n";
let err = Request::<DEFAULT_MAX_HEADERS>::parse(raw).unwrap_err();
assert!(matches!(err, Error::Parse(ParseErrorKind::DuplicateHeader)));
}
#[test]
fn rejects_duplicate_content_length() {
let raw =
b"GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\nContent-Length: 5\r\n\r\n";
let err = Request::<DEFAULT_MAX_HEADERS>::parse(raw).unwrap_err();
assert!(matches!(err, Error::Parse(ParseErrorKind::DuplicateHeader)));
}
#[test]
fn path_str_valid_utf8() {
let raw = b"GET /hello HTTP/1.1\r\nHost: localhost\r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
assert_eq!(req.path_str().unwrap(), "/hello");
}
#[test]
fn path_str_invalid_utf8() {
let mut raw = b"GET /".to_vec();
raw.extend_from_slice(&[0xFF, 0xFE]);
raw.extend_from_slice(b" HTTP/1.1\r\nHost: localhost\r\n\r\n");
let req: Request = Request::parse(&raw).unwrap();
assert!(req.path_str().is_err());
}
#[test]
fn header_str_valid_utf8() {
let raw = b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
assert_eq!(req.header_str("Host").unwrap(), Some("localhost"));
}
#[test]
fn header_str_missing() {
let raw = b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
assert_eq!(req.header_str("X-Missing").unwrap(), None);
}
#[test]
fn header_str_invalid_utf8() {
let mut raw = b"GET / HTTP/1.1\r\nHost: localhost\r\nX-Bad: ".to_vec();
raw.extend_from_slice(&[0xFF, 0xFE]);
raw.extend_from_slice(b"\r\n\r\n");
let req: Request = Request::parse(&raw).unwrap();
assert!(req.header_str("X-Bad").is_err());
}
#[test]
fn trims_leading_whitespace_from_value() {
let raw = b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
assert_eq!(req.header("Host"), Some(&b"localhost"[..]));
}
#[test]
fn trims_trailing_whitespace_from_value() {
let raw = b"GET / HTTP/1.1\r\nHost: localhost \r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
assert_eq!(req.header("Host"), Some(&b"localhost"[..]));
}
#[test]
fn trims_both_leading_and_trailing_whitespace() {
let raw = b"GET / HTTP/1.1\r\nHost: \t localhost \t \r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
assert_eq!(req.header("Host"), Some(&b"localhost"[..]));
}
#[test]
fn header_with_empty_value_after_colon() {
let raw = b"GET / HTTP/1.1\r\nHost: localhost\r\nX-Empty:\r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
assert_eq!(req.header("X-Empty"), Some(&b""[..]));
}
#[test]
fn header_with_only_whitespace_value() {
let raw = b"GET / HTTP/1.1\r\nHost: localhost\r\nX-Blank: \r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
assert_eq!(req.header("X-Blank"), Some(&b""[..]));
}
#[test]
fn method_is_get() {
let raw = b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
assert_eq!(req.method(), Method::Get);
assert_eq!(req.method().as_str(), "GET");
}
#[test]
fn version_http11() {
let raw = b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
assert_eq!(req.version(), HttpVersion::Http11);
}
#[test]
fn version_http10() {
let raw = b"GET / HTTP/1.0\r\nHost: localhost\r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
assert_eq!(req.version(), HttpVersion::Http10);
}
#[test]
fn rejects_unsupported_methods() {
for method in [b"CONNECT" as &[u8], b"TRACE", b"FOOBAR"] {
let raw = [method, b" /path HTTP/1.1\r\nHost: localhost\r\n\r\n"].concat();
let err = Request::<DEFAULT_MAX_HEADERS>::parse(&raw).unwrap_err();
assert!(matches!(
err,
Error::Parse(ParseErrorKind::UnsupportedMethod)
));
}
}
#[test]
fn parse_all_methods() {
let cases: &[(&[u8], Method)] = &[
(b"GET", Method::Get),
(b"HEAD", Method::Head),
(b"POST", Method::Post),
(b"PUT", Method::Put),
(b"DELETE", Method::Delete),
(b"PATCH", Method::Patch),
(b"OPTIONS", Method::Options),
];
for (method_bytes, expected) in cases {
let raw = [*method_bytes, b" /path HTTP/1.1\r\nHost: localhost\r\n\r\n"].concat();
let req: Request = Request::parse(&raw).unwrap();
assert_eq!(req.method(), *expected);
}
}
#[test]
fn parse_head_request() {
let raw = b"HEAD /resource HTTP/1.1\r\nHost: localhost\r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
assert_eq!(req.method(), Method::Head);
assert_eq!(req.method().as_str(), "HEAD");
assert_eq!(req.path(), b"/resource");
}
#[test]
fn empty_input() {
let err = Request::<DEFAULT_MAX_HEADERS>::parse(b"").unwrap_err();
assert!(matches!(err, Error::Parse(ParseErrorKind::NoRequestLine)));
}
#[test]
fn no_crlf_in_input() {
let err = Request::<DEFAULT_MAX_HEADERS>::parse(b"GET / HTTP/1.1").unwrap_err();
assert!(matches!(err, Error::Parse(ParseErrorKind::NoRequestLine)));
}
#[test]
fn request_line_no_spaces() {
let err = Request::<DEFAULT_MAX_HEADERS>::parse(b"GARBAGE\r\n\r\n").unwrap_err();
assert!(matches!(
err,
Error::Parse(ParseErrorKind::MalformedRequestLine)
));
}
#[test]
fn request_line_one_space_only() {
let err = Request::<DEFAULT_MAX_HEADERS>::parse(b"GET /path\r\n\r\n").unwrap_err();
assert!(matches!(
err,
Error::Parse(ParseErrorKind::MalformedRequestLine)
));
}
#[test]
fn rejects_invalid_http_version() {
let raw = b"GET / HTTP/2.0\r\nHost: localhost\r\n\r\n";
let err = Request::<DEFAULT_MAX_HEADERS>::parse(raw).unwrap_err();
assert!(matches!(
err,
Error::Parse(ParseErrorKind::UnsupportedHttpVersion)
));
}
#[test]
fn rejects_non_http_protocol() {
let raw = b"GET / SMTP/1.0\r\nHost: localhost\r\n\r\n";
let err = Request::<DEFAULT_MAX_HEADERS>::parse(raw).unwrap_err();
assert!(matches!(
err,
Error::Parse(ParseErrorKind::UnsupportedHttpVersion)
));
}
#[test]
fn accepts_http_1_0() {
let raw = b"GET / HTTP/1.0\r\nHost: localhost\r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
assert_eq!(req.path(), b"/");
}
#[test]
fn binary_path_high_bytes_allowed() {
let mut raw = b"GET /".to_vec();
raw.extend_from_slice(&[0xFF, 0xFE]);
raw.extend_from_slice(b" HTTP/1.1\r\nHost: localhost\r\n\r\n");
let req: Request = Request::parse(&raw).unwrap();
assert_eq!(&req.path()[..1], b"/");
assert!(req.path_str().is_err());
}
#[test]
fn rejects_path_with_null_byte() {
let raw = b"GET /file.txt\x00.jpg HTTP/1.1\r\nHost: localhost\r\n\r\n";
let err = Request::<DEFAULT_MAX_HEADERS>::parse(raw).unwrap_err();
assert!(matches!(
err,
Error::Parse(ParseErrorKind::MalformedRequestTarget)
));
}
#[test]
fn rejects_path_with_control_chars() {
let raw = b"GET /bad\x01path HTTP/1.1\r\nHost: localhost\r\n\r\n";
let err = Request::<DEFAULT_MAX_HEADERS>::parse(raw).unwrap_err();
assert!(matches!(
err,
Error::Parse(ParseErrorKind::MalformedRequestTarget)
));
}
#[test]
fn rejects_incomplete_headers() {
let raw = b"GET / HTTP/1.1\r\nHost: localhost\r\n";
let err = Request::<DEFAULT_MAX_HEADERS>::parse(raw).unwrap_err();
assert!(matches!(
err,
Error::Parse(ParseErrorKind::IncompleteHeaders)
));
}
#[test]
fn rejects_header_without_colon() {
let raw = b"GET / HTTP/1.1\r\nNoColonHere\r\n\r\n";
let err = Request::<DEFAULT_MAX_HEADERS>::parse(raw).unwrap_err();
assert!(matches!(err, Error::Parse(ParseErrorKind::MalformedHeader)));
}
#[test]
fn rejects_header_name_with_space() {
let raw = b"GET / HTTP/1.1\r\nBad Name: value\r\n\r\n";
let err = Request::<DEFAULT_MAX_HEADERS>::parse(raw).unwrap_err();
assert!(matches!(err, Error::Parse(ParseErrorKind::MalformedHeader)));
}
#[test]
fn rejects_empty_header_name() {
let raw = b"GET / HTTP/1.1\r\n: value\r\n\r\n";
let err = Request::<DEFAULT_MAX_HEADERS>::parse(raw).unwrap_err();
assert!(matches!(err, Error::Parse(ParseErrorKind::MalformedHeader)));
}
#[test]
fn rejects_header_name_with_control_char() {
let raw = b"GET / HTTP/1.1\r\nBad\x01Name: value\r\n\r\n";
let err = Request::<DEFAULT_MAX_HEADERS>::parse(raw).unwrap_err();
assert!(matches!(err, Error::Parse(ParseErrorKind::MalformedHeader)));
}
#[test]
fn rejects_header_value_with_nul() {
let raw = b"GET / HTTP/1.1\r\nHost: bad\x00value\r\n\r\n";
let err = Request::<DEFAULT_MAX_HEADERS>::parse(raw).unwrap_err();
assert!(matches!(err, Error::Parse(ParseErrorKind::MalformedHeader)));
}
#[test]
fn accepts_valid_tchar_header_names() {
let raw = b"GET / HTTP/1.1\r\nHost: localhost\r\nX-My_Header.v2: ok\r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
assert_eq!(req.header("X-My_Header.v2"), Some(&b"ok"[..]));
}
#[test]
fn headers_returns_parsed_slice() {
let raw = b"GET / HTTP/1.1\r\nHost: localhost\r\nAccept: text/html\r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
let hdrs = req.headers();
assert_eq!(hdrs.len(), 2);
assert_eq!(hdrs[0].name(), b"Host");
assert_eq!(hdrs[0].value(), b"localhost");
assert_eq!(hdrs[1].name(), b"Accept");
assert_eq!(hdrs[1].value(), b"text/html");
}
#[test]
fn custom_small_header_capacity_exact_fit() {
let raw = b"GET / HTTP/1.1\r\nHost: localhost\r\nA: 1\r\n\r\n";
let req: Request<'_, 2> = Request::parse(raw).unwrap();
assert_eq!(req.header_count(), 2);
assert_eq!(req.header(RequestHeader::Host), Some(&b"localhost"[..]));
assert_eq!(req.header("A"), Some(&b"1"[..]));
}
#[test]
fn too_many_headers_returns_error() {
let raw = b"GET / HTTP/1.1\r\nHost: localhost\r\nA: 1\r\nB: 2\r\n\r\n";
let err = Request::<2>::parse(raw).unwrap_err();
assert!(matches!(err, Error::Parse(ParseErrorKind::TooManyHeaders)));
}
#[test]
fn custom_large_header_capacity() {
let mut raw = b"GET / HTTP/1.1\r\nHost: localhost\r\n".to_vec();
for i in 0..63 {
raw.extend_from_slice(format!("X-H-{i}: v{i}\r\n").as_bytes());
}
raw.extend_from_slice(b"\r\n");
let req: Request<'_, 64> = Request::parse(&raw).unwrap();
assert_eq!(req.header_count(), 64);
assert_eq!(req.header("X-H-0"), Some(&b"v0"[..]));
assert_eq!(req.header("X-H-62"), Some(&b"v62"[..]));
}
#[test]
fn read_from_cursor() {
let data = b"GET /hello HTTP/1.1\r\nHost: localhost\r\n\r\n";
let mut cursor = std::io::Cursor::new(&data[..]);
let mut buf = [0u8; DEFAULT_MAX_HEADER_SIZE];
let result: ReadRequest = Request::read(&mut cursor, &mut buf).unwrap();
assert_eq!(result.request().path(), b"/hello");
assert_eq!(result.request().header("Host"), Some(&b"localhost"[..]));
assert_eq!(result.body_offset(), data.len());
assert_eq!(result.bytes_read(), data.len());
}
#[test]
fn read_returns_body_offset_and_prefetch() {
let data = b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\nbody data here";
let mut cursor = std::io::Cursor::new(&data[..]);
let mut buf = [0u8; DEFAULT_MAX_HEADER_SIZE];
let result: ReadRequest = Request::read(&mut cursor, &mut buf).unwrap();
let header_len = b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n".len();
assert_eq!(result.body_offset(), header_len);
assert_eq!(result.prefetch(), b"body data here");
}
#[test]
fn read_connection_closed() {
let mut cursor = std::io::Cursor::new(b"" as &[u8]);
let mut buf = [0u8; DEFAULT_MAX_HEADER_SIZE];
let err = Request::<DEFAULT_MAX_HEADERS>::read(&mut cursor, &mut buf).unwrap_err();
assert!(matches!(
err.error,
Error::Parse(ParseErrorKind::ConnectionClosed)
));
assert_eq!(err.bytes_read, 0);
}
#[test]
fn read_headers_too_large() {
let data = b"GET /path HTTP/1.1\r\nHost: localhost:8080\r\n\r\n";
let mut cursor = std::io::Cursor::new(&data[..]);
let mut buf = [0u8; 32];
let err = Request::<DEFAULT_MAX_HEADERS>::read(&mut cursor, &mut buf).unwrap_err();
assert!(matches!(
err.error,
Error::Parse(ParseErrorKind::HeadersTooLarge)
));
assert_eq!(err.bytes_read, 32);
}
#[test]
fn error_display_messages() {
assert_eq!(
ParseErrorKind::HeadersTooLarge.to_string(),
"headers too large"
);
assert_eq!(
ParseErrorKind::ConnectionClosed.to_string(),
"connection closed before headers complete"
);
assert_eq!(ParseErrorKind::NoRequestLine.to_string(), "no request line");
assert_eq!(
ParseErrorKind::IncompleteHeaders.to_string(),
"headers not terminated by \\r\\n\\r\\n"
);
assert_eq!(
ParseErrorKind::MalformedRequestLine.to_string(),
"malformed request line"
);
assert_eq!(
ParseErrorKind::MalformedRequestTarget.to_string(),
"request target contains invalid byte"
);
assert_eq!(
ParseErrorKind::UnsupportedMethod.to_string(),
"unsupported method"
);
assert_eq!(
ParseErrorKind::UnsupportedHttpVersion.to_string(),
"unsupported HTTP version"
);
assert_eq!(
ParseErrorKind::TooManyHeaders.to_string(),
"too many headers"
);
assert_eq!(
ParseErrorKind::MalformedHeader.to_string(),
"malformed header line"
);
assert_eq!(
ParseErrorKind::MissingHostHeader.to_string(),
"missing required Host header"
);
assert_eq!(
ParseErrorKind::DuplicateHeader.to_string(),
"duplicate header not allowed"
);
assert_eq!(
ParseErrorKind::ConflictingHeaders.to_string(),
"conflicting Transfer-Encoding and Content-Length"
);
assert_eq!(
ParseErrorKind::InvalidContentLength.to_string(),
"invalid Content-Length value"
);
assert_eq!(
ParseErrorKind::UnsupportedTransferEncoding.to_string(),
"unsupported Transfer-Encoding (only chunked is supported)"
);
}
#[test]
fn http11_requires_host_header() {
let raw = b"GET / HTTP/1.1\r\nAccept: text/html\r\n\r\n";
let err = Request::<DEFAULT_MAX_HEADERS>::parse(raw).unwrap_err();
assert!(matches!(
err,
Error::Parse(ParseErrorKind::MissingHostHeader)
));
}
#[test]
fn http10_allows_missing_host() {
let raw = b"GET / HTTP/1.0\r\nAccept: text/html\r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
assert_eq!(req.header(RequestHeader::Host), None);
}
#[test]
fn rejects_transfer_encoding_with_content_length() {
let raw = b"GET / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: chunked\r\nContent-Length: 10\r\n\r\n";
let err = Request::<DEFAULT_MAX_HEADERS>::parse(raw).unwrap_err();
assert!(matches!(
err,
Error::Parse(ParseErrorKind::ConflictingHeaders)
));
}
#[test]
fn rejects_duplicate_transfer_encoding() {
let raw = b"GET / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: identity\r\nTransfer-Encoding: chunked\r\n\r\n";
let err = Request::<DEFAULT_MAX_HEADERS>::parse(raw).unwrap_err();
assert!(matches!(err, Error::Parse(ParseErrorKind::DuplicateHeader)));
}
#[test]
fn rejects_transfer_encoding_on_bodyless_methods() {
for method in [b"GET" as &[u8], b"HEAD"] {
let raw = [
method,
b" / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: chunked\r\n\r\n",
]
.concat();
let err = Request::<DEFAULT_MAX_HEADERS>::parse(&raw).unwrap_err();
assert!(
matches!(err, Error::Parse(ParseErrorKind::ConflictingHeaders)),
"{} should reject Transfer-Encoding",
std::str::from_utf8(method).unwrap()
);
}
}
#[test]
fn allows_transfer_encoding_on_body_methods() {
for method in [b"POST" as &[u8], b"PUT", b"DELETE", b"PATCH", b"OPTIONS"] {
let raw = [
method,
b" / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: chunked\r\n\r\n",
]
.concat();
let req: Request = Request::parse(&raw).unwrap();
assert_eq!(
req.header(RequestHeader::TransferEncoding),
Some(&b"chunked"[..])
);
}
}
#[test]
fn rejects_invalid_content_length() {
let raw = b"POST / HTTP/1.1\r\nHost: localhost\r\nContent-Length: abc\r\n\r\n";
let err = Request::<DEFAULT_MAX_HEADERS>::parse(raw).unwrap_err();
assert!(matches!(
err,
Error::Parse(ParseErrorKind::InvalidContentLength)
));
}
#[test]
fn rejects_negative_content_length() {
let raw = b"POST / HTTP/1.1\r\nHost: localhost\r\nContent-Length: -1\r\n\r\n";
let err = Request::<DEFAULT_MAX_HEADERS>::parse(raw).unwrap_err();
assert!(matches!(
err,
Error::Parse(ParseErrorKind::InvalidContentLength)
));
}
#[test]
fn rejects_empty_content_length() {
let raw = b"POST / HTTP/1.1\r\nHost: localhost\r\nContent-Length: \r\n\r\n";
let err = Request::<DEFAULT_MAX_HEADERS>::parse(raw).unwrap_err();
assert!(matches!(
err,
Error::Parse(ParseErrorKind::InvalidContentLength)
));
}
#[test]
fn rejects_unsupported_transfer_encoding() {
let raw = b"POST / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: gzip\r\n\r\n";
let err = Request::<DEFAULT_MAX_HEADERS>::parse(raw).unwrap_err();
assert!(matches!(
err,
Error::Parse(ParseErrorKind::UnsupportedTransferEncoding)
));
}
#[test]
fn accepts_chunked_case_insensitive() {
let raw = b"POST / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: Chunked\r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
assert_eq!(req.body_kind(), crate::body::BodyKind::Chunked);
}
#[test]
fn rejects_transfer_encoding_on_http10() {
let raw = b"POST / HTTP/1.0\r\nHost: h\r\nTransfer-Encoding: chunked\r\n\r\n";
let err = Request::<DEFAULT_MAX_HEADERS>::parse(raw).unwrap_err();
assert!(matches!(
err,
Error::Parse(ParseErrorKind::UnsupportedTransferEncoding)
));
}
#[test]
fn rejects_double_space_in_request_line() {
let raw = b"GET /path HTTP/1.1\r\nHost: localhost\r\n\r\n";
let err = Request::<DEFAULT_MAX_HEADERS>::parse(raw).unwrap_err();
assert!(matches!(
err,
Error::Parse(ParseErrorKind::MalformedRequestTarget)
));
}
#[test]
fn rejects_trailing_space_in_path() {
let raw = b"GET /path HTTP/1.1\r\nHost: localhost\r\n\r\n";
let err = Request::<DEFAULT_MAX_HEADERS>::parse(raw).unwrap_err();
assert!(matches!(
err,
Error::Parse(
ParseErrorKind::MalformedRequestTarget | ParseErrorKind::UnsupportedHttpVersion,
)
));
}
#[test]
fn if_modified_since_o1_lookup() {
let raw = b"GET / HTTP/1.1\r\nHost: localhost\r\nIf-Modified-Since: Sat, 29 Oct 2024 19:43:31 GMT\r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
assert_eq!(
req.header(RequestHeader::IfModifiedSince),
Some(&b"Sat, 29 Oct 2024 19:43:31 GMT"[..])
);
}
#[test]
fn error_converts_to_io_error() {
let err = Error::Parse(ParseErrorKind::UnsupportedMethod);
let io_err: std::io::Error = err.into();
assert_eq!(io_err.kind(), std::io::ErrorKind::InvalidData);
}
#[test]
fn content_length_present() {
let raw = b"POST / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 42\r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
assert_eq!(req.content_length(), Some(42));
}
#[test]
fn content_length_zero() {
let raw = b"POST / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
assert_eq!(req.content_length(), Some(0));
}
#[test]
fn content_length_absent() {
let raw = b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
assert_eq!(req.content_length(), None);
}
#[test]
fn body_kind_none_for_get() {
let raw = b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
assert_eq!(req.body_kind(), crate::body::BodyKind::None);
}
#[test]
fn body_kind_content_length() {
let raw = b"POST / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 5\r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
assert_eq!(req.body_kind(), crate::body::BodyKind::ContentLength(5));
}
#[test]
fn body_kind_chunked() {
let raw = b"POST / HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: chunked\r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
assert_eq!(req.body_kind(), crate::body::BodyKind::Chunked);
}
#[test]
fn stream_body_get_no_body() {
let data = b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n";
let mut buf = [0u8; 1024];
let rr = Request::<DEFAULT_MAX_HEADERS>::read(&mut &data[..], &mut buf).unwrap();
let mut body = Vec::new();
let n = rr.stream_body_to(&mut &b""[..], &mut body).unwrap();
assert_eq!(n, 0);
assert!(body.is_empty());
}
#[test]
fn stream_body_content_length_all_in_prefetch() {
let data = b"POST /api HTTP/1.1\r\nHost: h\r\nContent-Length: 5\r\n\r\nhello";
let mut buf = [0u8; 1024];
let rr = Request::<DEFAULT_MAX_HEADERS>::read(&mut &data[..], &mut buf).unwrap();
assert_eq!(rr.request().method(), Method::Post);
assert_eq!(rr.prefetch(), b"hello");
let mut body = Vec::new();
let n = rr.stream_body_to(&mut &b""[..], &mut body).unwrap();
assert_eq!(n, 5);
assert_eq!(body, b"hello");
}
#[test]
fn stream_body_content_length_from_reader() {
let data = b"POST / HTTP/1.1\r\nHost: h\r\nContent-Length: 11\r\n\r\n";
let mut buf = [0u8; 1024];
let rr = Request::<DEFAULT_MAX_HEADERS>::read(&mut &data[..], &mut buf).unwrap();
assert!(rr.prefetch().is_empty());
let mut body = Vec::new();
let n = rr
.stream_body_to(&mut &b"hello world"[..], &mut body)
.unwrap();
assert_eq!(n, 11);
assert_eq!(body, b"hello world");
}
#[test]
fn stream_body_chunked() {
let data = b"POST / HTTP/1.1\r\nHost: h\r\nTransfer-Encoding: chunked\r\n\r\n";
let mut buf = [0u8; 1024];
let rr = Request::<DEFAULT_MAX_HEADERS>::read(&mut &data[..], &mut buf).unwrap();
let chunks = &b"5\r\nhello\r\n6\r\n world\r\n0\r\n\r\n"[..];
let mut body = Vec::new();
let n = rr.stream_body_to(&mut &*chunks, &mut body).unwrap();
assert_eq!(n, 11);
assert_eq!(body, b"hello world");
}
#[test]
fn stream_body_to_file_like_writer() {
let data = b"POST / HTTP/1.1\r\nHost: h\r\nContent-Length: 5\r\n\r\nhello";
let mut buf = [0u8; 1024];
let rr = Request::<DEFAULT_MAX_HEADERS>::read(&mut &data[..], &mut buf).unwrap();
let n = rr
.stream_body_to(&mut &b""[..], &mut std::io::sink())
.unwrap();
assert_eq!(n, 5);
}
#[test]
fn body_reader_streams_content_length() {
use std::io::Read;
let raw = b"POST / HTTP/1.1\r\nHost: h\r\nContent-Length: 5\r\n\r\nhello";
let mut buf = [0u8; 1024];
let rr = Request::<DEFAULT_MAX_HEADERS>::read(&mut &raw[..], &mut buf).unwrap();
let mut body = String::new();
rr.body_reader(&mut &b""[..])
.read_to_string(&mut body)
.unwrap();
assert_eq!(body, "hello");
}
#[test]
fn body_reader_streams_chunked() {
use std::io::Read;
let headers = b"POST / HTTP/1.1\r\nHost: h\r\nTransfer-Encoding: chunked\r\n\r\n";
let mut buf = [0u8; 1024];
let rr = Request::<DEFAULT_MAX_HEADERS>::read(&mut &headers[..], &mut buf).unwrap();
let chunks = &b"5\r\nhello\r\n6\r\n world\r\n0\r\n\r\n"[..];
let mut body = Vec::new();
rr.body_reader(&mut &*chunks)
.read_to_end(&mut body)
.unwrap();
assert_eq!(body, b"hello world");
}
#[test]
fn path_only_and_query_split_on_first_question_mark() {
let raw = b"GET /search?q=hello&lang=en HTTP/1.1\r\nHost: h\r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
assert_eq!(req.path_only(), b"/search");
assert_eq!(req.query(), b"q=hello&lang=en");
}
#[test]
fn path_only_returns_full_path_when_no_query() {
let raw = b"GET /no/query HTTP/1.1\r\nHost: h\r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
assert_eq!(req.path_only(), b"/no/query");
assert_eq!(req.query(), b"");
}
#[test]
fn query_pairs_iterates_decoded_pairs() {
let raw = b"GET /s?a=1&b=hello+world&flag HTTP/1.1\r\nHost: h\r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
let pairs: Vec<_> = req.query_pairs().collect();
assert_eq!(
pairs,
&[
(&b"a"[..], &b"1"[..]),
(&b"b"[..], &b"hello+world"[..]),
(&b"flag"[..], &b""[..]),
]
);
}
#[test]
fn path_decoded_zero_copy_when_clean() {
let raw = b"GET /no/escapes HTTP/1.1\r\nHost: h\r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
let mut out = [0u8; 64];
let got = req.path_decoded(&mut out).unwrap();
assert_eq!(got, b"/no/escapes");
assert!(std::ptr::eq(got.as_ptr(), req.path().as_ptr()));
}
#[test]
fn path_decoded_writes_when_escapes_present() {
let raw = b"GET /users/foo%20bar HTTP/1.1\r\nHost: h\r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
let mut out = [0u8; 64];
let got = req.path_decoded(&mut out).unwrap();
assert_eq!(got, b"/users/foo bar");
}
#[test]
fn content_type_returns_parsed_media_type() {
let raw = b"POST / HTTP/1.1\r\n\
Host: h\r\n\
Content-Type: application/json; charset=utf-8\r\n\
Content-Length: 0\r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
let mt = req.content_type().unwrap().unwrap();
assert_eq!(mt.type_(), b"application");
assert_eq!(mt.subtype(), b"json");
assert_eq!(mt.param("charset"), Some(&b"utf-8"[..]));
}
#[test]
fn content_type_absent_returns_none() {
let raw = b"GET / HTTP/1.1\r\nHost: h\r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
assert!(req.content_type().is_none());
}
#[test]
fn content_type_malformed_returns_some_err() {
let raw = b"POST / HTTP/1.1\r\n\
Host: h\r\n\
Content-Type: noslash\r\n\
Content-Length: 0\r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
assert!(matches!(
req.content_type(),
Some(Err(crate::error::MediaErrorKind::MissingSlash))
));
}
#[test]
fn path_decoded_buffer_too_small() {
use crate::error::{Error, PctErrorKind};
let raw = b"GET /a%20b HTTP/1.1\r\nHost: h\r\n\r\n";
let req: Request = Request::parse(raw).unwrap();
let mut out = [0u8; 2];
assert!(matches!(
req.path_decoded(&mut out),
Err(Error::Pct(PctErrorKind::BufferTooSmall))
));
}