use std::fmt;
use http::{HeaderName, HeaderValue, Response, StatusCode, Version, header};
use crate::Error;
use crate::body::{BodyWriter, parse_content_length_value};
use crate::ext::HeaderIterExt;
pub(crate) struct AmendedResponse {
response: Response<()>,
headers: Vec<(HeaderName, HeaderValue)>,
}
impl AmendedResponse {
pub fn new(response: Response<()>) -> Self {
AmendedResponse {
response,
headers: vec![],
}
}
pub fn prelude(&self) -> (Version, StatusCode) {
let r = &self.response;
(r.version(), r.status())
}
pub fn set_header<K, V>(&mut self, name: K, value: V) -> Result<(), Error>
where
HeaderName: TryFrom<K>,
<HeaderName as TryFrom<K>>::Error: Into<http::Error>,
HeaderValue: TryFrom<V>,
<HeaderValue as TryFrom<V>>::Error: Into<http::Error>,
{
let name = <HeaderName as TryFrom<K>>::try_from(name)
.map_err(Into::into)
.map_err(|e| Error::BadHeader(e.to_string()))?;
let value = <HeaderValue as TryFrom<V>>::try_from(value)
.map_err(Into::into)
.map_err(|e| Error::BadHeader(e.to_string()))?;
self.headers.push((name, value));
Ok(())
}
pub fn headers(&self) -> impl Iterator<Item = (&HeaderName, &HeaderValue)> {
self.headers
.iter()
.map(|v| (&v.0, &v.1))
.chain(self.response.headers().iter())
}
fn headers_get_all(&self, key: HeaderName) -> impl Iterator<Item = &HeaderValue> {
self.headers()
.filter(move |(k, _)| *k == key)
.map(|(_, v)| v)
}
fn headers_get(&self, key: HeaderName) -> Option<&HeaderValue> {
self.headers_get_all(key).next()
}
pub fn headers_len(&self) -> usize {
self.headers().count()
}
pub fn analyze(&self, wanted_mode: BodyWriter) -> Result<ResponseInfo, Error> {
let count_len = self.headers_get_all(header::CONTENT_LENGTH).count();
if count_len > 1 {
return Err(Error::TooManyContentLengthHeaders);
}
let content_length = self
.headers_get(header::CONTENT_LENGTH)
.map(|h| parse_content_length_value(h.as_bytes()))
.transpose()?;
let has_chunked = self.headers().has(header::TRANSFER_ENCODING, "chunked");
if has_chunked && content_length.is_some() {
return Err(Error::ConflictingBodyHeaders);
}
let mut res_body_header = false;
let body_mode = if has_chunked {
res_body_header = true;
BodyWriter::new_chunked()
} else if let Some(n) = content_length {
res_body_header = true;
BodyWriter::new_sized(n)
} else {
wanted_mode
};
Ok(ResponseInfo {
body_mode,
res_body_header,
})
}
}
pub(crate) struct ResponseInfo {
pub body_mode: BodyWriter,
pub res_body_header: bool,
}
impl fmt::Debug for AmendedResponse {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AmendedResponse")
.field("status", &self.response.status())
.field("headers", &self.headers)
.finish()
}
}
#[cfg(test)]
mod transfer_encoding_tests {
use super::*;
#[test]
fn response_writer_detects_chunked_in_lists_and_repeated_headers() {
for values in [
vec!["gzip, chunked"],
vec!["chunked, gzip"],
vec!["gzip", "ChUnKeD"],
] {
let mut response = Response::builder().status(200);
for value in values {
response = response.header(header::TRANSFER_ENCODING, value);
}
let response = AmendedResponse::new(response.body(()).unwrap());
assert!(
response
.analyze(BodyWriter::new_none())
.unwrap()
.body_mode
.is_chunked()
);
}
}
#[test]
fn response_rejects_content_length_with_listed_chunked() {
for value in ["gzip, chunked", "chunked, gzip"] {
let response = Response::builder()
.header(header::TRANSFER_ENCODING, value)
.header(header::CONTENT_LENGTH, "5")
.body(())
.unwrap();
assert!(matches!(
AmendedResponse::new(response).analyze(BodyWriter::new_none()),
Err(Error::ConflictingBodyHeaders)
));
}
}
}