461 lines
13 KiB
Rust
461 lines
13 KiB
Rust
use std::borrow::Cow;
|
|
use std::fmt;
|
|
use std::net::SocketAddr;
|
|
use std::pin::Pin;
|
|
|
|
use bytes::Bytes;
|
|
use encoding_rs::{Encoding, UTF_8};
|
|
use futures_util::stream::StreamExt;
|
|
use hyper::client::connect::HttpInfo;
|
|
use hyper::{HeaderMap, StatusCode, Version};
|
|
use mime::Mime;
|
|
#[cfg(feature = "json")]
|
|
use serde::de::DeserializeOwned;
|
|
#[cfg(feature = "json")]
|
|
use serde_json;
|
|
use tokio::time::Sleep;
|
|
use url::Url;
|
|
|
|
use super::body::Body;
|
|
use super::decoder::{Accepts, Decoder};
|
|
#[cfg(feature = "cookies")]
|
|
use crate::cookie;
|
|
use crate::response::ResponseUrl;
|
|
|
|
/// A Response to a submitted `Request`.
|
|
pub struct Response {
|
|
status: StatusCode,
|
|
headers: HeaderMap,
|
|
// Boxed to save space (11 words to 1 word), and it's not accessed
|
|
// frequently internally.
|
|
url: Box<Url>,
|
|
body: Decoder,
|
|
version: Version,
|
|
extensions: http::Extensions,
|
|
}
|
|
|
|
impl Response {
|
|
pub(super) fn new(
|
|
res: hyper::Response<hyper::Body>,
|
|
url: Url,
|
|
accepts: Accepts,
|
|
timeout: Option<Pin<Box<Sleep>>>,
|
|
) -> Response {
|
|
let (parts, body) = res.into_parts();
|
|
let status = parts.status;
|
|
let version = parts.version;
|
|
let extensions = parts.extensions;
|
|
|
|
let mut headers = parts.headers;
|
|
let decoder = Decoder::detect(&mut headers, Body::response(body, timeout), accepts);
|
|
|
|
Response {
|
|
status,
|
|
headers,
|
|
url: Box::new(url),
|
|
body: decoder,
|
|
version,
|
|
extensions,
|
|
}
|
|
}
|
|
|
|
/// Get the `StatusCode` of this `Response`.
|
|
#[inline]
|
|
pub fn status(&self) -> StatusCode {
|
|
self.status
|
|
}
|
|
|
|
/// Get the HTTP `Version` of this `Response`.
|
|
#[inline]
|
|
pub fn version(&self) -> Version {
|
|
self.version
|
|
}
|
|
|
|
/// Get the `Headers` of this `Response`.
|
|
#[inline]
|
|
pub fn headers(&self) -> &HeaderMap {
|
|
&self.headers
|
|
}
|
|
|
|
/// Get a mutable reference to the `Headers` of this `Response`.
|
|
#[inline]
|
|
pub fn headers_mut(&mut self) -> &mut HeaderMap {
|
|
&mut self.headers
|
|
}
|
|
|
|
/// Get the content-length of this response, if known.
|
|
///
|
|
/// Reasons it may not be known:
|
|
///
|
|
/// - The server didn't send a `content-length` header.
|
|
/// - The response is compressed and automatically decoded (thus changing
|
|
/// the actual decoded length).
|
|
pub fn content_length(&self) -> Option<u64> {
|
|
use hyper::body::HttpBody;
|
|
|
|
HttpBody::size_hint(&self.body).exact()
|
|
}
|
|
|
|
/// Retrieve the cookies contained in the response.
|
|
///
|
|
/// Note that invalid 'Set-Cookie' headers will be ignored.
|
|
///
|
|
/// # Optional
|
|
///
|
|
/// This requires the optional `cookies` feature to be enabled.
|
|
#[cfg(feature = "cookies")]
|
|
#[cfg_attr(docsrs, doc(cfg(feature = "cookies")))]
|
|
pub fn cookies<'a>(&'a self) -> impl Iterator<Item = cookie::Cookie<'a>> + 'a {
|
|
cookie::extract_response_cookies(&self.headers).filter_map(Result::ok)
|
|
}
|
|
|
|
/// Get the final `Url` of this `Response`.
|
|
#[inline]
|
|
pub fn url(&self) -> &Url {
|
|
&self.url
|
|
}
|
|
|
|
/// Get the remote address used to get this `Response`.
|
|
pub fn remote_addr(&self) -> Option<SocketAddr> {
|
|
self.extensions
|
|
.get::<HttpInfo>()
|
|
.map(|info| info.remote_addr())
|
|
}
|
|
|
|
/// Returns a reference to the associated extensions.
|
|
pub fn extensions(&self) -> &http::Extensions {
|
|
&self.extensions
|
|
}
|
|
|
|
/// Returns a mutable reference to the associated extensions.
|
|
pub fn extensions_mut(&mut self) -> &mut http::Extensions {
|
|
&mut self.extensions
|
|
}
|
|
|
|
// body methods
|
|
|
|
/// Get the full response text.
|
|
///
|
|
/// This method decodes the response body with BOM sniffing
|
|
/// and with malformed sequences replaced with the REPLACEMENT CHARACTER.
|
|
/// Encoding is determinated from the `charset` parameter of `Content-Type` header,
|
|
/// and defaults to `utf-8` if not presented.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```
|
|
/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|
/// let content = reqwest::get("http://httpbin.org/range/26")
|
|
/// .await?
|
|
/// .text()
|
|
/// .await?;
|
|
///
|
|
/// println!("text: {:?}", content);
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
pub async fn text(self) -> crate::Result<String> {
|
|
self.text_with_charset("utf-8").await
|
|
}
|
|
|
|
/// Get the full response text given a specific encoding.
|
|
///
|
|
/// This method decodes the response body with BOM sniffing
|
|
/// and with malformed sequences replaced with the REPLACEMENT CHARACTER.
|
|
/// You can provide a default encoding for decoding the raw message, while the
|
|
/// `charset` parameter of `Content-Type` header is still prioritized. For more information
|
|
/// about the possible encoding name, please go to [`encoding_rs`] docs.
|
|
///
|
|
/// [`encoding_rs`]: https://docs.rs/encoding_rs/0.8/encoding_rs/#relationship-with-windows-code-pages
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```
|
|
/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|
/// let content = reqwest::get("http://httpbin.org/range/26")
|
|
/// .await?
|
|
/// .text_with_charset("utf-8")
|
|
/// .await?;
|
|
///
|
|
/// println!("text: {:?}", content);
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
pub async fn text_with_charset(self, default_encoding: &str) -> crate::Result<String> {
|
|
let content_type = self
|
|
.headers
|
|
.get(crate::header::CONTENT_TYPE)
|
|
.and_then(|value| value.to_str().ok())
|
|
.and_then(|value| value.parse::<Mime>().ok());
|
|
let encoding_name = content_type
|
|
.as_ref()
|
|
.and_then(|mime| mime.get_param("charset").map(|charset| charset.as_str()))
|
|
.unwrap_or(default_encoding);
|
|
let encoding = Encoding::for_label(encoding_name.as_bytes()).unwrap_or(UTF_8);
|
|
|
|
let full = self.bytes().await?;
|
|
|
|
let (text, _, _) = encoding.decode(&full);
|
|
if let Cow::Owned(s) = text {
|
|
return Ok(s);
|
|
}
|
|
unsafe {
|
|
// decoding returned Cow::Borrowed, meaning these bytes
|
|
// are already valid utf8
|
|
Ok(String::from_utf8_unchecked(full.to_vec()))
|
|
}
|
|
}
|
|
|
|
/// Try to deserialize the response body as JSON.
|
|
///
|
|
/// # Optional
|
|
///
|
|
/// This requires the optional `json` feature enabled.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// # extern crate reqwest;
|
|
/// # extern crate serde;
|
|
/// #
|
|
/// # use reqwest::Error;
|
|
/// # use serde::Deserialize;
|
|
/// #
|
|
/// // This `derive` requires the `serde` dependency.
|
|
/// #[derive(Deserialize)]
|
|
/// struct Ip {
|
|
/// origin: String,
|
|
/// }
|
|
///
|
|
/// # async fn run() -> Result<(), Error> {
|
|
/// let ip = reqwest::get("http://httpbin.org/ip")
|
|
/// .await?
|
|
/// .json::<Ip>()
|
|
/// .await?;
|
|
///
|
|
/// println!("ip: {}", ip.origin);
|
|
/// # Ok(())
|
|
/// # }
|
|
/// #
|
|
/// # fn main() { }
|
|
/// ```
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// This method fails whenever the response body is not in JSON format
|
|
/// or it cannot be properly deserialized to target type `T`. For more
|
|
/// details please see [`serde_json::from_reader`].
|
|
///
|
|
/// [`serde_json::from_reader`]: https://docs.serde.rs/serde_json/fn.from_reader.html
|
|
#[cfg(feature = "json")]
|
|
#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
|
|
pub async fn json<T: DeserializeOwned>(self) -> crate::Result<T> {
|
|
let full = self.bytes().await?;
|
|
|
|
serde_json::from_slice(&full).map_err(crate::error::decode)
|
|
}
|
|
|
|
/// Get the full response body as `Bytes`.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```
|
|
/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|
/// let bytes = reqwest::get("http://httpbin.org/ip")
|
|
/// .await?
|
|
/// .bytes()
|
|
/// .await?;
|
|
///
|
|
/// println!("bytes: {:?}", bytes);
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
pub async fn bytes(self) -> crate::Result<Bytes> {
|
|
hyper::body::to_bytes(self.body).await
|
|
}
|
|
|
|
/// Stream a chunk of the response body.
|
|
///
|
|
/// When the response body has been exhausted, this will return `None`.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```
|
|
/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|
/// let mut res = reqwest::get("https://hyper.rs").await?;
|
|
///
|
|
/// while let Some(chunk) = res.chunk().await? {
|
|
/// println!("Chunk: {:?}", chunk);
|
|
/// }
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
pub async fn chunk(&mut self) -> crate::Result<Option<Bytes>> {
|
|
if let Some(item) = self.body.next().await {
|
|
Ok(Some(item?))
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
/// Convert the response into a `Stream` of `Bytes` from the body.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```
|
|
/// use futures_util::StreamExt;
|
|
///
|
|
/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|
/// let mut stream = reqwest::get("http://httpbin.org/ip")
|
|
/// .await?
|
|
/// .bytes_stream();
|
|
///
|
|
/// while let Some(item) = stream.next().await {
|
|
/// println!("Chunk: {:?}", item?);
|
|
/// }
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
///
|
|
/// # Optional
|
|
///
|
|
/// This requires the optional `stream` feature to be enabled.
|
|
#[cfg(feature = "stream")]
|
|
#[cfg_attr(docsrs, doc(cfg(feature = "stream")))]
|
|
pub fn bytes_stream(self) -> impl futures_core::Stream<Item = crate::Result<Bytes>> {
|
|
self.body
|
|
}
|
|
|
|
// util methods
|
|
|
|
/// Turn a response into an error if the server returned an error.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```
|
|
/// # use reqwest::Response;
|
|
/// fn on_response(res: Response) {
|
|
/// match res.error_for_status() {
|
|
/// Ok(_res) => (),
|
|
/// Err(err) => {
|
|
/// // asserting a 400 as an example
|
|
/// // it could be any status between 400...599
|
|
/// assert_eq!(
|
|
/// err.status(),
|
|
/// Some(reqwest::StatusCode::BAD_REQUEST)
|
|
/// );
|
|
/// }
|
|
/// }
|
|
/// }
|
|
/// # fn main() {}
|
|
/// ```
|
|
pub fn error_for_status(self) -> crate::Result<Self> {
|
|
if self.status.is_client_error() || self.status.is_server_error() {
|
|
Err(crate::error::status_code(*self.url, self.status))
|
|
} else {
|
|
Ok(self)
|
|
}
|
|
}
|
|
|
|
/// Turn a reference to a response into an error if the server returned an error.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```
|
|
/// # use reqwest::Response;
|
|
/// fn on_response(res: &Response) {
|
|
/// match res.error_for_status_ref() {
|
|
/// Ok(_res) => (),
|
|
/// Err(err) => {
|
|
/// // asserting a 400 as an example
|
|
/// // it could be any status between 400...599
|
|
/// assert_eq!(
|
|
/// err.status(),
|
|
/// Some(reqwest::StatusCode::BAD_REQUEST)
|
|
/// );
|
|
/// }
|
|
/// }
|
|
/// }
|
|
/// # fn main() {}
|
|
/// ```
|
|
pub fn error_for_status_ref(&self) -> crate::Result<&Self> {
|
|
if self.status.is_client_error() || self.status.is_server_error() {
|
|
Err(crate::error::status_code(*self.url.clone(), self.status))
|
|
} else {
|
|
Ok(self)
|
|
}
|
|
}
|
|
|
|
// private
|
|
|
|
// The Response's body is an implementation detail.
|
|
// You no longer need to get a reference to it, there are async methods
|
|
// on the `Response` itself.
|
|
//
|
|
// This method is just used by the blocking API.
|
|
#[cfg(feature = "blocking")]
|
|
pub(crate) fn body_mut(&mut self) -> &mut Decoder {
|
|
&mut self.body
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for Response {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
f.debug_struct("Response")
|
|
.field("url", self.url())
|
|
.field("status", &self.status())
|
|
.field("headers", self.headers())
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
impl<T: Into<Body>> From<http::Response<T>> for Response {
|
|
fn from(r: http::Response<T>) -> Response {
|
|
let (mut parts, body) = r.into_parts();
|
|
let body = body.into();
|
|
let body = Decoder::detect(&mut parts.headers, body, Accepts::none());
|
|
let url = parts
|
|
.extensions
|
|
.remove::<ResponseUrl>()
|
|
.unwrap_or_else(|| ResponseUrl(Url::parse("http://no.url.provided.local").unwrap()));
|
|
let url = url.0;
|
|
Response {
|
|
status: parts.status,
|
|
headers: parts.headers,
|
|
url: Box::new(url),
|
|
body,
|
|
version: parts.version,
|
|
extensions: parts.extensions,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A `Response` can be piped as the `Body` of another request.
|
|
impl From<Response> for Body {
|
|
fn from(r: Response) -> Body {
|
|
Body::stream(r.body)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::Response;
|
|
use crate::ResponseBuilderExt;
|
|
use http::response::Builder;
|
|
use url::Url;
|
|
|
|
#[test]
|
|
fn test_from_http_response() {
|
|
let url = Url::parse("http://example.com").unwrap();
|
|
let response = Builder::new()
|
|
.status(200)
|
|
.url(url.clone())
|
|
.body("foo")
|
|
.unwrap();
|
|
let response = Response::from(response);
|
|
|
|
assert_eq!(response.status(), 200);
|
|
assert_eq!(*response.url(), url);
|
|
}
|
|
}
|