use std::fmt;
const BODY_LIMIT: u64 = 256 * 1024 * 1024;
#[derive(Debug)]
pub enum Error {
Status(u16, String),
Transport(ureq::Error),
UnsupportedMethod(String),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Status(code, _) => write!(f, "http status {code}"),
Error::Transport(e) => write!(f, "{e}"),
Error::UnsupportedMethod(m) => write!(f, "unsupported HTTP method '{m}'"),
}
}
}
impl std::error::Error for Error {}
impl From<ureq::Error> for Error {
fn from(e: ureq::Error) -> Self {
match e {
ureq::Error::StatusCode(code) => Error::Status(code, String::new()),
other => Error::Transport(other),
}
}
}
type Headers<'a> = &'a [(&'a str, &'a str)];
fn get_builder(url: &str, headers: Headers) -> ureq::RequestBuilder<ureq::typestate::WithoutBody> {
let mut req = ureq::get(url).config().http_status_as_error(false).build();
for (k, v) in headers {
req = req.header(*k, *v);
}
req
}
fn post_builder(url: &str, headers: Headers) -> ureq::RequestBuilder<ureq::typestate::WithBody> {
let mut req = ureq::post(url).config().http_status_as_error(false).build();
for (k, v) in headers {
req = req.header(*k, *v);
}
req
}
fn text_of(mut resp: ureq::http::Response<ureq::Body>) -> Result<String, Error> {
let status = resp.status().as_u16();
let body = resp
.body_mut()
.with_config()
.limit(BODY_LIMIT)
.read_to_string();
if (200..300).contains(&status) {
body.map_err(Error::Transport)
} else {
Err(Error::Status(status, body.unwrap_or_default()))
}
}
pub fn get_text(url: &str, headers: Headers) -> Result<String, Error> {
text_of(get_builder(url, headers).call()?)
}
pub fn get_bytes(url: &str, headers: Headers) -> Result<Vec<u8>, Error> {
let mut resp = get_builder(url, headers).call()?;
let status = resp.status().as_u16();
if !(200..300).contains(&status) {
let detail = resp
.body_mut()
.with_config()
.limit(BODY_LIMIT)
.read_to_string()
.unwrap_or_default();
return Err(Error::Status(status, detail));
}
resp.body_mut()
.with_config()
.limit(BODY_LIMIT)
.read_to_vec()
.map_err(Error::Transport)
}
pub fn post_text(url: &str, headers: Headers, body: &str) -> Result<String, Error> {
text_of(post_builder(url, headers).send(body)?)
}
pub struct Response {
pub status: u16,
pub retry_after: Option<String>,
pub text: String,
}
pub fn send(
method: &str,
url: &str,
headers: Headers,
query: &[(String, String)],
body: Option<&str>,
) -> Result<Response, Error> {
let agent = ureq::Agent::new_with_config(
ureq::Agent::config_builder()
.http_status_as_error(false)
.build(),
);
let mut resp = match method.to_uppercase().as_str() {
"GET" | "DELETE" => {
let mut req = if method.eq_ignore_ascii_case("GET") {
agent.get(url)
} else {
agent.delete(url)
};
for (k, v) in headers {
req = req.header(*k, *v);
}
for (k, v) in query {
req = req.query(k, v);
}
req.call()?
}
"POST" | "PUT" => {
let mut req = if method.eq_ignore_ascii_case("POST") {
agent.post(url)
} else {
agent.put(url)
};
for (k, v) in headers {
req = req.header(*k, *v);
}
for (k, v) in query {
req = req.query(k, v);
}
match body {
Some(b) => req.send(b)?,
None => req.send_empty()?,
}
}
other => return Err(Error::UnsupportedMethod(other.to_string())),
};
let status = resp.status().as_u16();
let retry_after = resp
.headers()
.get("Retry-After")
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string());
let text = resp
.body_mut()
.with_config()
.limit(BODY_LIMIT)
.read_to_string()
.map_err(Error::Transport)?;
Ok(Response {
status,
retry_after,
text,
})
}