Skip to content

Commit ab5e477

Browse files
author
Jason Schein
committed
Add (configurable) automatic gzip decompression.
1 parent 1d3c134 commit ab5e477

File tree

4 files changed

+159
-19
lines changed

4 files changed

+159
-19
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ serde = "0.9"
1717
serde_json = "0.9"
1818
serde_urlencoded = "0.4"
1919
url = "1.2"
20+
libflate = "0.1.3"
2021

2122
[dev-dependencies]
2223
env_logger = "0.3"

src/client.rs

Lines changed: 114 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
use std::fmt;
22
use std::io::{self, Read};
33
use std::sync::{Arc, Mutex};
4+
use std::sync::atomic::{AtomicBool, Ordering};
45

56
use hyper::client::IntoUrl;
6-
use hyper::header::{Headers, ContentType, Location, Referer, UserAgent, Accept};
7+
use hyper::header::{Headers, ContentType, Location, Referer, UserAgent, Accept, ContentEncoding, Encoding, ContentLength};
78
use hyper::method::Method;
89
use hyper::status::StatusCode;
910
use hyper::version::HttpVersion;
@@ -38,10 +39,16 @@ impl Client {
3839
inner: Arc::new(ClientRef {
3940
hyper: client,
4041
redirect_policy: Mutex::new(RedirectPolicy::default()),
42+
auto_ungzip: AtomicBool::new(true),
4143
}),
4244
})
4345
}
4446

47+
/// Enable auto gzip decompression by checking the ContentEncoding response header.
48+
pub fn gzip(&mut self, enable: bool) {
49+
self.inner.auto_ungzip.store(enable, Ordering::Relaxed);
50+
}
51+
4552
/// Set a `RedirectPolicy` for this client.
4653
pub fn redirect(&mut self, policy: RedirectPolicy) {
4754
*self.inner.redirect_policy.lock().unwrap() = policy;
@@ -94,13 +101,15 @@ impl fmt::Debug for Client {
94101
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
95102
f.debug_struct("Client")
96103
.field("redirect_policy", &self.inner.redirect_policy)
104+
.field("auto_ungzip", &self.inner.auto_ungzip)
97105
.finish()
98106
}
99107
}
100108

101109
struct ClientRef {
102110
hyper: ::hyper::Client,
103111
redirect_policy: Mutex<RedirectPolicy>,
112+
auto_ungzip: AtomicBool,
104113
}
105114

106115
fn new_hyper_client() -> ::Result<::hyper::Client> {
@@ -268,7 +277,7 @@ impl RequestBuilder {
268277
loc
269278
} else {
270279
return Ok(Response {
271-
inner: res
280+
inner: Decoder::from_hyper_response(res, client.auto_ungzip.load(Ordering::Relaxed))
272281
});
273282
}
274283
};
@@ -282,14 +291,14 @@ impl RequestBuilder {
282291
} else {
283292
debug!("redirect_policy disallowed redirection to '{}'", loc);
284293
return Ok(Response {
285-
inner: res
294+
inner: Decoder::from_hyper_response(res, client.auto_ungzip.load(Ordering::Relaxed))
286295
})
287296
}
288297
},
289298
Err(e) => {
290299
debug!("Location header had invalid URI: {:?}", e);
291300
return Ok(Response {
292-
inner: res
301+
inner: Decoder::from_hyper_response(res, client.auto_ungzip.load(Ordering::Relaxed))
293302
})
294303
}
295304
};
@@ -299,7 +308,7 @@ impl RequestBuilder {
299308
//TODO: removeSensitiveHeaders(&mut headers, &url);
300309
} else {
301310
return Ok(Response {
302-
inner: res
311+
inner: Decoder::from_hyper_response(res, client.auto_ungzip.load(Ordering::Relaxed))
303312
});
304313
}
305314
}
@@ -318,26 +327,56 @@ impl fmt::Debug for RequestBuilder {
318327

319328
/// A Response to a submitted `Request`.
320329
pub struct Response {
321-
inner: ::hyper::client::Response,
330+
inner: Decoder,
331+
}
332+
333+
impl fmt::Debug for Response {
334+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
335+
return match &self.inner {
336+
&Decoder::PlainText(ref hyper_response) => {
337+
f.debug_struct("Response")
338+
.field("status", &hyper_response.status)
339+
.field("headers", &hyper_response.headers)
340+
.field("version", &hyper_response.version)
341+
.finish()
342+
},
343+
&Decoder::Gzip{ref status, ref version, ref headers, ..} => {
344+
f.debug_struct("Response")
345+
.field("status", &status)
346+
.field("headers", &headers)
347+
.field("version", &version)
348+
.finish()
349+
}
350+
}
351+
}
322352
}
323353

324354
impl Response {
325355
/// Get the `StatusCode`.
326356
#[inline]
327357
pub fn status(&self) -> &StatusCode {
328-
&self.inner.status
358+
match &self.inner {
359+
&Decoder::PlainText(ref hyper_response) => &hyper_response.status,
360+
&Decoder::Gzip{ref status, ..} => status
361+
}
329362
}
330363

331364
/// Get the `Headers`.
332365
#[inline]
333366
pub fn headers(&self) -> &Headers {
334-
&self.inner.headers
367+
match &self.inner {
368+
&Decoder::PlainText(ref hyper_response) => &hyper_response.headers,
369+
&Decoder::Gzip{ref headers, ..} => headers
370+
}
335371
}
336372

337373
/// Get the `HttpVersion`.
338374
#[inline]
339375
pub fn version(&self) -> &HttpVersion {
340-
&self.inner.version
376+
match &self.inner {
377+
&Decoder::PlainText(ref hyper_response) => &hyper_response.version,
378+
&Decoder::Gzip{ref version, ..} => version
379+
}
341380
}
342381

343382
/// Try and deserialize the response body as JSON.
@@ -347,6 +386,72 @@ impl Response {
347386
}
348387
}
349388

389+
enum Decoder {
390+
/// A `PlainText` decoder just returns the response content as is.
391+
PlainText(::hyper::client::Response),
392+
/// A `Gzip` decoder will uncompress the gziped response content before returning it.
393+
Gzip {
394+
decoder: ::libflate::gzip::Decoder<::hyper::client::Response>,
395+
headers: ::hyper::header::Headers,
396+
version: ::hyper::version::HttpVersion,
397+
status: ::hyper::status::StatusCode,
398+
}
399+
}
400+
401+
impl Decoder {
402+
/// Constructs a Decoder from a hyper request.
403+
///
404+
/// A decoder is just a wrapper around the hyper request that knows
405+
/// how to decode the content body of the request.
406+
///
407+
/// Uses the correct variant by inspecting the Content-Encoding header.
408+
fn from_hyper_response(res: ::hyper::client::Response, check_gzip: bool) -> Self {
409+
if !check_gzip {
410+
return Decoder::PlainText(res);
411+
}
412+
413+
let mut is_gzip = false;
414+
match res.headers.get::<ContentEncoding>() {
415+
Some(encoding_types) => {
416+
if encoding_types.contains(&Encoding::Gzip) {
417+
is_gzip = true;
418+
}
419+
if let Some(content_length) = res.headers.get::<ContentLength>() {
420+
if content_length.0 == 0 {
421+
warn!("GZipped response with content-length of 0");
422+
is_gzip = false;
423+
}
424+
}
425+
}
426+
_ => {}
427+
}
428+
429+
if is_gzip {
430+
return Decoder::Gzip {
431+
status: res.status.clone(),
432+
version: res.version.clone(),
433+
headers: res.headers.clone(),
434+
decoder: ::libflate::gzip::Decoder::new(res).unwrap(),
435+
};
436+
} else {
437+
return Decoder::PlainText(res);
438+
}
439+
}
440+
}
441+
442+
impl Read for Decoder {
443+
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
444+
match self {
445+
&mut Decoder::PlainText(ref mut hyper_response) => {
446+
hyper_response.read(buf)
447+
},
448+
&mut Decoder::Gzip{ref mut decoder, ..} => {
449+
decoder.read(buf)
450+
}
451+
}
452+
}
453+
}
454+
350455
/// Read the body of the Response.
351456
impl Read for Response {
352457
#[inline]
@@ -355,16 +460,6 @@ impl Read for Response {
355460
}
356461
}
357462

358-
impl fmt::Debug for Response {
359-
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
360-
f.debug_struct("Response")
361-
.field("status", self.status())
362-
.field("headers", self.headers())
363-
.field("version", self.version())
364-
.finish()
365-
}
366-
}
367-
368463
#[cfg(test)]
369464
mod tests {
370465
use super::*;

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
extern crate hyper;
102102

103103
#[macro_use] extern crate log;
104+
extern crate libflate;
104105
extern crate hyper_native_tls;
105106
extern crate serde;
106107
extern crate serde_json;

tests/client.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
extern crate reqwest;
2+
extern crate libflate;
23

34
#[macro_use] mod server;
45

56
use std::io::Read;
7+
use std::io::prelude::*;
68

79
#[test]
810
fn test_get() {
@@ -248,3 +250,44 @@ fn test_accept_header_is_not_changed_if_set() {
248250

249251
assert_eq!(res.status(), &reqwest::StatusCode::Ok);
250252
}
253+
254+
#[test]
255+
fn test_gzip_response() {
256+
let mut encoder = ::libflate::gzip::Encoder::new(Vec::new()).unwrap();
257+
match encoder.write(b"test request") {
258+
Ok(n) => assert!(n > 0, "Failed to write to encoder."),
259+
_ => panic!("Failed to gzip encode string.")
260+
};
261+
262+
let gzipped_content = encoder.finish().into_result().unwrap();
263+
264+
let mut response = format!("\
265+
HTTP/1.1 200 OK\r\n\
266+
Server: test-accept\r\n\
267+
Content-Encoding: gzip\r\n\
268+
Content-Length: {}\r\n\
269+
\r\n", &gzipped_content.len())
270+
.into_bytes();
271+
response.extend(&gzipped_content);
272+
273+
let server = server! {
274+
request: b"\
275+
GET /gzip HTTP/1.1\r\n\
276+
Host: $HOST\r\n\
277+
User-Agent: $USERAGENT\r\n\
278+
Accept: */*\r\n\
279+
\r\n\
280+
",
281+
response: response
282+
};
283+
let mut res = reqwest::get(&format!("http://{}/gzip", server.addr()))
284+
.unwrap();
285+
286+
let mut body = ::std::string::String::new();
287+
match res.read_to_string(&mut body) {
288+
Ok(n) => assert!(n > 0, "Failed to write to buffer."),
289+
_ => panic!("Failed to write to buffer.")
290+
};
291+
292+
assert_eq!(body, "test request");
293+
}

0 commit comments

Comments
 (0)