Skip to content

improve detection of Client Response bodies #625

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 6, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/client/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ impl Response {
version: version,
headers: headers,
url: url,
message: message,
status_raw: raw_status,
is_drained: false,
is_drained: !message.has_body(),
message: message,
})
}

Expand Down
115 changes: 82 additions & 33 deletions src/header/common/content_length.rs
Original file line number Diff line number Diff line change
@@ -1,37 +1,86 @@
header! {
#[doc="`Content-Length` header, defined in"]
#[doc="[RFC7230](http://tools.ietf.org/html/rfc7230#section-3.3.2)"]
#[doc=""]
#[doc="When a message does not have a `Transfer-Encoding` header field, a"]
#[doc="Content-Length header field can provide the anticipated size, as a"]
#[doc="decimal number of octets, for a potential payload body. For messages"]
#[doc="that do include a payload body, the Content-Length field-value"]
#[doc="provides the framing information necessary for determining where the"]
#[doc="body (and message) ends. For messages that do not include a payload"]
#[doc="body, the Content-Length indicates the size of the selected"]
#[doc="representation."]
#[doc=""]
#[doc="# ABNF"]
#[doc="```plain"]
#[doc="Content-Length = 1*DIGIT"]
#[doc="```"]
#[doc=""]
#[doc="# Example values"]
#[doc="* `3495`"]
#[doc=""]
#[doc="# Example"]
#[doc="```"]
#[doc="use hyper::header::{Headers, ContentLength};"]
#[doc=""]
#[doc="let mut headers = Headers::new();"]
#[doc="headers.set(ContentLength(1024u64));"]
#[doc="```"]
(ContentLength, "Content-Length") => [u64]

test_content_length {
// Testcase from RFC
test_header!(test1, vec![b"3495"], Some(HeaderField(3495)));
use std::fmt;

use header::{HeaderFormat, Header, parsing};

#[doc="`Content-Length` header, defined in"]
#[doc="[RFC7230](http://tools.ietf.org/html/rfc7230#section-3.3.2)"]
#[doc=""]
#[doc="When a message does not have a `Transfer-Encoding` header field, a"]
#[doc="Content-Length header field can provide the anticipated size, as a"]
#[doc="decimal number of octets, for a potential payload body. For messages"]
#[doc="that do include a payload body, the Content-Length field-value"]
#[doc="provides the framing information necessary for determining where the"]
#[doc="body (and message) ends. For messages that do not include a payload"]
#[doc="body, the Content-Length indicates the size of the selected"]
#[doc="representation."]
#[doc=""]
#[doc="# ABNF"]
#[doc="```plain"]
#[doc="Content-Length = 1*DIGIT"]
#[doc="```"]
#[doc=""]
#[doc="# Example values"]
#[doc="* `3495`"]
#[doc=""]
#[doc="# Example"]
#[doc="```"]
#[doc="use hyper::header::{Headers, ContentLength};"]
#[doc=""]
#[doc="let mut headers = Headers::new();"]
#[doc="headers.set(ContentLength(1024u64));"]
#[doc="```"]
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ContentLength(pub u64);

impl Header for ContentLength {
#[inline]
fn header_name() -> &'static str {
"Content-Length"
}
fn parse_header(raw: &[Vec<u8>]) -> ::Result<ContentLength> {
// If multiple Content-Length headers were sent, everything can still
// be alright if they all contain the same value, and all parse
// correctly. If not, then it's an error.
raw.iter()
.map(::std::ops::Deref::deref)
.map(parsing::from_raw_str)
.fold(None, |prev, x| {
match (prev, x) {
(None, x) => Some(x),
(e@Some(Err(_)), _ ) => e,
(Some(Ok(prev)), Ok(x)) if prev == x => Some(Ok(prev)),
_ => Some(Err(::Error::Header))
}
})
.unwrap_or(Err(::Error::Header))
.map(ContentLength)
}
}

impl HeaderFormat for ContentLength {
#[inline]
fn fmt_header(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}

impl fmt::Display for ContentLength {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}

__hyper__deref!(ContentLength => u64);
__hyper_generate_header_serialization!(ContentLength);

__hyper__tm!(ContentLength, tests {
// Testcase from RFC
test_header!(test1, vec![b"3495"], Some(HeaderField(3495)));

test_header!(test_invalid, vec![b"34v95"], None);
test_header!(test_duplicates, vec![b"5", b"5"], Some(HeaderField(5)));
test_header!(test_duplicates_vary, vec![b"5", b"6", b"5"], None);
});

bench_header!(bench, ContentLength, { vec![b"42349984".to_vec()] });
7 changes: 4 additions & 3 deletions src/header/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,12 +148,13 @@ macro_rules! test_header {
fn $id() {
let a: Vec<Vec<u8>> = $raw.iter().map(|x| x.to_vec()).collect();
let val = HeaderField::parse_header(&a[..]);
let typed: Option<HeaderField> = $typed;
// Test parsing
assert_eq!(val.ok(), $typed);
assert_eq!(val.ok(), typed);
// Test formatting
if $typed != None {
if typed.is_some() {
let res: &str = str::from_utf8($raw[0]).unwrap();
assert_eq!(format!("{}", $typed.unwrap()), res);
assert_eq!(format!("{}", typed.unwrap()), res);
}
}
}
Expand Down
15 changes: 8 additions & 7 deletions src/header/parsing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@
use std::str;
use std::fmt::{self, Display};

/// Reads a single raw string when parsing a header
/// Reads a single raw string when parsing a header.
pub fn from_one_raw_str<T: str::FromStr>(raw: &[Vec<u8>]) -> ::Result<T> {
if raw.len() != 1 || unsafe { raw.get_unchecked(0) } == b"" { return Err(::Error::Header) }
// we JUST checked that raw.len() == 1, so raw[0] WILL exist.
let s: &str = try!(str::from_utf8(& unsafe { raw.get_unchecked(0) }[..]));
if let Ok(x) = str::FromStr::from_str(s) {
Ok(x)
} else {
Err(::Error::Header)
}
from_raw_str(& unsafe { raw.get_unchecked(0) })
}

/// Reads a raw string into a value.
pub fn from_raw_str<T: str::FromStr>(raw: &[u8]) -> ::Result<T> {
let s = try!(str::from_utf8(raw));
T::from_str(s).or(Err(::Error::Header))
}

/// Reads a comma-delimited raw header into a Vec.
Expand Down
55 changes: 36 additions & 19 deletions src/http/h1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ use version;
/// An implementation of the `HttpMessage` trait for HTTP/1.1.
#[derive(Debug)]
pub struct Http11Message {
method: Option<Method>,
stream: Option<Box<NetworkStream + Send>>,
writer: Option<HttpWriter<BufWriter<Box<NetworkStream + Send>>>>,
reader: Option<HttpReader<BufReader<Box<NetworkStream + Send>>>>,
Expand Down Expand Up @@ -91,8 +92,8 @@ impl HttpMessage for Http11Message {
try!(write!(&mut stream, "{} {} {}{}",
head.method, uri, version, LINE_ENDING));

let stream = match head.method {
Method::Get | Method::Head => {
let stream = match &head.method {
&Method::Get | &Method::Head => {
debug!("headers={:?}", head.headers);
try!(write!(&mut stream, "{}{}", head.headers, LINE_ENDING));
EmptyWriter(stream)
Expand Down Expand Up @@ -137,6 +138,7 @@ impl HttpMessage for Http11Message {
}
};

self.method = Some(head.method.clone());
self.writer = Some(stream);

Ok(head)
Expand All @@ -159,30 +161,37 @@ impl HttpMessage for Http11Message {
let raw_status = head.subject;
let headers = head.headers;

let body = if headers.has::<TransferEncoding>() {
match headers.get::<TransferEncoding>() {
Some(&TransferEncoding(ref codings)) => {
if codings.len() > 1 {
trace!("TODO: #2 handle other codings: {:?}", codings);
};

if codings.contains(&Chunked) {
let method = self.method.take().unwrap_or(Method::Get);
// According to https://tools.ietf.org/html/rfc7230#section-3.3.3
// 1. HEAD reponses, and Status 1xx, 204, and 304 cannot have a body.
// 2. Status 2xx to a CONNECT cannot have a body.
// 3. Transfer-Encoding: chunked has a chunked body.
// 4. If multiple differing Content-Length headers or invalid, close connection.
// 5. Content-Length header has a sized body.
// 6. Not Client.
// 7. Read till EOF.
let body = match (method, raw_status.0) {
(Method::Head, _) => EmptyReader(stream),
(_, 100...199) | (_, 204) | (_, 304) => EmptyReader(stream),
(Method::Connect, 200...299) => EmptyReader(stream),
_ => {
if let Some(&TransferEncoding(ref codings)) = headers.get() {
if codings.last() == Some(&Chunked) {
ChunkedReader(stream, None)
} else {
trace!("not chuncked. read till eof");
EofReader(stream)
}
} else if let Some(&ContentLength(len)) = headers.get() {
SizedReader(stream, len)
} else if headers.has::<ContentLength>() {
trace!("illegal Content-Length: {:?}", headers.get_raw("Content-Length"));
return Err(Error::Header);
} else {
trace!("neither Transfer-Encoding nor Content-Length");
EofReader(stream)
}
None => unreachable!()
}
} else if headers.has::<ContentLength>() {
match headers.get::<ContentLength>() {
Some(&ContentLength(len)) => SizedReader(stream, len),
None => unreachable!()
}
} else {
trace!("neither Transfer-Encoding nor Content-Length");
EofReader(stream)
};

self.reader = Some(body);
Expand All @@ -194,6 +203,13 @@ impl HttpMessage for Http11Message {
})
}

fn has_body(&self) -> bool {
match self.reader {
Some(EmptyReader(..)) => false,
_ => true
}
}

#[cfg(feature = "timeouts")]
#[inline]
fn set_read_timeout(&self, dur: Option<Duration>) -> io::Result<()> {
Expand Down Expand Up @@ -259,6 +275,7 @@ impl Http11Message {
/// the peer.
pub fn with_stream(stream: Box<NetworkStream + Send>) -> Http11Message {
Http11Message {
method: None,
stream: Some(stream),
writer: None,
reader: None,
Expand Down
4 changes: 4 additions & 0 deletions src/http/h2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,10 @@ impl<S> HttpMessage for Http2Message<S> where S: CloneableStream {
Ok(head)
}

fn has_body(&self) -> bool {
true
}

#[cfg(feature = "timeouts")]
#[inline]
fn set_read_timeout(&self, _dur: Option<Duration>) -> io::Result<()> {
Expand Down
2 changes: 2 additions & 0 deletions src/http/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ pub trait HttpMessage: Write + Read + Send + Any + Typeable + Debug {
fn set_write_timeout(&self, dur: Option<Duration>) -> io::Result<()>;
/// Closes the underlying HTTP connection.
fn close_connection(&mut self) -> ::Result<()>;
/// Returns whether the incoming message has a body.
fn has_body(&self) -> bool;
}

impl HttpMessage {
Expand Down