#![cfg(feature = "ureq")]
use ureq::tls::TlsProvider;
use ureq::{http::Response, Agent, Body};
use super::{ClientOverride, HeaderMap, HttpResponse};
use crate::{errors::status_to_error, Error, Result};
pub fn get(
url: &str,
headers: HeaderMap,
timeout: Option<std::time::Duration>,
client: &ClientOverride,
) -> Result<impl HttpResponse> {
let built_agent;
let (agent, is_injected): (&Agent, bool) = match &client.agent {
Some(agent) => (agent, true),
None => {
#[cfg(feature = "rustls")]
let provider = TlsProvider::Rustls;
#[cfg(not(feature = "rustls"))]
let provider = TlsProvider::NativeTls;
let config = Agent::config_builder()
.tls_config(ureq::tls::TlsConfig::builder().provider(provider).build())
.timeout_global(timeout)
.proxy(ureq::Proxy::try_from_env())
.http_status_as_error(false)
.build();
built_agent = Agent::new_with_config(config);
(&built_agent, false)
}
};
let mut req = agent.get(url);
for (key, value) in headers.into_iter() {
if let Some(key) = key {
req = req.header(key, value);
}
}
let res = match req.call() {
Ok(r) => r,
Err(ureq::Error::StatusCode(code)) if is_injected => {
return Err(status_to_error(code, url));
}
Err(e) => return Err(Error::Transport(Box::new(e))),
};
if !res.status().is_success() {
return Err(status_to_error(res.status().as_u16(), url));
}
Ok(res)
}
impl HttpResponse for Response<Body> {
fn headers(&self) -> &HeaderMap<http::HeaderValue> {
Response::headers(self)
}
fn body(self) -> impl std::io::Read {
self.into_body().into_reader()
}
fn json<T: serde::de::DeserializeOwned>(mut self) -> Result<T> {
Ok(self.body_mut().read_json::<T>()?)
}
fn text(mut self) -> Result<String> {
Ok(self.body_mut().read_to_string()?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::http_client::ClientOverride;
use std::io::{Read as _, Write as _};
use std::net::TcpListener;
fn stub(status: &'static str) -> String {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let base = format!("http://{}/", listener.local_addr().unwrap());
std::thread::spawn(move || {
if let Ok((mut stream, _)) = listener.accept() {
let mut buf = [0u8; 4096];
let _ = stream.read(&mut buf);
let body = "err";
let out = format!(
"HTTP/1.1 {}\r\nContent-Type: text/plain\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
status,
body.len(),
body
);
let _ = stream.write_all(out.as_bytes());
let _ = stream.flush();
}
});
base
}
fn get_default(status: &'static str) -> Error {
let client = ClientOverride::default();
let base = stub(status);
super::get(&base, HeaderMap::new(), None, &client)
.err()
.expect("non-2xx must be an Err")
}
fn get_injected(status: &'static str) -> Error {
let agent = ureq::Agent::new_with_config(ureq::Agent::config_builder().build());
let client = ClientOverride { agent: Some(agent) };
let base = stub(status);
super::get(&base, HeaderMap::new(), None, &client)
.err()
.expect("non-2xx must be an Err")
}
fn get_injected_no_status_error(status: &'static str) -> Error {
let agent = ureq::Agent::new_with_config(
ureq::Agent::config_builder()
.http_status_as_error(false)
.build(),
);
let client = ClientOverride { agent: Some(agent) };
let base = stub(status);
super::get(&base, HeaderMap::new(), None, &client)
.err()
.expect("non-2xx must be an Err")
}
#[test]
fn injected_agent_no_status_error_falls_through_to_is_success_check() {
let err = get_injected_no_status_error("404 Not Found");
assert!(
matches!(err, Error::NotFound { .. }),
"injected no-status-error 404 must map to Error::NotFound via is_success(), got {:?}",
err
);
assert_eq!(err.http_status(), Some(404));
let err = get_injected_no_status_error("500 Internal Server Error");
assert!(
matches!(err, Error::HttpStatus { status: 500, .. }),
"injected no-status-error 500 must map to Error::HttpStatus(500), got {:?}",
err
);
assert_eq!(err.http_status(), Some(500));
}
#[test]
fn injected_agent_status_code_arm_maps_404_to_not_found() {
let err = get_injected("404 Not Found");
assert!(
matches!(err, Error::NotFound { .. }),
"injected-agent 404 must map to Error::NotFound, got {:?}",
err
);
assert_eq!(err.http_status(), Some(404));
}
#[test]
fn injected_agent_status_code_arm_maps_401_and_403_to_unauthorized() {
let err = get_injected("401 Unauthorized");
assert!(
matches!(err, Error::Unauthorized { status: 401, .. }),
"injected-agent 401 must map to Error::Unauthorized(401), got {:?}",
err
);
let err = get_injected("403 Forbidden");
assert!(
matches!(err, Error::Unauthorized { status: 403, .. }),
"injected-agent 403 must map to Error::Unauthorized(403), got {:?}",
err
);
}
#[test]
fn injected_agent_status_code_arm_maps_500_and_400_to_http_status() {
let err = get_injected("500 Internal Server Error");
assert!(
matches!(err, Error::HttpStatus { status: 500, .. }),
"injected-agent 500 must map to Error::HttpStatus(500), got {:?}",
err
);
let err = get_injected("400 Bad Request");
assert!(
matches!(err, Error::HttpStatus { status: 400, .. }),
"injected-agent 400 must map to Error::HttpStatus(400), got {:?}",
err
);
}
#[test]
fn default_agent_path_maps_statuses_identically_to_injected() {
assert!(matches!(
get_default("404 Not Found"),
Error::NotFound { .. }
));
assert!(matches!(
get_default("401 Unauthorized"),
Error::Unauthorized { status: 401, .. }
));
assert!(matches!(
get_default("403 Forbidden"),
Error::Unauthorized { status: 403, .. }
));
assert!(matches!(
get_default("503 Service Unavailable"),
Error::HttpStatus { status: 503, .. }
));
}
#[test]
fn transport_failure_maps_to_transport_variant() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
drop(listener);
let url = format!("http://{}/", addr);
let client = ClientOverride::default();
let err = super::get(&url, HeaderMap::new(), None, &client)
.err()
.expect("connection refused must be an Err");
assert!(
matches!(err, Error::Transport(_)),
"a failed (uncompleted) request must map to Error::Transport, got {:?}",
err
);
assert_eq!(err.http_status(), None, "Transport has no HTTP status code");
}
}