use crate::{
Body,
client::Client,
error::Error,
response::Response,
url::{IntoUrl, Url},
util::{narrow_latin1, widen_latin1},
};
use std::time::Duration;
fn _assert_send_future(rb: RequestBuilder) {
fn require_send<T: Send>(_t: &T) {}
let fut = rb.send();
require_send(&fut);
}
pub struct Request {
method: http::Method,
url: Url,
headers: http::HeaderMap,
body: Option<Body>,
timeout: Option<Duration>,
version: http::Version,
}
impl std::fmt::Debug for Request {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Request")
.field("method", &self.method)
.field("url", &self.url.as_str())
.finish()
}
}
impl Request {
pub fn new(method: http::Method, url: Url) -> Self {
Self {
method,
url,
headers: http::HeaderMap::new(),
body: None,
timeout: None,
version: http::Version::default(),
}
}
pub fn method(&self) -> &http::Method {
&self.method
}
pub fn method_mut(&mut self) -> &mut http::Method {
&mut self.method
}
pub fn url(&self) -> &Url {
&self.url
}
pub fn url_mut(&mut self) -> &mut Url {
&mut self.url
}
pub fn headers(&self) -> &http::HeaderMap {
&self.headers
}
pub fn headers_mut(&mut self) -> &mut http::HeaderMap {
&mut self.headers
}
pub fn body(&self) -> Option<&Body> {
self.body.as_ref()
}
pub fn body_mut(&mut self) -> &mut Option<Body> {
&mut self.body
}
pub fn timeout(&self) -> Option<&Duration> {
self.timeout.as_ref()
}
pub fn timeout_mut(&mut self) -> &mut Option<Duration> {
&mut self.timeout
}
pub fn version(&self) -> http::Version {
self.version
}
pub fn version_mut(&mut self) -> &mut http::Version {
&mut self.version
}
pub fn try_clone(&self) -> Option<Request> {
let body = match &self.body {
Some(b) => Some(b.try_clone()?),
None => None,
};
Some(Request {
method: self.method.clone(),
url: self.url.clone(),
headers: self.headers.clone(),
body,
timeout: self.timeout,
version: self.version,
})
}
pub(crate) fn into_parts(
self,
) -> (http::Method, Url, http::HeaderMap, Option<Body>, Option<Duration>) {
(self.method, self.url, self.headers, self.body, self.timeout)
}
}
impl<T: Into<Body>> TryFrom<http::Request<T>> for Request {
type Error = Error;
fn try_from(req: http::Request<T>) -> Result<Self, Self::Error> {
let (parts, body) = req.into_parts();
let url = Url::from_http_uri(&parts.uri).map_err(Error::builder)?;
Ok(Request {
method: parts.method,
url,
headers: parts.headers,
body: Some(body.into()),
timeout: None,
version: parts.version,
})
}
}
impl TryFrom<Request> for http::Request<Body> {
type Error = Error;
fn try_from(req: Request) -> Result<Self, Self::Error> {
let uri = req.url.to_http_uri().map_err(Error::builder)?;
let mut out = http::Request::builder()
.method(req.method)
.uri(uri)
.version(req.version)
.body(req.body.unwrap_or_default())
.map_err(Error::builder)?;
*out.headers_mut() = req.headers;
Ok(out)
}
}
pub struct RequestBuilder {
client: Client,
method: String,
url: Result<Url, Error>,
headers: Vec<(String, String)>,
body: Option<Body>,
timeout: Option<Duration>,
}
impl std::fmt::Debug for RequestBuilder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let url_str = match &self.url {
Ok(u) => u.as_str().to_owned(),
Err(_) => "<invalid>".to_owned(),
};
f.debug_struct("RequestBuilder")
.field("method", &self.method)
.field("url", &url_str)
.finish()
}
}
impl RequestBuilder {
pub(crate) fn new(client: Client, method: &str, url: impl IntoUrl) -> Self {
Self {
client,
method: method.to_owned(),
url: url.into_url(),
headers: Vec::new(),
body: None,
timeout: None,
}
}
#[must_use]
pub fn header<K, V>(mut self, key: K, value: V) -> Self
where
http::HeaderName: TryFrom<K>,
<http::HeaderName as TryFrom<K>>::Error: Into<http::Error>,
http::HeaderValue: TryFrom<V>,
<http::HeaderValue as TryFrom<V>>::Error: Into<http::Error>,
{
let name = match http::HeaderName::try_from(key) {
Ok(n) => n,
Err(e) => {
let e: http::Error = e.into();
self.url = Err(Error::builder(e));
return self;
}
};
let value = match http::HeaderValue::try_from(value) {
Ok(v) => v,
Err(e) => {
let e: http::Error = e.into();
self.url = Err(Error::builder(e));
return self;
}
};
self.headers
.push((name.as_str().to_owned(), widen_latin1(value.as_bytes())));
self
}
#[cfg(feature = "json")]
#[must_use]
pub fn json<T: serde::Serialize + ?Sized>(mut self, body: &T) -> Self {
match serde_json::to_vec(body) {
Ok(data) => {
self.headers
.push(("Content-Type".to_owned(), "application/json".to_owned()));
self.body = Some(Body::from(data));
}
Err(e) => {
self.url = Err(Error::builder(crate::error::ContextError::new(
"JSON serialization failed",
e,
)));
}
}
self
}
#[must_use]
pub fn body<B: Into<Body>>(mut self, body: B) -> Self {
self.body = Some(body.into());
self
}
#[must_use]
pub fn headers(mut self, headers: http::HeaderMap) -> Self {
for (name, value) in &headers {
self.headers
.push((name.as_str().to_owned(), widen_latin1(value.as_bytes())));
}
self
}
#[must_use]
pub fn bearer_auth<T: std::fmt::Display>(self, token: T) -> Self {
let value = format!("Bearer {token}");
self.header(http::header::AUTHORIZATION, value)
}
#[must_use]
pub fn basic_auth<U, P>(self, username: U, password: Option<P>) -> Self
where
U: std::fmt::Display,
P: std::fmt::Display,
{
use base64::Engine as _;
let credentials = match password {
Some(p) => format!("{username}:{p}"),
None => format!("{username}:"),
};
let encoded = base64::engine::general_purpose::STANDARD.encode(credentials);
let value = format!("Basic {encoded}");
self.header(http::header::AUTHORIZATION, value)
}
#[cfg(feature = "query")]
#[must_use]
pub fn query<T: serde::Serialize + ?Sized>(mut self, query: &T) -> Self {
let query_str = match serialize_form_urlencoded(query) {
Ok(s) => s,
Err(e) => {
self.url = Err(e);
return self;
}
};
if let Ok(ref mut url) = self.url {
let new_query = match &url.query {
Some(existing) => format!("{existing}&{query_str}"),
None => query_str,
};
url.set_query_string(new_query);
}
self
}
#[cfg(feature = "form")]
#[must_use]
pub fn form<T: serde::Serialize + ?Sized>(mut self, form: &T) -> Self {
match serialize_form_urlencoded(form) {
Ok(encoded) => {
self.headers.push((
"Content-Type".to_owned(),
"application/x-www-form-urlencoded".to_owned(),
));
self.body = Some(Body::from(encoded.into_bytes()));
}
Err(e) => {
self.url = Err(e);
}
}
self
}
#[must_use]
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
#[cfg(feature = "noop-compat")]
#[must_use]
pub fn version(self, _version: http::Version) -> Self {
self
}
pub fn build(self) -> Result<Request, Error> {
let url = self.url?;
let mut header_map = http::HeaderMap::new();
let per_request_names: std::collections::HashSet<&str> =
self.headers.iter().map(|(name, _)| name.as_str()).collect();
for (name, value) in &self.client.inner.default_headers {
if !per_request_names.contains(name.as_str()) {
header_map.append(name.clone(), value.clone());
}
}
for (name, value) in &self.headers {
let header_name =
http::header::HeaderName::from_bytes(name.as_bytes()).map_err(Error::builder)?;
let header_value = http::header::HeaderValue::from_bytes(&narrow_latin1(value))
.map_err(Error::builder)?;
header_map.append(header_name, header_value);
}
if !header_map.contains_key(http::header::ACCEPT) {
header_map.insert(http::header::ACCEPT, http::HeaderValue::from_static("*/*"));
}
if !url.username.is_empty() && !header_map.contains_key(http::header::AUTHORIZATION) {
let credentials = match &url.password {
Some(pass) => format!("{}:{pass}", url.username),
None => format!("{}:", url.username),
};
use base64::Engine;
let encoded = base64::engine::general_purpose::STANDARD.encode(credentials);
if let Ok(val) = http::HeaderValue::from_str(&format!("Basic {encoded}")) {
header_map.insert(http::header::AUTHORIZATION, val);
trace!("injected Basic auth from URL userinfo");
}
}
let method = http::Method::from_bytes(self.method.as_bytes()).map_err(Error::builder)?;
Ok(Request {
method,
url,
headers: header_map,
body: self.body,
timeout: self.timeout,
version: http::Version::default(),
})
}
pub fn build_split(self) -> (Client, Result<Request, Error>) {
let client = self.client.clone();
let result = self.build();
(client, result)
}
pub fn from_parts(client: Client, request: Request) -> Self {
Self {
client,
method: request.method.as_str().to_owned(),
url: Ok(request.url),
headers: request
.headers
.iter()
.map(|(k, v)| (k.as_str().to_owned(), widen_latin1(v.as_bytes())))
.collect(),
body: request.body,
timeout: request.timeout,
}
}
pub fn try_clone(&self) -> Option<RequestBuilder> {
let url = match &self.url {
Ok(u) => Ok(u.clone()),
Err(_) => return None,
};
let body = match &self.body {
Some(b) => Some(b.try_clone()?),
None => None,
};
Some(RequestBuilder {
client: self.client.clone(),
method: self.method.clone(),
url,
headers: self.headers.clone(),
body,
timeout: self.timeout,
})
}
pub async fn send(self) -> Result<Response, Error> {
let (client, result) = self.build_split();
client.execute(result?).await
}
}
#[cfg(any(feature = "query", feature = "form"))]
fn serialize_form_urlencoded<T: serde::Serialize + ?Sized>(value: &T) -> Result<String, Error> {
let json = serde_json::to_value(value).map_err(|e| {
Error::builder(crate::error::ContextError::new("form serialization failed", e))
})?;
let mut ser = form_urlencoded::Serializer::new(String::new());
match json {
serde_json::Value::Object(map) => {
for (k, v) in &map {
if let Some(s) = json_value_to_str(v) {
ser.append_pair(k, &s);
}
}
}
serde_json::Value::Array(arr) => {
for item in &arr {
let pair = item.as_array().filter(|a| a.len() == 2).ok_or_else(|| {
Error::builder(
"form serialization failed: \
sequence items must be [key, value] pairs",
)
})?;
let k = pair
.first()
.and_then(json_value_to_str)
.ok_or_else(|| Error::builder("form serialization failed: null key"))?;
let v = pair.get(1).and_then(json_value_to_str).unwrap_or_default();
ser.append_pair(&k, &v);
}
}
_ => {
return Err(Error::builder(
"form serialization failed: \
value must be a struct, map, or sequence of pairs",
));
}
}
Ok(ser.finish())
}
#[cfg(any(feature = "query", feature = "form"))]
fn json_value_to_str(v: &serde_json::Value) -> Option<String> {
match v {
serde_json::Value::String(s) => Some(s.clone()),
serde_json::Value::Number(n) => Some(n.to_string()),
serde_json::Value::Bool(b) => Some(b.to_string()),
serde_json::Value::Null => None,
other => Some(other.to_string()),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn bare_client() -> Client {
Client::builder().build().expect("client build")
}
#[test]
fn body_from_table() {
let cases: &[(&[u8], &str)] =
&[(&[1u8, 2, 3], "raw bytes"), (b"hello world", "string bytes")];
for &(input, label) in cases {
let rb = bare_client()
.post("https://example.com")
.body(input.to_vec());
let clone = rb.try_clone().unwrap();
assert_eq!(clone.body.unwrap().as_bytes().unwrap(), input, "{label}");
}
}
#[test]
fn headers_merge() {
let mut map = http::HeaderMap::new();
map.insert("x-one", "1".parse().unwrap());
map.insert("x-two", "2".parse().unwrap());
let rb = bare_client().get("https://example.com").headers(map);
let clone = rb.try_clone().unwrap();
assert!(clone.headers.iter().any(|(k, v)| k == "x-one" && v == "1"));
assert!(clone.headers.iter().any(|(k, v)| k == "x-two" && v == "2"));
}
#[test]
fn headers_preserve_extended_bytes() {
let mut map = http::HeaderMap::new();
let value = http::HeaderValue::from_bytes(&[0x80, b'A', 0xFF]).expect("value");
map.insert("x-raw", value.clone());
let req = bare_client()
.get("https://example.com")
.headers(map)
.build()
.expect("build should succeed");
let got = req.headers().get("x-raw").expect("header should exist");
assert_eq!(got.as_bytes(), value.as_bytes());
}
#[test]
fn bearer_auth_sets_header() {
let rb = bare_client()
.get("https://example.com")
.bearer_auth("my-token-123");
let clone = rb.try_clone().unwrap();
let auth = clone
.headers
.iter()
.find(|(k, _)| k == "authorization")
.map(|(_, v)| v.clone());
assert_eq!(auth, Some("Bearer my-token-123".to_owned()));
}
#[test]
fn basic_auth_table() {
use base64::Engine as _;
let cases: &[(&str, Option<&str>, &str, &str)] = &[
("user", Some("pass"), "user:pass", "with password"),
("user", None, "user:", "without password"),
];
for &(username, password, creds, label) in cases {
let rb = bare_client()
.get("https://example.com")
.basic_auth(username, password);
let clone = rb.try_clone().unwrap();
let auth = clone
.headers
.iter()
.find(|(k, _)| k == "authorization")
.map(|(_, v)| v.clone())
.unwrap();
let expected =
format!("Basic {}", base64::engine::general_purpose::STANDARD.encode(creds));
assert_eq!(auth, expected, "{label}");
}
}
#[cfg(feature = "query")]
#[test]
fn query_appends_params() {
let rb = bare_client()
.get("https://example.com/api")
.query(&[("key", "val"), ("a", "b")]);
let clone = rb.try_clone().unwrap();
let url = clone.url.unwrap();
assert_eq!(url.query(), Some("key=val&a=b"));
}
#[cfg(feature = "query")]
#[test]
fn query_called_twice_appends() {
let rb = bare_client()
.get("https://example.com/api")
.query(&[("key", "val")])
.query(&[("a", "b")]);
let clone = rb.try_clone().unwrap();
let url = clone.url.unwrap();
assert_eq!(url.query(), Some("key=val&a=b"));
}
#[cfg(feature = "query")]
#[test]
fn query_with_existing_query() {
let rb = bare_client()
.get("https://example.com/api?existing=1")
.query(&[("added", "2")]);
let clone = rb.try_clone().unwrap();
let url = clone.url.unwrap();
assert_eq!(url.query(), Some("existing=1&added=2"));
}
#[cfg(feature = "form")]
#[test]
fn form_sets_body_and_content_type() {
let rb = bare_client()
.post("https://example.com/login")
.form(&[("user", "admin"), ("pass", "secret")]);
let clone = rb.try_clone().unwrap();
assert!(
clone
.headers
.iter()
.any(|(k, v)| k.eq_ignore_ascii_case("content-type")
&& v == "application/x-www-form-urlencoded")
);
let body = String::from_utf8(clone.body.unwrap().as_bytes().unwrap().to_vec()).unwrap();
assert!(body.contains("user=admin"));
assert!(body.contains("pass=secret"));
}
#[test]
fn per_request_timeout() {
let rb = bare_client()
.get("https://example.com")
.timeout(Duration::from_secs(5));
assert_eq!(rb.timeout, Some(Duration::from_secs(5)));
}
#[test]
#[cfg(feature = "noop-compat")]
fn version_accepted_as_noop() {
let _rb = bare_client()
.get("https://example.com")
.version(http::Version::HTTP_11);
}
#[test]
fn try_clone_succeeds() {
let rb = bare_client()
.post("https://example.com")
.header("x-test", "value")
.body(b"data".to_vec())
.timeout(Duration::from_secs(3));
let clone = rb.try_clone().unwrap();
assert_eq!(clone.method, "POST");
assert_eq!(clone.url.as_ref().unwrap().as_str(), "https://example.com/");
assert!(
clone
.headers
.iter()
.any(|(k, v)| k == "x-test" && v == "value")
);
assert_eq!(clone.body.as_ref().unwrap().as_bytes().unwrap(), b"data");
assert_eq!(clone.timeout, Some(Duration::from_secs(3)));
}
#[test]
fn try_clone_returns_none_on_error() {
let rb = bare_client().get("not-a-url");
assert!(rb.try_clone().is_none());
}
#[test]
fn request_version_default() {
let req = Request::new(http::Method::GET, "https://example.com".parse().unwrap());
let _v = req.version();
}
#[test]
fn request_version_mut() {
let mut req = Request::new(http::Method::GET, "https://example.com".parse().unwrap());
*req.version_mut() = http::Version::HTTP_2;
assert_eq!(req.version(), http::Version::HTTP_2);
}
#[test]
fn build_split_returns_client_and_request() {
let rb = bare_client().get("https://example.com");
let (client, result) = rb.build_split();
assert!(result.is_ok());
let req = result.unwrap();
assert_eq!(req.url().as_str(), "https://example.com/");
let _rb2 = client.get("https://other.com");
}
#[test]
fn build_split_preserves_error() {
let rb = bare_client().get("not-a-url");
let (_client, result) = rb.build_split();
assert!(result.is_err());
}
#[test]
fn from_parts_round_trips() {
let client = bare_client();
let req = client
.post("https://example.com/api")
.header("x-test", "val")
.body(b"payload".to_vec())
.timeout(Duration::from_secs(5))
.build()
.unwrap();
let rb = RequestBuilder::from_parts(client, req);
let rebuilt = rb.build().unwrap();
assert_eq!(rebuilt.method(), http::Method::POST);
assert_eq!(rebuilt.url().as_str(), "https://example.com/api");
assert!(rebuilt.headers().contains_key("x-test"));
assert!(rebuilt.body().is_some());
assert_eq!(rebuilt.timeout(), Some(&Duration::from_secs(5)));
}
#[test]
fn per_request_header_overrides_default() {
let mut defaults = http::HeaderMap::new();
defaults.insert("x-custom", "default-value".parse().unwrap());
let client = Client::builder().default_headers(defaults).build().unwrap();
let req = client
.get("https://example.com")
.header("x-custom", "override-value")
.build()
.unwrap();
let values: Vec<_> = req
.headers()
.get_all("x-custom")
.iter()
.map(|v| v.to_str().unwrap().to_owned())
.collect();
assert_eq!(values, vec!["override-value"]);
}
#[test]
fn default_header_kept_when_not_overridden() {
let mut defaults = http::HeaderMap::new();
defaults.insert("x-default", "kept".parse().unwrap());
let client = Client::builder().default_headers(defaults).build().unwrap();
let req = client
.get("https://example.com")
.header("x-other", "value")
.build()
.unwrap();
assert_eq!(req.headers().get("x-default").unwrap().to_str().unwrap(), "kept");
assert_eq!(req.headers().get("x-other").unwrap().to_str().unwrap(), "value");
}
#[test]
fn header_invalid_deferred_error_table() {
let cases: &[(&str, &str, &str)] = &[
("invalid header name!", "value", "invalid name"),
("x-ok", "value\0with-null", "invalid value"),
];
for &(name, value, label) in cases {
let result = bare_client()
.get("https://example.com")
.header(name, value)
.build();
let err = result.expect_err(&format!("{label}: should fail"));
assert!(err.is_builder(), "{label}: should be builder error");
}
}
#[test]
fn request_accessors_mut() {
let mut req = Request::new(http::Method::GET, "https://example.com".parse().unwrap());
*req.method_mut() = http::Method::POST;
assert_eq!(req.method(), &http::Method::POST);
let new_url: Url = "https://other.com".parse().unwrap();
*req.url_mut() = new_url;
assert_eq!(req.url().host_str(), Some("other.com"));
req.headers_mut()
.insert("x-custom", "value".parse().unwrap());
assert_eq!(req.headers().get("x-custom").unwrap().to_str().unwrap(), "value");
assert!(req.body().is_none());
*req.body_mut() = Some(Body::from("payload"));
assert!(req.body().is_some());
assert_eq!(req.body().unwrap().as_bytes().unwrap(), b"payload");
assert!(req.timeout().is_none());
*req.timeout_mut() = Some(Duration::from_secs(5));
assert_eq!(req.timeout(), Some(&Duration::from_secs(5)));
}
#[test]
fn request_try_clone_table() {
let cases: Vec<(Option<Body>, bool, &str)> = vec![
(None, true, "no body"),
(Some(Body::from("payload")), true, "bytes body"),
];
for (body, _, label) in cases {
let mut req = Request::new(http::Method::POST, "https://example.com".parse().unwrap());
req.body = body;
*req.timeout_mut() = Some(Duration::from_secs(7));
let cloned = req
.try_clone()
.unwrap_or_else(|| panic!("{label}: should clone"));
assert_eq!(cloned.method(), &http::Method::POST, "{label}");
assert_eq!(cloned.timeout(), Some(&Duration::from_secs(7)), "{label}");
assert_eq!(
cloned.body().and_then(|b| b.as_bytes()).map(|b| b.to_vec()),
cloned.body().and_then(|b| b.as_bytes()).map(|b| b.to_vec()),
"{label}: body"
);
}
let stream = futures_util::stream::once(async {
Ok::<_, Error>(bytes::Bytes::from_static(b"data"))
});
let mut req = Request::new(http::Method::POST, "https://example.com".parse().unwrap());
req.body = Some(Body::wrap_stream(stream));
assert!(req.try_clone().is_none(), "stream body cannot be cloned");
}
#[test]
fn request_builder_debug_shows_invalid_on_error() {
let rb = bare_client().get("not-a-url");
let debug = format!("{rb:?}");
assert!(debug.contains("<invalid>"), "got: {debug}");
}
#[cfg(feature = "form")]
#[test]
fn form_non_struct_value_error() {
let rb = bare_client()
.post("https://example.com")
.form(&"plain string");
let result = rb.build();
assert!(result.is_err(), "plain string should fail form()");
}
#[cfg(feature = "form")]
#[test]
fn form_array_bad_pair_error() {
let bad: Vec<Vec<&str>> = vec![vec!["only-one"]];
let rb = bare_client().post("https://example.com").form(&bad);
let result = rb.build();
assert!(result.is_err(), "non-pair array should fail form()");
}
#[cfg(feature = "json")]
#[test]
fn json_serialization_failure() {
struct FailSerialize;
impl serde::Serialize for FailSerialize {
fn serialize<S: serde::Serializer>(&self, _: S) -> Result<S::Ok, S::Error> {
Err(serde::ser::Error::custom("intentional failure"))
}
}
let rb = bare_client()
.post("https://example.com")
.json(&FailSerialize);
let result = rb.build();
assert!(result.is_err(), "FailSerialize should fail json()");
assert!(result.unwrap_err().is_builder());
}
#[cfg(feature = "query")]
#[test]
fn query_errors() {
let err = bare_client()
.get("https://example.com")
.query(&"plain string")
.build()
.unwrap_err();
assert!(err.is_builder(), "non-struct value should be a builder error");
let err = bare_client()
.get("not a valid url")
.query(&[("key", "val")])
.build()
.unwrap_err();
assert!(err.is_builder(), "errored URL should still be a builder error");
}
#[test]
fn repeated_header_accumulates() {
let req = bare_client()
.get("https://example.com")
.header("x-multi", "a")
.header("x-multi", "b")
.build()
.unwrap();
let values: Vec<_> = req
.headers()
.get_all("x-multi")
.iter()
.map(|v| v.to_str().unwrap().to_owned())
.collect();
assert_eq!(values, vec!["a", "b"]);
}
#[test]
fn default_accept_header_injected() {
let req = bare_client().get("https://example.com").build().unwrap();
assert_eq!(
req.headers()
.get(http::header::ACCEPT)
.unwrap()
.to_str()
.unwrap(),
"*/*",
);
}
#[test]
fn explicit_accept_header_not_overwritten() {
let req = bare_client()
.get("https://example.com")
.header(http::header::ACCEPT, "application/json")
.build()
.unwrap();
let values: Vec<_> = req
.headers()
.get_all(http::header::ACCEPT)
.iter()
.map(|v| v.to_str().unwrap().to_owned())
.collect();
assert_eq!(values, vec!["application/json"]);
}
#[test]
fn default_accept_in_default_headers_not_doubled() {
let mut defaults = http::HeaderMap::new();
defaults.insert(http::header::ACCEPT, "text/html".parse().unwrap());
let client = Client::builder().default_headers(defaults).build().unwrap();
let req = client.get("https://example.com").build().unwrap();
let values: Vec<_> = req
.headers()
.get_all(http::header::ACCEPT)
.iter()
.map(|v| v.to_str().unwrap().to_owned())
.collect();
assert_eq!(values, vec!["text/html"]);
}
#[test]
fn build_invalid_method_error() {
let rb = RequestBuilder {
client: bare_client(),
method: "INVALID METHOD WITH SPACES".to_owned(),
url: Ok("https://example.com".parse().unwrap()),
headers: Vec::new(),
body: None,
timeout: None,
};
let result = rb.build();
assert!(result.is_err());
assert!(result.unwrap_err().is_builder());
}
#[test]
fn build_url_userinfo_table() {
#[cfg(feature = "tracing")]
let _guard = ::tracing::subscriber::set_default(crate::tracing::SinkSubscriber);
let cases: &[(&str, &str, Option<&str>, Option<&str>)] = &[
(
"user:pass injects Basic auth",
"https://alice:s3cret@example.com/api",
None,
Some("Basic YWxpY2U6czNjcmV0"),
),
(
"explicit auth overrides userinfo",
"https://alice:s3cret@example.com/api",
Some("Bearer tok123"),
Some("Bearer tok123"),
),
(
"username only → user: base64",
"https://bob@example.com/",
None,
Some("Basic Ym9iOg=="),
),
("no userinfo → no auth", "https://example.com/api", None, None),
];
let client = bare_client();
for &(label, url, explicit, expected) in cases {
let mut rb = client.get(url);
if let Some(hdr) = explicit {
rb = rb.header("authorization", hdr);
}
let req = rb.build().unwrap();
let auth = req
.headers()
.get(http::header::AUTHORIZATION)
.map(|v| v.to_str().unwrap());
assert_eq!(auth, expected, "{label}");
}
}
#[cfg(any(feature = "query", feature = "form"))]
#[test]
fn json_value_to_str_arms() {
let cases: &[(serde_json::Value, Option<&str>, &str)] = &[
(serde_json::Value::String("hello".into()), Some("hello"), "String"),
(serde_json::json!(42), Some("42"), "Number"),
(serde_json::json!(true), Some("true"), "Bool true"),
(serde_json::json!(false), Some("false"), "Bool false"),
(serde_json::Value::Null, None, "Null"),
(serde_json::json!([1, 2]), Some("[1,2]"), "Array"),
(serde_json::json!({"a": 1}), Some("{\"a\":1}"), "Object"),
];
for (value, expected, label) in cases {
let result = json_value_to_str(value);
assert_eq!(result.as_deref(), *expected, "{label}");
}
}
#[cfg(feature = "form")]
#[test]
fn form_skips_null_fields() {
#[derive(serde::Serialize)]
struct Params {
name: &'static str,
optional: Option<&'static str>,
}
let rb = bare_client().post("https://example.com").form(&Params {
name: "alice",
optional: None,
});
let clone = rb.try_clone().unwrap();
let body = String::from_utf8(clone.body.unwrap().as_bytes().unwrap().to_vec()).unwrap();
assert!(body.contains("name=alice"), "should contain name");
assert!(!body.contains("optional"), "null field should be skipped");
}
#[cfg(feature = "query")]
#[test]
fn query_serialization_with_port_and_fragment() {
let rb = bare_client()
.get("https://example.com:9443/api#frag")
.query(&[("key", "val")]);
let clone = rb.try_clone().unwrap();
let url = clone.url.unwrap();
assert_eq!(url.as_str(), "https://example.com:9443/api?key=val#frag");
assert_eq!(url.query(), Some("key=val"));
}
#[test]
fn http_request_roundtrip() {
let original = http::Request::builder()
.method(http::Method::POST)
.uri("https://example.com/api")
.header("x-custom", "value")
.version(http::Version::HTTP_11)
.body("payload")
.unwrap();
let wrest_req: Request = original.try_into().unwrap();
assert_eq!(wrest_req.method(), &http::Method::POST);
assert_eq!(wrest_req.url().as_str(), "https://example.com/api");
assert_eq!(wrest_req.headers().get("x-custom").unwrap(), "value");
assert_eq!(wrest_req.body().unwrap().as_bytes().unwrap(), b"payload");
let back: http::Request<Body> = wrest_req.try_into().unwrap();
assert_eq!(back.method(), http::Method::POST);
assert_eq!(back.uri(), "https://example.com/api");
assert_eq!(back.version(), http::Version::HTTP_11);
assert_eq!(back.headers().get("x-custom").unwrap(), "value");
assert_eq!(back.body().as_bytes().unwrap(), b"payload");
let no_body = Request::new(http::Method::GET, "https://example.com".parse().unwrap());
let http_req: http::Request<Body> = no_body.try_into().unwrap();
assert_eq!(http_req.body().as_bytes().unwrap(), b"");
let bad = http::Request::builder()
.uri("ftp://not-supported.com")
.body("")
.unwrap();
let result: Result<Request, _> = bad.try_into();
assert!(result.is_err(), "unsupported scheme should fail");
}
}