use std::{error::Error, fmt, net, rc::Rc};
use base64::{engine::general_purpose::STANDARD as base64, Engine};
#[cfg(feature = "cookie")]
use coo_kie::{Cookie, CookieJar};
use serde::Serialize;
use crate::http::body::Body;
use crate::http::error::HttpError;
use crate::http::header::{self, HeaderMap, HeaderName, HeaderValue};
use crate::http::{ConnectionType, Method, RequestHead, RequestHeadType, Uri, Version};
use crate::{time::Millis, util::Bytes, util::Stream};
use super::error::{FreezeRequestError, InvalidUrl};
use super::sender::{PrepForSendingError, SendClientRequest};
use super::{frozen::FrozenClientRequest, ClientConfig};
pub struct ClientRequest {
pub(crate) head: Box<RequestHead>,
err: Option<HttpError>,
addr: Option<net::SocketAddr>,
#[cfg(feature = "cookie")]
cookies: Option<CookieJar>,
response_decompress: bool,
timeout: Millis,
config: Rc<ClientConfig>,
}
impl ClientRequest {
pub(super) fn new<U>(method: Method, uri: U, config: Rc<ClientConfig>) -> Self
where
Uri: TryFrom<U>,
<Uri as TryFrom<U>>::Error: Into<HttpError>,
{
ClientRequest {
config,
head: Box::new(RequestHead::default()),
err: None,
addr: None,
#[cfg(feature = "cookie")]
cookies: None,
timeout: Millis::ZERO,
response_decompress: true,
}
.method(method)
.uri(uri)
}
#[inline]
pub fn uri<U>(mut self, uri: U) -> Self
where
Uri: TryFrom<U>,
<Uri as TryFrom<U>>::Error: Into<HttpError>,
{
match Uri::try_from(uri) {
Ok(uri) => self.head.uri = uri,
Err(e) => self.err = Some(e.into()),
}
self
}
pub fn get_uri(&self) -> &Uri {
&self.head.uri
}
pub fn address(mut self, addr: net::SocketAddr) -> Self {
self.addr = Some(addr);
self
}
#[inline]
pub fn method(mut self, method: Method) -> Self {
self.head.method = method;
self
}
pub fn get_method(&self) -> &Method {
&self.head.method
}
#[doc(hidden)]
#[inline]
pub fn version(mut self, version: Version) -> Self {
self.head.version = version;
self
}
pub fn get_version(&self) -> &Version {
&self.head.version
}
#[inline]
pub fn headers(&self) -> &HeaderMap {
&self.head.headers
}
#[inline]
pub fn headers_mut(&mut self) -> &mut HeaderMap {
&mut self.head.headers
}
pub fn header<K, V>(mut self, key: K, value: V) -> Self
where
HeaderName: TryFrom<K>,
HeaderValue: TryFrom<V>,
<HeaderName as TryFrom<K>>::Error: Into<HttpError>,
<HeaderValue as TryFrom<V>>::Error: Into<HttpError>,
{
match HeaderName::try_from(key) {
Ok(key) => match HeaderValue::try_from(value) {
Ok(value) => self.head.headers.append(key, value),
Err(e) => self.err = Some(e.into()),
},
Err(e) => self.err = Some(e.into()),
}
self
}
pub fn set_header<K, V>(mut self, key: K, value: V) -> Self
where
HeaderName: TryFrom<K>,
HeaderValue: TryFrom<V>,
<HeaderName as TryFrom<K>>::Error: Into<HttpError>,
<HeaderValue as TryFrom<V>>::Error: Into<HttpError>,
{
match HeaderName::try_from(key) {
Ok(key) => match HeaderValue::try_from(value) {
Ok(value) => self.head.headers.insert(key, value),
Err(e) => self.err = Some(e.into()),
},
Err(e) => self.err = Some(e.into()),
}
self
}
pub fn set_header_if_none<K, V>(mut self, key: K, value: V) -> Self
where
HeaderName: TryFrom<K>,
HeaderValue: TryFrom<V>,
<HeaderName as TryFrom<K>>::Error: Into<HttpError>,
<HeaderValue as TryFrom<V>>::Error: Into<HttpError>,
{
match HeaderName::try_from(key) {
Ok(key) => {
if !self.head.headers.contains_key(&key) {
match HeaderValue::try_from(value) {
Ok(value) => self.head.headers.insert(key, value),
Err(e) => self.err = Some(e.into()),
}
}
}
Err(e) => self.err = Some(e.into()),
}
self
}
#[inline]
pub fn set_connection_type(mut self, ctype: ConnectionType) -> Self {
self.head.set_connection_type(ctype);
self
}
#[inline]
pub fn force_close(mut self) -> Self {
self.head.set_connection_type(ConnectionType::Close);
self
}
#[inline]
pub fn content_type<V>(mut self, value: V) -> Self
where
HeaderValue: TryFrom<V>,
<HeaderValue as TryFrom<V>>::Error: Into<HttpError>,
{
match HeaderValue::try_from(value) {
Ok(value) => self.head.headers.insert(header::CONTENT_TYPE, value),
Err(e) => self.err = Some(e.into()),
}
self
}
#[inline]
pub fn content_length(self, len: u64) -> Self {
self.header(header::CONTENT_LENGTH, len)
}
pub fn basic_auth<U>(self, username: U, password: Option<&str>) -> Self
where
U: fmt::Display,
{
let auth = match password {
Some(password) => format!("{username}:{password}"),
None => format!("{username}:"),
};
self.header(
header::AUTHORIZATION,
format!("Basic {}", base64.encode(auth)),
)
}
pub fn bearer_auth<T>(self, token: T) -> Self
where
T: fmt::Display,
{
self.header(header::AUTHORIZATION, format!("Bearer {token}"))
}
#[cfg(feature = "cookie")]
pub fn cookie<C>(mut self, cookie: C) -> Self
where
C: Into<Cookie<'static>>,
{
if self.cookies.is_none() {
let mut jar = CookieJar::new();
jar.add(cookie.into());
self.cookies = Some(jar)
} else {
self.cookies.as_mut().unwrap().add(cookie.into());
}
self
}
pub fn no_decompress(mut self) -> Self {
self.response_decompress = false;
self
}
pub fn timeout<T: Into<Millis>>(mut self, timeout: T) -> Self {
self.timeout = timeout.into();
self
}
pub fn if_true<F>(self, value: bool, f: F) -> Self
where
F: FnOnce(ClientRequest) -> ClientRequest,
{
if value {
f(self)
} else {
self
}
}
pub fn if_some<T, F>(self, value: Option<T>, f: F) -> Self
where
F: FnOnce(T, ClientRequest) -> ClientRequest,
{
if let Some(val) = value {
f(val, self)
} else {
self
}
}
pub fn query<T: Serialize>(
mut self,
query: &T,
) -> Result<Self, serde_urlencoded::ser::Error> {
let mut parts = self.head.uri.clone().into_parts();
if let Some(path_and_query) = parts.path_and_query {
let query = serde_urlencoded::to_string(query)?;
let path = path_and_query.path();
parts.path_and_query = format!("{path}?{query}").parse().ok();
match Uri::from_parts(parts) {
Ok(uri) => self.head.uri = uri,
Err(e) => self.err = Some(e.into()),
}
}
Ok(self)
}
pub fn freeze(self) -> Result<FrozenClientRequest, FreezeRequestError> {
let slf = match self.prep_for_sending() {
Ok(slf) => slf,
Err(e) => return Err(e.into()),
};
let request = FrozenClientRequest {
head: Rc::new(*slf.head),
addr: slf.addr,
response_decompress: slf.response_decompress,
timeout: slf.timeout,
config: slf.config,
};
Ok(request)
}
pub fn send_body<B>(self, body: B) -> SendClientRequest
where
B: Into<Body>,
{
let slf = match self.prep_for_sending() {
Ok(slf) => slf,
Err(e) => return e.into(),
};
RequestHeadType::Owned(slf.head).send_body(
slf.addr,
slf.response_decompress,
slf.timeout,
slf.config,
body,
)
}
pub fn send_json<T: Serialize>(self, value: &T) -> SendClientRequest {
let slf = match self.prep_for_sending() {
Ok(slf) => slf,
Err(e) => return e.into(),
};
RequestHeadType::Owned(slf.head).send_json(
slf.addr,
slf.response_decompress,
slf.timeout,
slf.config,
value,
)
}
pub fn send_form<T: Serialize>(self, value: &T) -> SendClientRequest {
let slf = match self.prep_for_sending() {
Ok(slf) => slf,
Err(e) => return e.into(),
};
RequestHeadType::Owned(slf.head).send_form(
slf.addr,
slf.response_decompress,
slf.timeout,
slf.config,
value,
)
}
pub fn send_stream<S, E>(self, stream: S) -> SendClientRequest
where
S: Stream<Item = Result<Bytes, E>> + Unpin + 'static,
E: Error + 'static,
{
let slf = match self.prep_for_sending() {
Ok(slf) => slf,
Err(e) => return e.into(),
};
RequestHeadType::Owned(slf.head).send_stream(
slf.addr,
slf.response_decompress,
slf.timeout,
slf.config,
stream,
)
}
pub fn send(self) -> SendClientRequest {
let slf = match self.prep_for_sending() {
Ok(slf) => slf,
Err(e) => return e.into(),
};
RequestHeadType::Owned(slf.head).send(
slf.addr,
slf.response_decompress,
slf.timeout,
slf.config,
)
}
#[allow(unused_mut)]
fn prep_for_sending(mut self) -> Result<Self, PrepForSendingError> {
if let Some(e) = self.err {
return Err(e.into());
}
let uri = &self.head.uri;
if uri.host().is_none() {
return Err(InvalidUrl::MissingHost.into());
} else if uri.scheme().is_none() {
return Err(InvalidUrl::MissingScheme.into());
} else if let Some(scheme) = uri.scheme() {
match scheme.as_str() {
"http" | "ws" | "https" | "wss" => (),
_ => return Err(InvalidUrl::UnknownScheme.into()),
}
} else {
return Err(InvalidUrl::UnknownScheme.into());
}
#[cfg(feature = "cookie")]
{
use percent_encoding::percent_encode;
use std::fmt::Write as FmtWrite;
if let Some(ref mut jar) = self.cookies {
let mut cookie = String::new();
for c in jar.delta() {
let name =
percent_encode(c.name().as_bytes(), crate::http::helpers::USERINFO);
let value = percent_encode(
c.value().as_bytes(),
crate::http::helpers::USERINFO,
);
let _ = write!(cookie, "; {name}={value}");
}
self.head.headers.insert(
header::COOKIE,
HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(),
);
}
}
let mut slf = self;
#[cfg(feature = "compress")]
if slf.response_decompress {
slf = slf.set_header_if_none(header::ACCEPT_ENCODING, "gzip, deflate");
}
Ok(slf)
}
}
impl fmt::Debug for ClientRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(
f,
"\nClientRequest {:?} {}:{}",
self.head.version, self.head.method, self.head.uri
)?;
writeln!(f, " headers:")?;
for (key, val) in self.head.headers.iter() {
if key == header::AUTHORIZATION {
writeln!(f, " {key:?}: <REDACTED>")?;
} else {
writeln!(f, " {key:?}: {val:?}")?;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::http::client::Client;
#[crate::rt_test]
async fn test_debug() {
let request = Client::new().get("/").header("x-test", "111");
let repr = format!("{request:?}");
assert!(repr.contains("ClientRequest"));
assert!(repr.contains("x-test"));
}
#[crate::rt_test]
#[allow(clippy::let_underscore_future)]
async fn test_basics() {
let mut req = Client::new()
.put("/")
.version(Version::HTTP_2)
.header(header::DATE, "data")
.content_type("plain/text")
.if_true(true, |req| req.header(header::SERVER, "awc"))
.if_true(false, |req| req.header(header::EXPECT, "awc"))
.if_some(Some("server"), |val, req| {
req.header(header::USER_AGENT, val)
})
.if_some(Option::<&str>::None, |_, req| {
req.header(header::ALLOW, "1")
})
.content_length(100);
assert!(req.headers().contains_key(header::CONTENT_TYPE));
assert!(req.headers().contains_key(header::DATE));
assert!(req.headers().contains_key(header::SERVER));
assert!(req.headers().contains_key(header::USER_AGENT));
assert!(!req.headers().contains_key(header::ALLOW));
assert!(!req.headers().contains_key(header::EXPECT));
assert_eq!(req.head.version, Version::HTTP_2);
assert_eq!(req.get_version(), &Version::HTTP_2);
assert_eq!(req.get_method(), Method::PUT);
let _ = req.headers_mut();
let _ = req.send_body("");
}
#[crate::rt_test]
async fn test_client_header() {
let req = Client::build()
.header(header::CONTENT_TYPE, "111")
.finish()
.get("/");
assert_eq!(
req.head
.headers
.get(header::CONTENT_TYPE)
.unwrap()
.to_str()
.unwrap(),
"111"
);
}
#[crate::rt_test]
async fn test_client_header_override() {
let req = Client::build()
.header(header::CONTENT_TYPE, "111")
.finish()
.get("/")
.set_header(header::CONTENT_TYPE, "222");
assert_eq!(
req.head
.headers
.get(header::CONTENT_TYPE)
.unwrap()
.to_str()
.unwrap(),
"222"
);
}
#[crate::rt_test]
async fn client_basic_auth() {
let req = Client::new()
.get("/")
.basic_auth("username", Some("password"));
assert_eq!(
req.head
.headers
.get(header::AUTHORIZATION)
.unwrap()
.to_str()
.unwrap(),
"Basic dXNlcm5hbWU6cGFzc3dvcmQ="
);
let req = Client::new().get("/").basic_auth("username", None);
assert_eq!(
req.head
.headers
.get(header::AUTHORIZATION)
.unwrap()
.to_str()
.unwrap(),
"Basic dXNlcm5hbWU6"
);
}
#[crate::rt_test]
async fn client_bearer_auth() {
let req = Client::new().get("/").bearer_auth("someS3cr3tAutht0k3n");
assert_eq!(
req.head
.headers
.get(header::AUTHORIZATION)
.unwrap()
.to_str()
.unwrap(),
"Bearer someS3cr3tAutht0k3n"
);
}
#[crate::rt_test]
async fn client_query() {
let req = Client::new()
.get("/")
.query(&[("key1", "val1"), ("key2", "val2")])
.unwrap();
assert_eq!(req.get_uri().query().unwrap(), "key1=val1&key2=val2");
}
}