use serde_json::Value;
use crate::error::{AcmeError, Error, ProblemType};
#[derive(Debug, Clone)]
pub struct Problem {
pub problem_type: ProblemType,
pub detail: Option<String>,
pub status: u16,
pub retry_after: Option<std::time::Duration>,
}
impl Problem {
#[must_use]
pub fn parse(status: u16, body: &[u8]) -> Self {
if let Ok(doc) = serde_json::from_slice::<Value>(body) {
let type_urn = doc
.get("type")
.and_then(Value::as_str)
.unwrap_or("urn:ietf:params:acme:error:serverInternal");
return Self {
problem_type: ProblemType::from_urn(type_urn),
detail: doc.get("detail").and_then(Value::as_str).map(str::to_owned),
status,
retry_after: None,
};
}
let text = String::from_utf8_lossy(body);
Self {
problem_type: if status == 429 {
ProblemType::RateLimited
} else {
ProblemType::ServerInternal
},
detail: Some(text.trim().to_owned()),
status,
retry_after: None,
}
}
#[must_use]
pub fn with_retry_after(mut self, d: std::time::Duration) -> Self {
self.retry_after = Some(d);
self
}
#[must_use]
pub fn into_error(self) -> Error {
Error::Acme(AcmeError::Problem(
self.problem_type,
self.detail.unwrap_or_default(),
))
}
#[must_use]
pub fn is_bad_nonce(&self) -> bool {
self.problem_type == ProblemType::BadNonce
}
#[must_use]
pub fn is_account_does_not_exist(&self) -> bool {
self.problem_type == ProblemType::AccountDoesNotExist
}
}
#[must_use]
pub fn problem_from_response(resp: &crate::acme::transport::HttpResponse) -> Option<Problem> {
if resp.is_success() {
return None;
}
let mut problem = Problem::parse(resp.status, &resp.body);
if let Some(ra) = resp.retry_after() {
problem = problem.with_retry_after(ra);
}
Some(problem)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_rfc7807_document() {
let body = br#"{"type":"urn:ietf:params:acme:error:rateLimited","detail":"too many","status":429}"#;
let p = Problem::parse(429, body);
assert_eq!(p.problem_type, ProblemType::RateLimited);
assert_eq!(p.detail.as_deref(), Some("too many"));
assert_eq!(p.status, 429);
}
#[test]
fn non_json_body_maps_to_rate_limit_on_429() {
let p = Problem::parse(429, b"slow down");
assert_eq!(p.problem_type, ProblemType::RateLimited);
assert_eq!(p.detail.as_deref(), Some("slow down"));
}
#[test]
fn account_does_not_exist_detection() {
let body = br#"{"type":"urn:ietf:params:acme:error:accountDoesNotExist"}"#;
let p = Problem::parse(400, body);
assert!(p.is_account_does_not_exist());
}
#[test]
fn success_responses_have_no_problem() {
let resp = crate::acme::transport::HttpResponse {
status: 200,
headers: Default::default(),
body: b"{}".to_vec(),
};
assert!(problem_from_response(&resp).is_none());
}
#[test]
fn retry_after_is_preserved_on_http_problem_mapping() {
let resp = crate::acme::transport::HttpResponse {
status: 429,
headers: [("retry-after".into(), "11".into())].into_iter().collect(),
body: br#"{
"type":"urn:ietf:params:acme:error:rateLimited",
"detail":"account quota exceeded"
}"#
.to_vec(),
};
let problem = problem_from_response(&resp).expect("HTTP error problem");
assert_eq!(problem.problem_type, ProblemType::RateLimited);
assert_eq!(problem.status, 429);
assert_eq!(problem.detail.as_deref(), Some("account quota exceeded"));
assert_eq!(
problem.retry_after,
Some(std::time::Duration::from_secs(11))
);
}
}