//! Client Responses use std::io::{self, Read}; use std::marker::PhantomData; use std::net::Shutdown; use buffer::BufReader; use header; use header::{ContentLength, TransferEncoding}; use header::Encoding::Chunked; use net::{NetworkStream, HttpStream}; use http::{self, HttpReader, RawStatus}; use http::HttpReader::{SizedReader, ChunkedReader, EofReader}; use status; use version; /// A response for a client request to a remote server. #[derive(Debug)] pub struct Response { /// The status from the server. pub status: status::StatusCode, /// The headers from the server. pub headers: header::Headers, /// The HTTP version of this response from the server. pub version: version::HttpVersion, status_raw: RawStatus, body: HttpReader>>, _marker: PhantomData, } impl Response { /// Creates a new response from a server. pub fn new(stream: Box) -> ::Result { trace!("Response::new"); let mut stream = BufReader::new(stream); let head = try!(http::parse_response(&mut stream)); let raw_status = head.subject; let headers = head.headers; let status = status::StatusCode::from_u16(raw_status.0); debug!("version={:?}, status={:?}", head.version, status); debug!("headers={:?}", headers); let body = if headers.has::() { match headers.get::() { Some(&TransferEncoding(ref codings)) => { if codings.len() > 1 { trace!("TODO: #2 handle other codings: {:?}", codings); }; if codings.contains(&Chunked) { ChunkedReader(stream, None) } else { trace!("not chuncked. read till eof"); EofReader(stream) } } None => unreachable!() } } else if headers.has::() { match headers.get::() { Some(&ContentLength(len)) => SizedReader(stream, len), None => unreachable!() } } else { trace!("neither Transfer-Encoding nor Content-Length"); EofReader(stream) }; Ok(Response { status: status, version: head.version, headers: headers, body: body, status_raw: raw_status, _marker: PhantomData, }) } /// Get the raw status code and reason. pub fn status_raw(&self) -> &RawStatus { &self.status_raw } /// Consumes the Request to return the NetworkStream underneath. pub fn into_inner(self) -> Box { self.body.into_inner().into_inner() } } impl Read for Response { #[inline] fn read(&mut self, buf: &mut [u8]) -> io::Result { let count = try!(self.body.read(buf)); if count == 0 { if !http::should_keep_alive(self.version, &self.headers) { try!(self.body.get_mut().get_mut().close(Shutdown::Both)); } } Ok(count) } } #[cfg(test)] mod tests { use std::borrow::Cow::Borrowed; use std::io::{self, Read}; use std::marker::PhantomData; use buffer::BufReader; use header::Headers; use header::TransferEncoding; use header::Encoding; use http::HttpReader::EofReader; use http::RawStatus; use mock::MockStream; use status; use version; use super::Response; fn read_to_string(mut r: Response) -> io::Result { let mut s = String::new(); try!(r.read_to_string(&mut s)); Ok(s) } #[test] fn test_into_inner() { let res = Response { status: status::StatusCode::Ok, headers: Headers::new(), version: version::HttpVersion::Http11, body: EofReader(BufReader::new(Box::new(MockStream::new()))), status_raw: RawStatus(200, Borrowed("OK")), _marker: PhantomData, }; let b = res.into_inner().downcast::().ok().unwrap(); assert_eq!(b, Box::new(MockStream::new())); } #[test] fn test_parse_chunked_response() { let stream = MockStream::with_input(b"\ HTTP/1.1 200 OK\r\n\ Transfer-Encoding: chunked\r\n\ \r\n\ 1\r\n\ q\r\n\ 2\r\n\ we\r\n\ 2\r\n\ rt\r\n\ 0\r\n\ \r\n" ); let res = Response::new(Box::new(stream)).unwrap(); // The status line is correct? assert_eq!(res.status, status::StatusCode::Ok); assert_eq!(res.version, version::HttpVersion::Http11); // The header is correct? match res.headers.get::() { Some(encodings) => { assert_eq!(1, encodings.len()); assert_eq!(Encoding::Chunked, encodings[0]); }, None => panic!("Transfer-Encoding: chunked expected!"), }; // The body is correct? assert_eq!(read_to_string(res).unwrap(), "qwert".to_owned()); } /// Tests that when a chunk size is not a valid radix-16 number, an error /// is returned. #[test] fn test_invalid_chunk_size_not_hex_digit() { let stream = MockStream::with_input(b"\ HTTP/1.1 200 OK\r\n\ Transfer-Encoding: chunked\r\n\ \r\n\ X\r\n\ 1\r\n\ 0\r\n\ \r\n" ); let res = Response::new(Box::new(stream)).unwrap(); assert!(read_to_string(res).is_err()); } /// Tests that when a chunk size contains an invalid extension, an error is /// returned. #[test] fn test_invalid_chunk_size_extension() { let stream = MockStream::with_input(b"\ HTTP/1.1 200 OK\r\n\ Transfer-Encoding: chunked\r\n\ \r\n\ 1 this is an invalid extension\r\n\ 1\r\n\ 0\r\n\ \r\n" ); let res = Response::new(Box::new(stream)).unwrap(); assert!(read_to_string(res).is_err()); } /// Tests that when a valid extension that contains a digit is appended to /// the chunk size, the chunk is correctly read. #[test] fn test_chunk_size_with_extension() { let stream = MockStream::with_input(b"\ HTTP/1.1 200 OK\r\n\ Transfer-Encoding: chunked\r\n\ \r\n\ 1;this is an extension with a digit 1\r\n\ 1\r\n\ 0\r\n\ \r\n" ); let res = Response::new(Box::new(stream)).unwrap(); assert_eq!(read_to_string(res).unwrap(), "1".to_owned()); } }