diff --git a/README.md b/README.md index e6ff5b6..b363c25 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ - [x] Auth headers - [x] Repeated headers - [x] Cookie jar in agent +- [ ] Write tests for send body content-length +- [ ] Write tests for send body chunked - [ ] Forms with application/x-www-form-urlencoded - [ ] multipart/form-data - [ ] Connection reuse/keep-alive with pool diff --git a/src/conn.rs b/src/conn.rs index e6dd4cc..c1d194b 100644 --- a/src/conn.rs +++ b/src/conn.rs @@ -18,7 +18,7 @@ impl ConnectionPool { url: &Url, redirects: u32, mut jar: Option<&mut CookieJar>, - payload: Payload, + body: SizedReader, ) -> Result { // @@ -33,6 +33,12 @@ impl ConnectionPool { }; let headers = request.headers.iter().chain(cookie_headers.iter()); + let do_chunk = request.header("transfer-encoding") + // if the user has set an encoding header, obey that. + .map(|enc| enc.len() > 0) + // otherwise, no chunking. + .unwrap_or(false); + // open socket let mut stream = match url.scheme() { "http" => connect_http(request, &url), @@ -50,6 +56,13 @@ impl ConnectionPool { for header in headers { write!(prelude, "{}: {}\r\n", header.name(), header.value())?; } + // chunking and Content-Length headers are mutually exclusive + // also don't write this if the user has set it themselves + if !do_chunk && !request.has("content-length") { + if let Some(size) = body.size { + write!(prelude, "Content-Length: {}\r\n", size)?; + } + } write!(prelude, "\r\n")?; stream.write_all(&mut prelude[..])?; @@ -91,18 +104,19 @@ impl ConnectionPool { // perform the redirect differently depending on 3xx code. return match resp.status { 301 | 302 | 303 => { - send_payload(&request, payload, &mut stream)?; - self.connect(request, "GET", &new_url, redirects - 1, jar, Payload::Empty) + send_body(body, do_chunk, &mut stream)?; + let empty = Payload::Empty.into_read(); + self.connect(request, "GET", &new_url, redirects - 1, jar, empty) } 307 | 308 | _ => { - self.connect(request, method, &new_url, redirects - 1, jar, payload) + self.connect(request, method, &new_url, redirects - 1, jar, body) } }; } } - // send the payload (which can be empty now depending on redirects) - send_payload(&request, payload, &mut stream)?; + // send the body (which can be empty now depending on redirects) + send_body(body, do_chunk, &mut stream)?; // since it is not a redirect, give away the incoming stream to the response object resp.set_stream(stream); @@ -112,28 +126,11 @@ impl ConnectionPool { } } - -fn send_payload(request: &Request, payload: Payload, stream: &mut Stream) -> IoResult<()> { - // - let (size, reader) = payload.into_read(); - - let do_chunk = request.header("transfer-encoding") - // if the user has set an encoding header, obey that. - .map(|enc| enc.eq_ignore_ascii_case("chunked")) - // if the content has a size - .ok_or_else(|| size. - // or if the user set a content-length header - or_else(|| - request.header("content-length").map(|len| len.parse::().unwrap_or(0))) - // and that size is larger than 1MB, chunk, - .map(|size| size > CHUNK_SIZE)) - // otherwise, assume chunking since it can be really big. - .unwrap_or(true); - +fn send_body(body: SizedReader, do_chunk: bool, stream: &mut Stream) -> IoResult<()> { if do_chunk { - pipe(reader, chunked_transfer::Encoder::new(stream))?; + pipe(body.reader, chunked_transfer::Encoder::new(stream))?; } else { - pipe(reader, stream)?; + pipe(body.reader, stream)?; } Ok(()) diff --git a/src/request.rs b/src/request.rs index c1024c1..8909d7c 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,9 +1,9 @@ -use qstring::QString; use super::SerdeValue; -use std::sync::Arc; -use std::io::Cursor; -use std::io::empty; +use qstring::QString; use serde_json; +use std::io::empty; +use std::io::Cursor; +use std::sync::Arc; lazy_static! { static ref URL_BASE: Url = { Url::parse("http://localhost/").expect("Failed to parse URL_BASE") }; @@ -48,23 +48,34 @@ impl Default for Payload { } } +struct SizedReader { + size: Option, + reader: Box, +} + +impl SizedReader { + fn new(size: Option, reader: Box) -> Self { + SizedReader { size, reader } + } +} + impl Payload { - fn into_read(self) -> (Option, Box) { + fn into_read(self) -> SizedReader { match self { - Payload::Empty => (Some(0), Box::new(empty())), + Payload::Empty => SizedReader::new(Some(0), Box::new(empty())), Payload::Text(s) => { let bytes = s.into_bytes(); let len = bytes.len(); let cursor = Cursor::new(bytes); - (Some(len), Box::new(cursor)) + SizedReader::new(Some(len), Box::new(cursor)) } Payload::JSON(v) => { let bytes = serde_json::to_vec(&v).expect("Bad JSON in payload"); let len = bytes.len(); let cursor = Cursor::new(bytes); - (Some(len), Box::new(cursor)) + SizedReader::new(Some(len), Box::new(cursor)) } - Payload::Reader(read) => (None, read), + Payload::Reader(read) => SizedReader::new(None, read), } } } @@ -121,15 +132,20 @@ impl Request { &url, self.redirects, None, - payload, + payload.into_read(), ) } else { // reuse connection pool. let state = state.as_mut().unwrap(); let jar = &mut state.jar; - state - .pool - .connect(self, &self.method, &url, self.redirects, Some(jar), payload) + state.pool.connect( + self, + &self.method, + &url, + self.redirects, + Some(jar), + payload.into_read(), + ) } }) .unwrap_or_else(|e| e.into()) @@ -137,6 +153,8 @@ impl Request { /// Send data a json value. /// + /// The `Content-Length` header is implicitly set to the length of the serialized value. + /// /// ``` /// #[macro_use] /// extern crate ureq;