use std::fmt;
use crate::chunked::ChunkedDecoder;
use crate::types::Http1Error;
pub const MAX_HEADER_SECTION: usize = 64 * 1024;
pub const MAX_HEADER_COUNT: usize = 256;
pub const MAX_HEADER_NAME_LEN: usize = 64;
pub const MAX_HEADER_VALUE_LEN: usize = 8192;
pub const MAX_BODY_BYTES: u64 = 64 * 1024 * 1024;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClientResponse {
pub status: u16,
pub headers: Vec<(String, String)>,
pub body: Vec<u8>,
}
#[derive(Debug)]
pub enum ClientError {
InvalidStatusLine,
InvalidHeader(String),
HeaderTooLarge,
BodyTooLarge,
InvalidChunked(String),
ProtocolInconsistency(String),
Truncated,
}
impl fmt::Display for ClientError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidStatusLine => write!(f, "upstream status line invalid"),
Self::InvalidHeader(m) => write!(f, "upstream header invalid: {m}"),
Self::HeaderTooLarge => write!(f, "upstream header section too large"),
Self::BodyTooLarge => write!(f, "upstream body too large"),
Self::InvalidChunked(m) => write!(f, "upstream chunked encoding error: {m}"),
Self::ProtocolInconsistency(m) => write!(f, "upstream protocol inconsistency: {m}"),
Self::Truncated => write!(f, "upstream response truncated"),
}
}
}
impl std::error::Error for ClientError {}
#[inline]
fn is_valid_token(s: &str) -> bool {
!s.is_empty()
&& s.bytes().all(|b| {
b.is_ascii_alphanumeric()
|| matches!(
b,
b'!' | b'#'
| b'$'
| b'%'
| b'&'
| b'\''
| b'*'
| b'+'
| b'-'
| b'.'
| b'^'
| b'_'
| b'`'
| b'|'
| b'~'
)
})
}
#[inline]
fn contains_crlf(s: &str) -> bool {
s.contains('\r') || s.contains('\n')
}
pub fn encode_request(
method: &str,
path_with_query: &str,
host: &str,
headers: &[(String, String)],
body: &[u8],
) -> Option<Vec<u8>> {
if !is_valid_token(method)
|| path_with_query.is_empty()
|| contains_crlf(path_with_query)
|| path_with_query.contains(' ')
|| host.is_empty()
|| contains_crlf(host)
{
return None;
}
for (name, value) in headers {
if !is_valid_token(name) || contains_crlf(value) {
return None;
}
}
let mut out = Vec::with_capacity(method.len() + path_with_query.len() + host.len() + body.len() + 256);
out.extend_from_slice(method.as_bytes());
out.push(b' ');
out.extend_from_slice(path_with_query.as_bytes());
out.extend_from_slice(b" HTTP/1.1\r\nHost: ");
out.extend_from_slice(host.as_bytes());
out.extend_from_slice(b"\r\n");
for (name, value) in headers {
out.extend_from_slice(name.as_bytes());
out.extend_from_slice(b": ");
out.extend_from_slice(value.as_bytes());
out.extend_from_slice(b"\r\n");
}
out.extend_from_slice(b"Content-Length: ");
out.extend_from_slice(body.len().to_string().as_bytes());
out.extend_from_slice(b"\r\nConnection: close\r\n\r\n");
out.extend_from_slice(body);
Some(out)
}
#[inline]
fn find_header_end(buf: &[u8]) -> Option<usize> {
buf.windows(4).position(|w| w == b"\r\n\r\n")
}
fn parse_status_line(line: &[u8]) -> Result<u16, ClientError> {
let s = std::str::from_utf8(line).map_err(|_| ClientError::InvalidStatusLine)?;
let mut parts = s.splitn(3, ' ');
let version = parts.next().ok_or(ClientError::InvalidStatusLine)?;
let ver_ok = version.len() == 8
&& version.starts_with("HTTP/1.")
&& version.as_bytes()[7].is_ascii_digit();
if !ver_ok {
return Err(ClientError::InvalidStatusLine);
}
let status_str = parts.next().ok_or(ClientError::InvalidStatusLine)?;
if status_str.len() != 3 || !status_str.bytes().all(|b| b.is_ascii_digit()) {
return Err(ClientError::InvalidStatusLine);
}
status_str
.parse::<u16>()
.map_err(|_| ClientError::InvalidStatusLine)
}
fn parse_head(head: &[u8]) -> Result<(u16, Vec<(String, String)>), ClientError> {
let lines: Vec<&[u8]> = head.split(|&b| b == b'\n').collect();
let total = lines.len();
if total == 0 {
return Err(ClientError::InvalidStatusLine);
}
let status_line = lines[0];
let status_line = if total > 1 {
match status_line.last() {
Some(b'\r') => &status_line[..status_line.len() - 1],
_ => {
return Err(ClientError::InvalidHeader(
"bare LF in status line (protocol violation)".into(),
))
}
}
} else {
match status_line.last() {
Some(b'\r') => &status_line[..status_line.len() - 1],
_ => status_line,
}
};
let status = parse_status_line(status_line)?;
let mut headers: Vec<(String, String)> = Vec::new();
for (i, raw) in lines.iter().enumerate().skip(1) {
let line = if i < total - 1 {
match raw.last() {
Some(b'\r') => &raw[..raw.len() - 1],
_ => {
return Err(ClientError::InvalidHeader(
"bare LF in header line (protocol violation)".into(),
))
}
}
} else {
match raw.last() {
Some(b'\r') => &raw[..raw.len() - 1],
_ => raw,
}
};
if line.is_empty() {
continue;
}
let colon = line
.iter()
.position(|&b| b == b':')
.ok_or_else(|| ClientError::InvalidHeader("missing colon".into()))?;
let name = std::str::from_utf8(&line[..colon])
.map_err(|_| ClientError::InvalidHeader("name not UTF-8".into()))?;
if !is_valid_token(name) {
return Err(ClientError::InvalidHeader(format!(
"invalid header name: {name:?}"
)));
}
if name.len() > MAX_HEADER_NAME_LEN {
return Err(ClientError::HeaderTooLarge);
}
let value_raw = &line[colon + 1..];
let mut start = 0;
let mut end = value_raw.len();
while start < end && matches!(value_raw[start], b' ' | b'\t') {
start += 1;
}
while end > start && matches!(value_raw[end - 1], b' ' | b'\t') {
end -= 1;
}
if end - start > MAX_HEADER_VALUE_LEN {
return Err(ClientError::HeaderTooLarge);
}
let value = std::str::from_utf8(&value_raw[start..end])
.map_err(|_| ClientError::InvalidHeader("value not UTF-8".into()))?;
headers.push((name.to_string(), value.to_string()));
if headers.len() > MAX_HEADER_COUNT {
return Err(ClientError::HeaderTooLarge);
}
}
Ok((status, headers))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BodyFraming {
Bodiless,
Chunked,
Length(usize),
UntilEof,
}
fn determine_framing(
status: u16,
headers: &[(String, String)],
) -> Result<BodyFraming, ClientError> {
if matches!(status, 100..=199) || status == 204 || status == 304 {
return Ok(BodyFraming::Bodiless);
}
let mut content_lengths: Vec<usize> = Vec::new();
let mut chunked = false;
for (name, value) in headers {
if name.eq_ignore_ascii_case("content-length") {
let n: u64 = value
.trim()
.parse()
.map_err(|_| ClientError::InvalidHeader("content-length not a number".into()))?;
let n = usize::try_from(n)
.map_err(|_| ClientError::BodyTooLarge)?;
content_lengths.push(n);
} else if name.eq_ignore_ascii_case("transfer-encoding") {
for token in value.split(',') {
if token.trim().eq_ignore_ascii_case("chunked") {
chunked = true;
}
}
}
}
if chunked && !content_lengths.is_empty() {
return Err(ClientError::ProtocolInconsistency(
"content-length with transfer-encoding".into(),
));
}
if let Some(first) = content_lengths.first()
&& content_lengths.iter().any(|n| n != first)
{
return Err(ClientError::ProtocolInconsistency(
"conflicting content-length headers".into(),
));
}
if chunked {
Ok(BodyFraming::Chunked)
} else if let Some(&len) = content_lengths.first() {
Ok(BodyFraming::Length(len))
} else {
Ok(BodyFraming::UntilEof)
}
}
pub fn parse_response(buf: &[u8]) -> Result<Option<(ClientResponse, usize)>, ClientError> {
let head_end = match find_header_end(buf) {
Some(pos) => pos,
None => {
if buf.len() > MAX_HEADER_SECTION {
return Err(ClientError::HeaderTooLarge);
}
return Ok(None);
}
};
if head_end.saturating_add(4) > MAX_HEADER_SECTION {
return Err(ClientError::HeaderTooLarge);
}
let (status, headers) = parse_head(&buf[..head_end])?;
let body_start = head_end.saturating_add(4);
let body_bytes = &buf[body_start..];
match determine_framing(status, &headers)? {
BodyFraming::Bodiless => Ok(Some((
ClientResponse {
status,
headers,
body: Vec::new(),
},
body_start,
))),
BodyFraming::Length(len) => {
if len as u64 > MAX_BODY_BYTES {
return Err(ClientError::BodyTooLarge);
}
let need = body_start.checked_add(len).ok_or(ClientError::BodyTooLarge)?;
if buf.len() < need {
return Ok(None);
}
Ok(Some((
ClientResponse {
status,
headers,
body: buf[body_start..need].to_vec(),
},
need,
)))
}
BodyFraming::Chunked => {
let mut dec = ChunkedDecoder::new(MAX_BODY_BYTES);
let (out, consumed) = dec.feed(body_bytes).map_err(|e| match e {
Http1Error::BodyTooLarge => ClientError::BodyTooLarge,
other => ClientError::InvalidChunked(other.to_string()),
})?;
if dec.is_done() {
Ok(Some((
ClientResponse {
status,
headers,
body: out,
},
body_start.saturating_add(consumed),
)))
} else {
Ok(None)
}
}
BodyFraming::UntilEof => Ok(None),
}
}
#[derive(Debug)]
pub struct ResponseParser {
head: Option<ParsedHead>,
chunked_decoder: Option<ChunkedDecoder>,
body_processed: usize,
chunked_output: Vec<u8>,
}
#[derive(Debug, Clone)]
struct ParsedHead {
status: u16,
headers: Vec<(String, String)>,
body_start: usize,
framing: BodyFraming,
}
impl ResponseParser {
#[inline]
pub fn new() -> Self {
Self {
head: None,
chunked_decoder: None,
body_processed: 0,
chunked_output: Vec::new(),
}
}
pub fn feed(&mut self, buf: &[u8]) -> Result<Option<(ClientResponse, usize)>, ClientError> {
if self.head.is_none() {
let head_end = match find_header_end(buf) {
Some(pos) => pos,
None => {
if buf.len() > MAX_HEADER_SECTION {
return Err(ClientError::HeaderTooLarge);
}
return Ok(None);
}
};
if head_end.saturating_add(4) > MAX_HEADER_SECTION {
return Err(ClientError::HeaderTooLarge);
}
let (status, headers) = parse_head(&buf[..head_end])?;
let body_start = head_end.saturating_add(4);
let framing = determine_framing(status, &headers)?;
self.head = Some(ParsedHead {
status,
headers,
body_start,
framing,
});
}
let head = self.head.as_ref().expect("head just parsed");
let body_bytes = &buf[head.body_start..];
match head.framing {
BodyFraming::Bodiless => Ok(Some((
ClientResponse {
status: head.status,
headers: head.headers.clone(),
body: Vec::new(),
},
head.body_start,
))),
BodyFraming::Length(len) => {
if len as u64 > MAX_BODY_BYTES {
return Err(ClientError::BodyTooLarge);
}
let need = head
.body_start
.checked_add(len)
.ok_or(ClientError::BodyTooLarge)?;
if buf.len() < need {
return Ok(None);
}
Ok(Some((
ClientResponse {
status: head.status,
headers: head.headers.clone(),
body: buf[head.body_start..need].to_vec(),
},
need,
)))
}
BodyFraming::Chunked => {
if self.chunked_decoder.is_none() {
self.chunked_decoder = Some(ChunkedDecoder::new(MAX_BODY_BYTES));
}
let decoder = self.chunked_decoder.as_mut().expect("just initialized");
let new_data = &body_bytes[self.body_processed..];
if !new_data.is_empty() {
let (out, consumed) = decoder
.feed(new_data)
.map_err(|e| match e {
Http1Error::BodyTooLarge => ClientError::BodyTooLarge,
other => ClientError::InvalidChunked(other.to_string()),
})?;
self.body_processed += consumed;
self.chunked_output.extend_from_slice(&out);
if decoder.is_done() {
let total_consumed = head.body_start + self.body_processed;
let body = std::mem::take(&mut self.chunked_output);
return Ok(Some((
ClientResponse {
status: head.status,
headers: head.headers.clone(),
body,
},
total_consumed,
)));
}
}
Ok(None)
}
BodyFraming::UntilEof => Ok(None),
}
}
}
impl Default for ResponseParser {
#[inline]
fn default() -> Self {
Self::new()
}
}
pub fn parse_response_eof(buf: &[u8]) -> Result<ClientResponse, ClientError> {
let head_end = find_header_end(buf).ok_or(ClientError::Truncated)?;
let (status, headers) = parse_head(&buf[..head_end])?;
let body_start = head_end.saturating_add(4);
match determine_framing(status, &headers)? {
BodyFraming::UntilEof => Ok(ClientResponse {
status,
headers,
body: buf[body_start..].to_vec(),
}),
_ => match parse_response(buf)? {
Some((resp, _)) => Ok(resp),
None => Err(ClientError::Truncated),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encode_basic_get() {
let out = encode_request("GET", "/api/x?a=1", "127.0.0.1:8080", &[], b"");
let out = match out {
Some(v) => v,
None => panic!("encode should succeed"),
};
let s = String::from_utf8_lossy(&out);
assert!(s.starts_with("GET /api/x?a=1 HTTP/1.1\r\n"));
assert!(s.contains("Host: 127.0.0.1:8080\r\n"));
assert!(s.contains("Content-Length: 0\r\n"));
assert!(s.contains("Connection: close\r\n"));
assert!(s.ends_with("\r\n\r\n"));
}
#[test]
fn encode_post_with_body_and_headers() {
let headers = vec![
("content-type".to_string(), "application/json".to_string()),
("x-token".to_string(), "abc".to_string()),
];
let out = encode_request("POST", "/echo", "up:9000", &headers, b"hello");
let out = match out {
Some(v) => v,
None => panic!("encode should succeed"),
};
let s = String::from_utf8_lossy(&out);
assert!(s.starts_with("POST /echo HTTP/1.1\r\n"));
assert!(s.contains("content-type: application/json\r\n"));
assert!(s.contains("x-token: abc\r\n"));
assert!(s.contains("Content-Length: 5\r\n"));
assert!(s.ends_with("\r\n\r\nhello"));
}
#[test]
fn encode_rejects_crlf_injection_fail_closed() {
assert!(encode_request("GET\r\nEvil: x", "/", "h", &[], b"").is_none());
assert!(encode_request("GET", "/a\r\nb", "h", &[], b"").is_none());
assert!(encode_request("GET", "/", "ho\r\nst", &[], b"").is_none());
assert!(encode_request("GET", "/a\nb", "h", &[], b"").is_none());
let bad_value = vec![("x-bad".to_string(), "evil\r\nInjected: yes".to_string())];
assert!(encode_request("GET", "/", "h", &bad_value, b"").is_none());
let bad_name = vec![("x-bad\r\nInjected".to_string(), "v".to_string())];
assert!(encode_request("GET", "/", "h", &bad_name, b"").is_none());
assert!(encode_request("GE T", "/", "h", &[], b"").is_none());
assert!(encode_request("", "/", "h", &[], b"").is_none());
assert!(encode_request("GET", "", "h", &[], b"").is_none());
assert!(encode_request("GET", "/", "", &[], b"").is_none());
let empty_name = vec![("".to_string(), "v".to_string())];
assert!(encode_request("GET", "/", "h", &empty_name, b"").is_none());
}
#[test]
fn parse_content_length_response() {
let raw = b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 5\r\n\r\nhello";
let r = parse_response(raw);
let (resp, consumed) = match r {
Ok(Some(v)) => v,
other => panic!("expected complete response, got {other:?}"),
};
assert_eq!(resp.status, 200);
assert_eq!(resp.body, b"hello");
assert_eq!(consumed, raw.len());
assert_eq!(
resp.headers,
vec![
("Content-Type".to_string(), "text/plain".to_string()),
("Content-Length".to_string(), "5".to_string()),
]
);
}
#[test]
fn parse_content_length_incremental_feed() {
let raw = b"HTTP/1.1 200 OK\r\nContent-Length: 11\r\n\r\nhello world";
for i in 0..raw.len() {
match parse_response(&raw[..i]) {
Ok(None) => {}
other => panic!("prefix {i} should be incomplete, got {other:?}"),
}
}
match parse_response(raw) {
Ok(Some((resp, consumed))) => {
assert_eq!(resp.body, b"hello world");
assert_eq!(consumed, raw.len());
}
other => panic!("full input should complete, got {other:?}"),
}
}
#[test]
fn parse_content_length_sticky_packet_consumed() {
let raw = b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nhiNEXT-REQUEST-BYTES";
match parse_response(raw) {
Ok(Some((resp, consumed))) => {
assert_eq!(resp.body, b"hi");
assert_eq!(&raw[consumed..], b"NEXT-REQUEST-BYTES");
}
other => panic!("expected complete, got {other:?}"),
}
}
#[test]
fn parse_status_without_reason_phrase() {
let raw = b"HTTP/1.1 200\r\nContent-Length: 0\r\n\r\n";
match parse_response(raw) {
Ok(Some((resp, _))) => assert_eq!(resp.status, 200),
other => panic!("expected complete, got {other:?}"),
}
}
#[test]
fn parse_http10_response() {
let raw = b"HTTP/1.0 302 Found\r\nContent-Length: 2\r\n\r\nok";
match parse_response(raw) {
Ok(Some((resp, _))) => {
assert_eq!(resp.status, 302);
assert_eq!(resp.body, b"ok");
}
other => panic!("expected complete, got {other:?}"),
}
}
#[test]
fn parse_bodiless_status_complete_without_body() {
let raw = b"HTTP/1.1 204 No Content\r\nX-A: b\r\n\r\n";
match parse_response(raw) {
Ok(Some((resp, consumed))) => {
assert_eq!(resp.status, 204);
assert!(resp.body.is_empty());
assert_eq!(consumed, raw.len());
}
other => panic!("204 should complete immediately, got {other:?}"),
}
let raw304 = b"HTTP/1.1 304 Not Modified\r\n\r\n";
match parse_response(raw304) {
Ok(Some((resp, _))) => assert_eq!(resp.status, 304),
other => panic!("304 should complete immediately, got {other:?}"),
}
}
#[test]
fn parse_duplicate_identical_content_length_ok() {
let raw = b"HTTP/1.1 200 OK\r\nContent-Length: 3\r\nContent-Length: 3\r\n\r\nabc";
match parse_response(raw) {
Ok(Some((resp, _))) => assert_eq!(resp.body, b"abc"),
other => panic!("expected complete, got {other:?}"),
}
}
#[test]
fn parse_chunked_response() {
let raw = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n6\r\n world\r\n0\r\n\r\n";
match parse_response(raw) {
Ok(Some((resp, consumed))) => {
assert_eq!(resp.status, 200);
assert_eq!(resp.body, b"hello world");
assert_eq!(consumed, raw.len());
}
other => panic!("expected complete, got {other:?}"),
}
}
#[test]
fn parse_chunked_with_trailer_ignored() {
let raw = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n2\r\nhi\r\n0\r\nX-Trailer: v\r\n\r\n";
match parse_response(raw) {
Ok(Some((resp, _))) => assert_eq!(resp.body, b"hi"),
other => panic!("expected complete, got {other:?}"),
}
}
#[test]
fn parse_chunked_incremental_across_chunks() {
let raw = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n6\r\n world\r\n0\r\n\r\n";
let mut saw_complete_before_full = false;
for i in 0..raw.len() {
if let Ok(Some(_)) = parse_response(&raw[..i]) {
saw_complete_before_full = true;
}
}
assert!(!saw_complete_before_full, "chunked must not complete before terminator");
match parse_response(raw) {
Ok(Some((resp, _))) => assert_eq!(resp.body, b"hello world"),
other => panic!("expected complete, got {other:?}"),
}
}
#[test]
fn parse_chunked_uppercase_hex_and_extension() {
let raw = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\nA;ext=1\r\n0123456789\r\n0\r\n\r\n";
match parse_response(raw) {
Ok(Some((resp, _))) => assert_eq!(resp.body, b"0123456789"),
other => panic!("expected complete, got {other:?}"),
}
}
#[test]
fn parse_until_eof_waits_then_eof_completes() {
let raw = b"HTTP/1.1 200 OK\r\nX-A: b\r\n\r\nstreamed-body-until-close";
match parse_response(raw) {
Ok(None) => {}
other => panic!("until-eof framing must wait for EOF, got {other:?}"),
}
let resp = match parse_response_eof(raw) {
Ok(r) => r,
Err(e) => panic!("eof parse should succeed: {e}"),
};
assert_eq!(resp.status, 200);
assert_eq!(resp.body, b"streamed-body-until-close");
}
#[test]
fn parse_response_eof_rejects_truncated() {
assert!(matches!(
parse_response_eof(b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n"),
Err(ClientError::Truncated)
));
assert!(matches!(
parse_response_eof(b"HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\nshort"),
Err(ClientError::Truncated)
));
assert!(matches!(
parse_response_eof(b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhel"),
Err(ClientError::Truncated)
));
assert!(matches!(parse_response_eof(b""), Err(ClientError::Truncated)));
}
#[test]
fn parse_rejects_bad_status_line() {
for raw in [
&b"NOTHTTP 200 OK\r\n\r\n"[..],
b"HTTP/2 200 OK\r\n\r\n",
b"HTTP/1.1 20 OK\r\n\r\n",
b"HTTP/1.1 abc OK\r\n\r\n",
b"HTTP/1.1 \r\n\r\n",
] {
match parse_response(raw) {
Err(ClientError::InvalidStatusLine) => {}
other => panic!(
"should reject {:?}, got {other:?}",
String::from_utf8_lossy(raw)
),
}
}
}
#[test]
fn parse_rejects_header_without_colon() {
let raw = b"HTTP/1.1 200 OK\r\nBadHeaderLine\r\n\r\n";
assert!(matches!(
parse_response(raw),
Err(ClientError::InvalidHeader(_))
));
}
#[test]
fn parse_rejects_invalid_header_name() {
let raw = b"HTTP/1.1 200 OK\r\nBad Name: v\r\n\r\n";
assert!(matches!(
parse_response(raw),
Err(ClientError::InvalidHeader(_))
));
}
#[test]
fn parse_rejects_bare_lf_in_status_line() {
let raw = b"HTTP/1.1 200 OK\nInjected: evil\r\nContent-Length: 0\r\n\r\n";
assert!(matches!(
parse_response(raw),
Err(ClientError::InvalidHeader(_))
));
}
#[test]
fn parse_rejects_bare_lf_in_header_line() {
let raw = b"HTTP/1.1 200 OK\r\nX-A: b\nInjected: evil\r\nContent-Length: 0\r\n\r\n";
assert!(matches!(
parse_response(raw),
Err(ClientError::InvalidHeader(_))
));
}
#[test]
fn parse_rejects_cl_and_te_coexist() {
let raw = b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\n";
assert!(matches!(
parse_response(raw),
Err(ClientError::ProtocolInconsistency(_))
));
}
#[test]
fn parse_rejects_conflicting_content_length() {
let raw = b"HTTP/1.1 200 OK\r\nContent-Length: 3\r\nContent-Length: 4\r\n\r\nabcd";
assert!(matches!(
parse_response(raw),
Err(ClientError::ProtocolInconsistency(_))
));
}
#[test]
fn parse_rejects_invalid_chunk_size() {
let raw = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\nZZ\r\nx\r\n0\r\n\r\n";
assert!(matches!(
parse_response(raw),
Err(ClientError::InvalidChunked(_))
));
}
#[test]
fn parse_rejects_oversized_header_section() {
let mut raw = Vec::new();
raw.extend_from_slice(b"HTTP/1.1 200 OK\r\n");
while raw.len() <= MAX_HEADER_SECTION {
raw.extend_from_slice(b"X-Pad: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n");
}
assert!(matches!(
parse_response(&raw),
Err(ClientError::HeaderTooLarge)
));
}
#[test]
fn parse_rejects_oversized_header_section_with_terminator() {
let mut raw = Vec::new();
raw.extend_from_slice(b"HTTP/1.1 200 OK\r\n");
while raw.len() <= MAX_HEADER_SECTION {
raw.extend_from_slice(b"X-Pad: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n");
}
raw.extend_from_slice(b"\r\n"); raw.extend_from_slice(b"ok");
assert!(matches!(
parse_response(&raw),
Err(ClientError::HeaderTooLarge)
));
assert!(matches!(
parse_response_eof(&raw),
Err(ClientError::HeaderTooLarge)
));
}
#[test]
fn parse_rejects_too_many_headers() {
let mut raw = Vec::new();
raw.extend_from_slice(b"HTTP/1.1 200 OK\r\n");
for i in 0..=MAX_HEADER_COUNT {
raw.extend_from_slice(format!("X-H{i}: v\r\n").as_bytes());
}
raw.extend_from_slice(b"Content-Length: 0\r\n\r\n");
assert!(matches!(
parse_response(&raw),
Err(ClientError::HeaderTooLarge)
));
}
#[test]
fn parse_accepts_header_count_at_limit() {
let mut raw = Vec::new();
raw.extend_from_slice(b"HTTP/1.1 200 OK\r\n");
for i in 1..MAX_HEADER_COUNT {
raw.extend_from_slice(format!("X-H{i}: v\r\n").as_bytes());
}
raw.extend_from_slice(b"Content-Length: 0\r\n\r\n");
assert!(
matches!(parse_response(&raw), Ok(Some(_))),
"恰在上限内的条数必须放行"
);
}
#[test]
fn parse_rejects_oversized_header_name() {
let name = format!("X-{}", "A".repeat(MAX_HEADER_NAME_LEN));
let raw = format!("HTTP/1.1 200 OK\r\n{name}: v\r\nContent-Length: 0\r\n\r\n");
assert!(matches!(
parse_response(raw.as_bytes()),
Err(ClientError::HeaderTooLarge)
));
}
#[test]
fn parse_rejects_oversized_header_value() {
let value = "v".repeat(MAX_HEADER_VALUE_LEN + 1);
let raw = format!("HTTP/1.1 200 OK\r\nX-Pad: {value}\r\nContent-Length: 0\r\n\r\n");
assert!(matches!(
parse_response(raw.as_bytes()),
Err(ClientError::HeaderTooLarge)
));
}
#[test]
fn parse_header_ows_trimmed() {
let raw = b"HTTP/1.1 200 OK\r\nX-Pad: value \r\nContent-Length: 1\r\n\r\nx";
match parse_response(raw) {
Ok(Some((resp, _))) => {
assert_eq!(resp.headers[0], ("X-Pad".to_string(), "value".to_string()))
}
other => panic!("expected complete, got {other:?}"),
}
}
#[test]
fn error_display_impl() {
let e = ClientError::InvalidStatusLine;
assert!(!format!("{e}").is_empty());
let e = ClientError::Truncated;
assert!(format!("{e}").contains("truncated"));
let _: &dyn std::error::Error = &ClientError::HeaderTooLarge;
}
}