mirror of https://github.com/http-rs/surf
625 lines
20 KiB
Rust
625 lines
20 KiB
Rust
use std::convert::TryFrom;
|
|
use std::fmt;
|
|
use std::sync::Arc;
|
|
|
|
use crate::http::{Method, Url};
|
|
use crate::middleware::{Middleware, Next};
|
|
use crate::{Config, HttpClient, Request, RequestBuilder, Response, Result};
|
|
|
|
use cfg_if::cfg_if;
|
|
|
|
cfg_if! {
|
|
if #[cfg(feature = "curl-client")] {
|
|
use http_client::isahc::IsahcClient as DefaultClient;
|
|
} else if #[cfg(feature = "wasm-client")] {
|
|
use http_client::wasm::WasmClient as DefaultClient;
|
|
} else if #[cfg(any(feature = "h1-client", feature = "h1-client-rustls", feature = "h1-client-no-tls"))] {
|
|
use http_client::h1::H1Client as DefaultClient;
|
|
} else if #[cfg(feature = "hyper-client")] {
|
|
use http_client::hyper::HyperClient as DefaultClient;
|
|
}
|
|
}
|
|
cfg_if! {
|
|
if #[cfg(any(feature = "curl-client", feature = "h1-client", feature = "h1-client-rustls", feature = "hyper-client"))] {
|
|
use once_cell::sync::Lazy;
|
|
static GLOBAL_CLIENT: Lazy<Arc<DefaultClient>> = Lazy::new(|| Arc::new(DefaultClient::new()));
|
|
}
|
|
}
|
|
|
|
/// An HTTP client, capable of sending `Request`s and running a middleware stack.
|
|
///
|
|
/// Can be optionally set with a base url.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```no_run
|
|
/// # #[async_std::main]
|
|
/// # async fn main() -> surf::Result<()> {
|
|
/// let client = surf::Client::new();
|
|
/// let res1 = client.recv_string(surf::get("https://httpbin.org/get"));
|
|
/// let res2 = client.recv_string(surf::get("https://httpbin.org/get"));
|
|
/// let (str1, str2) = futures_util::future::try_join(res1, res2).await?;
|
|
/// # Ok(()) }
|
|
/// ```
|
|
pub struct Client {
|
|
config: Config,
|
|
http_client: Arc<dyn HttpClient>,
|
|
/// Holds the middleware stack.
|
|
///
|
|
/// Note(Fishrock123): We do actually want this structure.
|
|
/// The outer Arc allows us to clone in .send() without cloning the array.
|
|
/// The Vec allows us to add middleware at runtime.
|
|
/// The inner Arc-s allow us to implement Clone without sharing the vector with the parent.
|
|
/// We don't use a Mutex around the Vec here because adding a middleware during execution should be an error.
|
|
#[allow(clippy::rc_buffer)]
|
|
middleware: Arc<Vec<Arc<dyn Middleware>>>,
|
|
}
|
|
|
|
impl Clone for Client {
|
|
/// Clones the Client.
|
|
///
|
|
/// This copies the middleware stack from the original, but shares
|
|
/// the `HttpClient` and http client config of the original.
|
|
/// Note that individual middleware in the middleware stack are
|
|
/// still shared by reference.
|
|
fn clone(&self) -> Self {
|
|
Self {
|
|
config: self.config.clone(),
|
|
http_client: self.http_client.clone(),
|
|
middleware: Arc::new(self.middleware.iter().cloned().collect()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for Client {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "Client {{}}")
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "default-client")]
|
|
impl Default for Client {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl Client {
|
|
/// Create a new `Client` instance.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```rust
|
|
/// # #[async_std::main]
|
|
/// # async fn main() -> surf::Result<()> {
|
|
/// let client = surf::Client::new();
|
|
///
|
|
/// let req = surf::get("https://httpbin.org/get");
|
|
/// let res = client.send(req).await?;
|
|
/// # Ok(()) }
|
|
/// ```
|
|
#[cfg(feature = "default-client")]
|
|
pub fn new() -> Self {
|
|
Self::with_http_client(DefaultClient::new())
|
|
}
|
|
|
|
pub(crate) fn new_shared_or_panic() -> Self {
|
|
cfg_if! {
|
|
if #[cfg(feature = "default-client")] {
|
|
Self::new_shared()
|
|
} else {
|
|
panic!("default client not configured")
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Create a new `Client` instance with an `http_client::HttpClient` backend.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```rust
|
|
/// # #[cfg(feature = "curl-client")] {
|
|
/// # use std::sync::Arc;
|
|
/// use http_client::isahc::IsahcClient;
|
|
/// let client = surf::Client::with_http_client(IsahcClient::new());
|
|
/// # }
|
|
/// ```
|
|
pub fn with_http_client<C: HttpClient>(http_client: C) -> Self {
|
|
Self::with_http_client_internal(Arc::new(http_client))
|
|
}
|
|
|
|
fn with_http_client_internal(http_client: Arc<dyn HttpClient>) -> Self {
|
|
let client = Self {
|
|
config: Config::default(),
|
|
http_client,
|
|
middleware: Arc::new(vec![]),
|
|
};
|
|
|
|
#[cfg(feature = "middleware-logger")]
|
|
let client = client.with(crate::middleware::Logger::new());
|
|
|
|
client
|
|
}
|
|
|
|
#[cfg(feature = "default-client")]
|
|
pub(crate) fn new_shared() -> Self {
|
|
cfg_if! {
|
|
if #[cfg(any(feature = "curl-client", feature = "h1-client", feature = "h1-client-rustls", feature = "hyper-client"))] {
|
|
Self::with_http_client_internal(GLOBAL_CLIENT.clone())
|
|
} else {
|
|
Self::new()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Push middleware onto the middleware stack.
|
|
///
|
|
/// See the [middleware] submodule for more information on middleware.
|
|
///
|
|
/// [middleware]: ../middleware/index.html
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```no_run
|
|
/// # #[async_std::main]
|
|
/// # async fn main() -> surf::Result<()> {
|
|
/// let req = surf::get("https://httpbin.org/get");
|
|
/// let client = surf::client()
|
|
/// .with(surf::middleware::Redirect::default());
|
|
/// let res = client.send(req).await?;
|
|
/// # Ok(()) }
|
|
/// ```
|
|
pub fn with(mut self, middleware: impl Middleware) -> Self {
|
|
let m = Arc::get_mut(&mut self.middleware)
|
|
.expect("Registering middleware is not possible after the Client has been used");
|
|
m.push(Arc::new(middleware));
|
|
self
|
|
}
|
|
|
|
/// Send a `Request` using this client.
|
|
///
|
|
/// Client middleware is run before per-request middleware.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```no_run
|
|
/// # #[async_std::main]
|
|
/// # async fn main() -> surf::Result<()> {
|
|
/// let req = surf::get("https://httpbin.org/get");
|
|
/// let client = surf::client();
|
|
/// let res = client.send(req).await?;
|
|
/// # Ok(()) }
|
|
/// ```
|
|
pub async fn send(&self, req: impl Into<Request>) -> Result<Response> {
|
|
let mut req: Request = req.into();
|
|
let http_client = self.http_client.clone();
|
|
let middleware = self.middleware.clone();
|
|
|
|
let mw_stack = match req.take_middleware() {
|
|
Some(req_mw) => {
|
|
let mut mw = Vec::with_capacity(middleware.len() + req_mw.len());
|
|
mw.extend(middleware.iter().cloned());
|
|
mw.extend(req_mw);
|
|
Arc::new(mw)
|
|
}
|
|
None => middleware,
|
|
};
|
|
|
|
let next = Next::new(&mw_stack, &|req, client| {
|
|
Box::pin(async move {
|
|
let req: http_types::Request = req.into();
|
|
client.http_client.send(req).await.map(Into::into)
|
|
})
|
|
});
|
|
|
|
let client = Self {
|
|
config: self.config.clone(),
|
|
http_client,
|
|
// Erase the middleware stack for the Client accessible from within middleware.
|
|
// This avoids gratuitous circular borrow & logic issues.
|
|
middleware: Arc::new(vec![]),
|
|
};
|
|
|
|
let res = next.run(req, client).await?;
|
|
Ok(Response::new(res.into()))
|
|
}
|
|
|
|
/// Submit a `Request` and get the response body as bytes.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```no_run
|
|
/// # #[async_std::main]
|
|
/// # async fn main() -> surf::Result<()> {
|
|
/// let req = surf::get("https://httpbin.org/get");
|
|
/// let bytes = surf::client().recv_bytes(req).await?;
|
|
/// assert!(bytes.len() > 0);
|
|
/// # Ok(()) }
|
|
/// ```
|
|
pub async fn recv_bytes(&self, req: impl Into<Request>) -> Result<Vec<u8>> {
|
|
let mut res = self.send(req.into()).await?;
|
|
res.body_bytes().await
|
|
}
|
|
|
|
/// Submit a `Request` and get the response body as a string.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```no_run
|
|
/// # #[async_std::main]
|
|
/// # async fn main() -> surf::Result<()> {
|
|
/// let req = surf::get("https://httpbin.org/get");
|
|
/// let string = surf::client().recv_string(req).await?;
|
|
/// assert!(string.len() > 0);
|
|
/// # Ok(()) }
|
|
/// ```
|
|
pub async fn recv_string(&self, req: impl Into<Request>) -> Result<String> {
|
|
let mut res = self.send(req.into()).await?;
|
|
res.body_string().await
|
|
}
|
|
|
|
/// Submit a `Request` and decode the response body from json into a struct.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```no_run
|
|
/// # use serde::{Deserialize, Serialize};
|
|
/// # #[async_std::main]
|
|
/// # async fn main() -> surf::Result<()> {
|
|
/// #[derive(Deserialize, Serialize)]
|
|
/// struct Ip {
|
|
/// ip: String
|
|
/// }
|
|
///
|
|
/// let req = surf::get("https://api.ipify.org?format=json");
|
|
/// let Ip { ip } = surf::client().recv_json(req).await?;
|
|
/// assert!(ip.len() > 10);
|
|
/// # Ok(()) }
|
|
/// ```
|
|
pub async fn recv_json<T: serde::de::DeserializeOwned>(
|
|
&self,
|
|
req: impl Into<Request>,
|
|
) -> Result<T> {
|
|
let mut res = self.send(req.into()).await?;
|
|
res.body_json::<T>().await
|
|
}
|
|
|
|
/// Submit a `Request` and decode the response body from form encoding into a struct.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Any I/O error encountered while reading the body is immediately returned
|
|
/// as an `Err`.
|
|
///
|
|
/// If the body cannot be interpreted as valid json for the target type `T`,
|
|
/// an `Err` is returned.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```no_run
|
|
/// # use serde::{Deserialize, Serialize};
|
|
/// # #[async_std::main]
|
|
/// # async fn main() -> surf::Result<()> {
|
|
/// #[derive(Deserialize, Serialize)]
|
|
/// struct Body {
|
|
/// apples: u32
|
|
/// }
|
|
///
|
|
/// let req = surf::get("https://api.example.com/v1/response");
|
|
/// let Body { apples } = surf::client().recv_form(req).await?;
|
|
/// # Ok(()) }
|
|
/// ```
|
|
pub async fn recv_form<T: serde::de::DeserializeOwned>(
|
|
&self,
|
|
req: impl Into<Request>,
|
|
) -> Result<T> {
|
|
let mut res = self.send(req.into()).await?;
|
|
res.body_form::<T>().await
|
|
}
|
|
|
|
/// Perform an HTTP `GET` request using the `Client` connection.
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// This will panic if a malformed URL is passed.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns errors from the middleware, http backend, and network sockets.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```no_run
|
|
/// # #[async_std::main]
|
|
/// # async fn main() -> surf::Result<()> {
|
|
/// let client = surf::client();
|
|
/// let string = client.get("https://httpbin.org/get").recv_string().await?;
|
|
/// # Ok(()) }
|
|
/// ```
|
|
pub fn get(&self, uri: impl AsRef<str>) -> RequestBuilder {
|
|
RequestBuilder::new(Method::Get, self.url(uri)).with_client(self.clone())
|
|
}
|
|
|
|
/// Perform an HTTP `HEAD` request using the `Client` connection.
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// This will panic if a malformed URL is passed.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns errors from the middleware, http backend, and network sockets.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```no_run
|
|
/// # #[async_std::main]
|
|
/// # async fn main() -> surf::Result<()> {
|
|
/// let client = surf::client();
|
|
/// let string = client.head("https://httpbin.org/head").recv_string().await?;
|
|
/// # Ok(()) }
|
|
/// ```
|
|
pub fn head(&self, uri: impl AsRef<str>) -> RequestBuilder {
|
|
RequestBuilder::new(Method::Head, self.url(uri)).with_client(self.clone())
|
|
}
|
|
|
|
/// Perform an HTTP `POST` request using the `Client` connection.
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// This will panic if a malformed URL is passed.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns errors from the middleware, http backend, and network sockets.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```no_run
|
|
/// # #[async_std::main]
|
|
/// # async fn main() -> surf::Result<()> {
|
|
/// let client = surf::client();
|
|
/// let string = client.post("https://httpbin.org/post").recv_string().await?;
|
|
/// # Ok(()) }
|
|
/// ```
|
|
pub fn post(&self, uri: impl AsRef<str>) -> RequestBuilder {
|
|
RequestBuilder::new(Method::Post, self.url(uri)).with_client(self.clone())
|
|
}
|
|
|
|
/// Perform an HTTP `PUT` request using the `Client` connection.
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// This will panic if a malformed URL is passed.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns errors from the middleware, http backend, and network sockets.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```no_run
|
|
/// # #[async_std::main]
|
|
/// # async fn main() -> surf::Result<()> {
|
|
/// let client = surf::client();
|
|
/// let string = client.put("https://httpbin.org/put").recv_string().await?;
|
|
/// # Ok(()) }
|
|
/// ```
|
|
pub fn put(&self, uri: impl AsRef<str>) -> RequestBuilder {
|
|
RequestBuilder::new(Method::Put, self.url(uri)).with_client(self.clone())
|
|
}
|
|
|
|
/// Perform an HTTP `DELETE` request using the `Client` connection.
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// This will panic if a malformed URL is passed.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns errors from the middleware, http backend, and network sockets.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```no_run
|
|
/// # #[async_std::main]
|
|
/// # async fn main() -> surf::Result<()> {
|
|
/// let client = surf::client();
|
|
/// let string = client.delete("https://httpbin.org/delete").recv_string().await?;
|
|
/// # Ok(()) }
|
|
/// ```
|
|
pub fn delete(&self, uri: impl AsRef<str>) -> RequestBuilder {
|
|
RequestBuilder::new(Method::Delete, self.url(uri)).with_client(self.clone())
|
|
}
|
|
|
|
/// Perform an HTTP `CONNECT` request using the `Client` connection.
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// This will panic if a malformed URL is passed.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns errors from the middleware, http backend, and network sockets.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```no_run
|
|
/// # #[async_std::main]
|
|
/// # async fn main() -> surf::Result<()> {
|
|
/// let client = surf::client();
|
|
/// let string = client.connect("https://httpbin.org/connect").recv_string().await?;
|
|
/// # Ok(()) }
|
|
/// ```
|
|
pub fn connect(&self, uri: impl AsRef<str>) -> RequestBuilder {
|
|
RequestBuilder::new(Method::Connect, self.url(uri)).with_client(self.clone())
|
|
}
|
|
|
|
/// Perform an HTTP `OPTIONS` request using the `Client` connection.
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// This will panic if a malformed URL is passed.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns errors from the middleware, http backend, and network sockets.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```no_run
|
|
/// # #[async_std::main]
|
|
/// # async fn main() -> surf::Result<()> {
|
|
/// let client = surf::client();
|
|
/// let string = client.options("https://httpbin.org/options").recv_string().await?;
|
|
/// # Ok(()) }
|
|
/// ```
|
|
pub fn options(&self, uri: impl AsRef<str>) -> RequestBuilder {
|
|
RequestBuilder::new(Method::Options, self.url(uri)).with_client(self.clone())
|
|
}
|
|
|
|
/// Perform an HTTP `TRACE` request using the `Client` connection.
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// This will panic if a malformed URL is passed.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns errors from the middleware, http backend, and network sockets.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```no_run
|
|
/// # #[async_std::main]
|
|
/// # async fn main() -> surf::Result<()> {
|
|
/// let client = surf::client();
|
|
/// let string = client.trace("https://httpbin.org/trace").recv_string().await?;
|
|
/// # Ok(()) }
|
|
/// ```
|
|
pub fn trace(&self, uri: impl AsRef<str>) -> RequestBuilder {
|
|
RequestBuilder::new(Method::Trace, self.url(uri)).with_client(self.clone())
|
|
}
|
|
|
|
/// Perform an HTTP `PATCH` request using the `Client` connection.
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// This will panic if a malformed URL is passed.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns errors from the middleware, http backend, and network sockets.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```no_run
|
|
/// # #[async_std::main]
|
|
/// # async fn main() -> surf::Result<()> {
|
|
/// let client = surf::client();
|
|
/// let string = client.patch("https://httpbin.org/patch").recv_string().await?;
|
|
/// # Ok(()) }
|
|
/// ```
|
|
pub fn patch(&self, uri: impl AsRef<str>) -> RequestBuilder {
|
|
RequestBuilder::new(Method::Patch, self.url(uri)).with_client(self.clone())
|
|
}
|
|
|
|
/// Perform a HTTP request with the given verb using the `Client` connection.
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// This will panic if a malformed URL is passed.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns errors from the middleware, http backend, and network sockets.
|
|
///
|
|
/// # Examples
|
|
/// ```no_run
|
|
/// # #[async_std::main]
|
|
/// # async fn main() -> surf::Result<()> {
|
|
/// use http_types::Method;
|
|
/// let client = surf::client();
|
|
/// let req = client.request(Method::Get, "http://httpbin.org/get");
|
|
/// let res = client.send(req).await?;
|
|
/// # Ok(()) }
|
|
/// ```
|
|
pub fn request(&self, verb: Method, uri: impl AsRef<str>) -> RequestBuilder {
|
|
RequestBuilder::new(verb, self.url(uri)).with_client(self.clone())
|
|
}
|
|
|
|
/// Sets the base URL for this client. All request URLs will be relative to this URL.
|
|
///
|
|
/// Note: a trailing slash is significant.
|
|
/// Without it, the last path component is considered to be a “file” name
|
|
/// to be removed to get at the “directory” that is used as the base.
|
|
///
|
|
/// # Examples
|
|
/// ```no_run
|
|
/// # use http_types::Url;
|
|
/// # fn main() -> http_types::Result<()> { async_std::task::block_on(async {
|
|
/// let mut client = surf::client();
|
|
/// client.set_base_url(Url::parse("http://example.com/api/v1/")?);
|
|
/// client.get("posts.json").recv_json().await?; /// http://example.com/api/v1/posts.json
|
|
/// # Ok(()) }) }
|
|
/// ```
|
|
#[deprecated(since = "6.5.0", note = "Please use `Config` instead")]
|
|
pub fn set_base_url(&mut self, base: Url) {
|
|
self.config.base_url = Some(base);
|
|
}
|
|
|
|
/// Get the current configuration.
|
|
pub fn config(&self) -> &Config {
|
|
&self.config
|
|
}
|
|
|
|
// private function to generate a url based on the base_path
|
|
fn url(&self, uri: impl AsRef<str>) -> Url {
|
|
match &self.config.base_url {
|
|
None => uri.as_ref().parse().unwrap(),
|
|
Some(base) => base.join(uri.as_ref()).unwrap(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TryFrom<Config> for Client {
|
|
#[cfg(feature = "default-client")]
|
|
type Error = <DefaultClient as TryFrom<http_client::Config>>::Error;
|
|
#[cfg(not(feature = "default-client"))]
|
|
type Error = std::convert::Infallible;
|
|
|
|
fn try_from(mut config: Config) -> std::result::Result<Self, Self::Error> {
|
|
let http_client = match config.http_client.take() {
|
|
Some(client) => client,
|
|
#[cfg(feature = "default-client")]
|
|
None => Arc::new(DefaultClient::try_from(config.http_config.clone())?),
|
|
#[cfg(not(feature = "default-client"))]
|
|
None => panic!("Config without an http client provided to Surf configured without a default client.")
|
|
};
|
|
|
|
Ok(Client {
|
|
config,
|
|
http_client,
|
|
middleware: Arc::new(vec![]),
|
|
})
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod client_tests {
|
|
use std::convert::TryInto;
|
|
|
|
use super::Client;
|
|
use super::Config;
|
|
use crate::Url;
|
|
|
|
#[test]
|
|
fn base_url() {
|
|
let base_url = Url::parse("http://example.com/api/v1/").unwrap();
|
|
|
|
let client: Client = Config::new().set_base_url(base_url).try_into().unwrap();
|
|
let url = client.url("posts.json");
|
|
assert_eq!(url.as_str(), "http://example.com/api/v1/posts.json");
|
|
}
|
|
}
|