use std::any;
use std::error::Error;
use bytes::Bytes;
use http::{Response, Uri};
use thiserror::Error;
use crate::api::PaginationError;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum BodyError {
#[error("failed to URL encode form parameters: {}", source)]
UrlEncoded {
#[from]
source: serde_urlencoded::ser::Error,
},
#[error("failed to serialize request body: {}", source)]
Serialize {
#[from]
source: serde_json::Error,
},
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ApiError<E>
where
E: Error + Send + Sync + 'static,
{
#[error("client error: {}", source)]
Client {
source: E,
},
#[error("failed to parse url: {}", source)]
UrlParse {
#[from]
source: url::ParseError,
},
#[error("failed to create form data: {}", source)]
Body {
#[from]
source: BodyError,
},
#[error("could not parse JSON response: {}", source)]
Json {
#[from]
source: serde_json::Error,
},
#[error("resource cannot be found")]
ResourceNotFound,
#[error("cannot find unambiguous resource by identifier")]
IdNotUnique,
#[error("openstack session error: {}", msg)]
Session {
msg: String,
},
#[error(
"openstack server error:\n\turi: `{}`\n\tstatus: `{}`\n\tmessage: `{}`\n\trequest-id: `{}`",
uri,
status,
msg,
req_id.as_deref().unwrap_or("")
)]
OpenStack {
status: http::StatusCode,
uri: Uri,
msg: String,
req_id: Option<String>,
},
#[error(
"openstack internal server error:\n\turi: `{}`\n\tstatus: `{}`\n\trequest-id: `{}`",
uri,
status,
req_id.as_deref().unwrap_or("")
)]
OpenStackService {
status: http::StatusCode,
uri: Uri,
data: String,
req_id: Option<String>,
},
#[error(
"openstack server error:\n\turi: `{}`\n\tstatus: `{}`\n\tdata: `{}`\n\trequest-id: `{}`",
uri,
status,
obj,
req_id.as_deref().unwrap_or("")
)]
OpenStackUnrecognized {
status: http::StatusCode,
uri: Uri,
obj: serde_json::Value,
req_id: Option<String>,
},
#[error("could not parse {} data from JSON: {}", typename, source)]
DataType {
source: serde_json::Error,
typename: &'static str,
},
#[error("failed to handle pagination: {}", source)]
Pagination {
#[from]
source: PaginationError,
},
#[error("service catalog error: {}", source)]
Catalog {
#[from]
source: crate::catalog::CatalogError,
},
#[error("internal error: poisoned lock: {}", context)]
PoisonedLock {
context: String,
},
#[error("endpoint builder error: {}", message)]
EndpointBuilder {
message: String,
},
#[error("invalid header {}: {}", header, message)]
InvalidHeader {
header: String,
message: String,
},
#[error("invalid url: {}", source)]
InvalidUri {
#[from]
source: http::uri::InvalidUri,
},
}
impl<E> ApiError<E>
where
E: Error + Send + Sync + 'static,
{
pub fn client(source: E) -> Self {
ApiError::Client { source }
}
pub fn catalog(source: crate::catalog::CatalogError) -> Self {
ApiError::Catalog { source }
}
pub(crate) fn server_error(
uri: Option<Uri>,
rsp: &Response<Bytes>,
body: &bytes::Bytes,
) -> Self {
let status = rsp.status();
let req_id = rsp
.headers()
.get("x-openstack-request-id")
.and_then(|x| x.to_str().ok().map(Into::into));
if http::StatusCode::NOT_FOUND.as_u16() == status {
return Self::OpenStack {
status,
uri: uri.unwrap_or(Uri::from_static("/")),
msg: String::new(),
req_id,
};
};
Self::OpenStackService {
status,
uri: uri.unwrap_or(Uri::from_static("/")),
data: String::from_utf8_lossy(body).into(),
req_id,
}
}
pub(crate) fn from_openstack(
uri: Option<Uri>,
rsp: &Response<Bytes>,
value: serde_json::Value,
) -> Self {
let status = rsp.status();
let req_id = rsp
.headers()
.get("x-openstack-request-id")
.and_then(|x| x.to_str().ok().map(Into::into));
if http::StatusCode::NOT_FOUND.as_u16() == status {
return Self::OpenStack {
status,
uri: uri.unwrap_or(Uri::from_static("/")),
msg: value.to_string(),
req_id,
};
};
let error_value = value
.pointer("/message")
.or_else(|| value.pointer("/error"))
.or_else(|| value.pointer("/faultstring"));
if let Some(error_value) = error_value {
if let Some(msg) = error_value.as_str() {
ApiError::OpenStack {
status,
uri: uri.unwrap_or(Uri::from_static("/")),
msg: msg.into(),
req_id,
}
} else {
ApiError::OpenStackUnrecognized {
status,
uri: uri.unwrap_or(Uri::from_static("/")),
obj: error_value.clone(),
req_id,
}
}
} else {
ApiError::OpenStackUnrecognized {
status,
uri: uri.unwrap_or(Uri::from_static("/")),
obj: value,
req_id,
}
}
}
pub(crate) fn data_type<T>(source: serde_json::Error) -> Self {
ApiError::DataType {
source,
typename: any::type_name::<T>(),
}
}
pub(crate) fn poisoned_lock<CTX: AsRef<str>>(context: CTX) -> Self {
ApiError::PoisonedLock {
context: context.as_ref().into(),
}
}
pub(crate) fn endpoint_builder<EX: Error + Send>(error: EX) -> Self {
ApiError::EndpointBuilder {
message: error.to_string(),
}
}
}
#[cfg(test)]
mod tests {
use bytes::Bytes;
use http::{Response, Uri};
use serde_json::json;
use thiserror::Error;
use crate::api::ApiError;
#[derive(Debug, Error)]
#[error("my error")]
enum MyError {}
#[test]
fn openstack_error_error() {
let obj = json!({
"error": "error contents",
});
let err: ApiError<MyError> = ApiError::from_openstack(
Some(Uri::from_static("http://foo.bar")),
&Response::builder()
.status(http::StatusCode::CONFLICT)
.header("x-openstack-request-id", "reqid")
.body(Bytes::from(serde_json::to_vec(&obj).unwrap()))
.unwrap(),
obj.clone(),
);
if let ApiError::OpenStack {
status,
uri,
msg,
req_id,
} = err
{
assert_eq!(uri, Uri::from_static("http://foo.bar"));
assert_eq!(msg, "error contents");
assert_eq!(status, http::StatusCode::CONFLICT);
assert_eq!(req_id, Some("reqid".into()));
} else {
panic!("unexpected error: {err}");
}
}
#[test]
fn openstack_error_message_string() {
let obj = json!({
"message": "error contents",
});
let err: ApiError<MyError> = ApiError::from_openstack(
Some(Uri::from_static("http://foo.bar")),
&Response::builder()
.status(http::StatusCode::CONFLICT)
.header("x-openstack-request-id", "reqid")
.body(Bytes::from(serde_json::to_vec(&obj).unwrap()))
.unwrap(),
obj.clone(),
);
if let ApiError::OpenStack {
status,
uri,
msg,
req_id,
} = err
{
assert_eq!(uri, Uri::from_static("http://foo.bar"));
assert_eq!(msg, "error contents");
assert_eq!(status, http::StatusCode::CONFLICT);
assert_eq!(req_id, Some("reqid".into()));
} else {
panic!("unexpected error: {err}");
}
}
#[test]
fn openstack_error_message_object() {
let err_obj = json!({
"blah": "foo",
});
let obj = json!({
"message": err_obj,
});
let err: ApiError<MyError> = ApiError::from_openstack(
Some(Uri::from_static("http://foo.bar")),
&Response::builder()
.status(http::StatusCode::CONFLICT)
.header("x-openstack-request-id", "reqid")
.body(Bytes::from(serde_json::to_vec(&obj).unwrap()))
.unwrap(),
obj.clone(),
);
if let ApiError::OpenStackUnrecognized {
status,
uri,
obj,
req_id,
} = err
{
assert_eq!(uri, Uri::from_static("http://foo.bar"));
assert_eq!(obj, err_obj);
assert_eq!(status, http::StatusCode::CONFLICT);
assert_eq!(req_id, Some("reqid".into()));
} else {
panic!("unexpected error: {err}");
}
}
#[test]
fn openstack_error_message_unrecognized() {
let err_obj = json!({
"some_weird_key": "an even weirder value",
});
let err: ApiError<MyError> = ApiError::from_openstack(
Some(Uri::from_static("http://foo.bar")),
&Response::builder()
.status(http::StatusCode::CONFLICT)
.header("x-openstack-request-id", "reqid")
.body(Bytes::from(serde_json::to_vec(&err_obj).unwrap()))
.unwrap(),
err_obj.clone(),
);
if let ApiError::OpenStackUnrecognized {
status,
uri,
obj,
req_id,
} = err
{
assert_eq!(uri, Uri::from_static("http://foo.bar"));
assert_eq!(obj, err_obj);
assert_eq!(status, http::StatusCode::CONFLICT);
assert_eq!(req_id, Some("reqid".into()));
} else {
panic!("unexpected error: {err}");
}
}
#[test]
fn openstack_error_not_found() {
let err_obj = json!({
"some_weird_key": "an even weirder value",
});
let err: ApiError<MyError> = ApiError::from_openstack(
Some(Uri::from_static("http://foo.bar")),
&Response::builder()
.status(http::StatusCode::NOT_FOUND)
.header("x-openstack-request-id", "reqid")
.body(Bytes::from(serde_json::to_vec(&err_obj).unwrap()))
.unwrap(),
err_obj.clone(),
);
if let ApiError::OpenStack {
status,
uri,
msg,
req_id,
} = err
{
assert_eq!(uri, Uri::from_static("http://foo.bar"));
assert_eq!(msg, err_obj.to_string());
assert_eq!(status, http::StatusCode::NOT_FOUND);
assert_eq!(req_id, Some("reqid".into()));
} else {
panic!("unexpected error: {err}");
}
}
#[test]
fn openstack_error_message_octavia() {
let obj = json!({
"faultstring": "foo",
"debuginfo": null
});
let err: ApiError<MyError> = ApiError::from_openstack(
Some(Uri::from_static("http://foo.bar")),
&Response::builder()
.status(http::StatusCode::CONFLICT)
.header("x-openstack-request-id", "reqid")
.body(Bytes::from(serde_json::to_vec(&obj).unwrap()))
.unwrap(),
obj.clone(),
);
if let ApiError::OpenStack {
status,
uri,
msg,
req_id,
} = err
{
assert_eq!(uri, Uri::from_static("http://foo.bar"));
assert_eq!(obj["faultstring"], msg);
assert_eq!(status, http::StatusCode::CONFLICT);
assert_eq!(req_id, Some("reqid".into()));
} else {
panic!("unexpected error: {err}");
}
}
}