use crate::ClientErrorStatusCode;
use crate::ErrorStatusCode;
use hyper::Error as HyperError;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::error::Error;
use std::fmt;
#[derive(Debug)]
pub struct HttpError {
pub status_code: ErrorStatusCode,
pub error_code: Option<String>,
pub external_message: String,
pub internal_message: String,
pub headers: Option<Box<http::HeaderMap>>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct HttpErrorResponseBody {
pub request_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub error_code: Option<String>,
pub message: String,
}
impl JsonSchema for HttpErrorResponseBody {
fn schema_name() -> String {
"Error".to_string()
}
fn json_schema(
gen: &mut schemars::gen::SchemaGenerator,
) -> schemars::schema::Schema {
let str_schema = String::json_schema(gen);
schemars::schema::SchemaObject {
metadata: Some(
schemars::schema::Metadata {
description: Some(
"Error information from a response.".into(),
),
..Default::default()
}
.into(),
),
instance_type: Some(schemars::schema::InstanceType::Object.into()),
object: Some(
schemars::schema::ObjectValidation {
required: ["message".into(), "request_id".into()]
.into_iter()
.collect(),
properties: [
("error_code".into(), str_schema.clone()),
("message".into(), str_schema.clone()),
("request_id".into(), str_schema.clone()),
]
.into_iter()
.collect(),
..Default::default()
}
.into(),
),
..Default::default()
}
.into()
}
}
impl From<HyperError> for HttpError {
fn from(error: HyperError) -> Self {
HttpError::for_bad_request(
None,
format!("error processing request: {}", error),
)
}
}
impl From<http::Error> for HttpError {
fn from(error: http::Error) -> Self {
HttpError::for_bad_request(
None,
format!("error processing request: {}", error),
)
}
}
impl HttpError {
pub fn for_client_error(
error_code: Option<String>,
status_code: ClientErrorStatusCode,
message: String,
) -> Self {
HttpError {
status_code: status_code.into(),
error_code,
internal_message: message.clone(),
external_message: message,
headers: None,
}
}
pub fn for_internal_error(internal_message: String) -> Self {
let status_code = ErrorStatusCode::INTERNAL_SERVER_ERROR;
HttpError {
status_code,
error_code: Some(String::from("Internal")),
external_message: status_code
.canonical_reason()
.unwrap()
.to_string(),
internal_message,
headers: None,
}
}
pub fn for_unavail(
error_code: Option<String>,
internal_message: String,
) -> Self {
let status_code = ErrorStatusCode::SERVICE_UNAVAILABLE;
HttpError {
status_code,
error_code,
external_message: status_code
.canonical_reason()
.unwrap()
.to_string(),
internal_message,
headers: None,
}
}
pub fn for_bad_request(
error_code: Option<String>,
message: String,
) -> Self {
HttpError::for_client_error(
error_code,
ClientErrorStatusCode::BAD_REQUEST,
message,
)
}
pub fn for_client_error_with_status(
error_code: Option<String>,
status_code: ClientErrorStatusCode,
) -> Self {
let message = status_code.canonical_reason().unwrap().to_string();
HttpError::for_client_error(error_code, status_code, message)
}
pub fn for_not_found(
error_code: Option<String>,
internal_message: String,
) -> Self {
let status_code = ErrorStatusCode::NOT_FOUND;
let external_message =
status_code.canonical_reason().unwrap().to_string();
HttpError {
status_code,
error_code,
internal_message,
external_message,
headers: None,
}
}
pub fn headers_mut(&mut self) -> &mut http::HeaderMap {
self.headers.get_or_insert_with(|| Box::new(http::HeaderMap::new()))
}
pub fn add_header<K, V>(
&mut self,
name: K,
value: V,
) -> Result<&mut Self, http::Error>
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 = <http::HeaderName as TryFrom<K>>::try_from(name)
.map_err(Into::into)?;
let value = <http::HeaderValue as TryFrom<V>>::try_from(value)
.map_err(Into::into)?;
self.headers_mut().try_append(name, value)?;
Ok(self)
}
pub fn with_header<K, V>(
mut self,
name: K,
value: V,
) -> Result<Self, http::Error>
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 = <http::HeaderName as TryFrom<K>>::try_from(name)
.map_err(Into::into)?;
let value = <http::HeaderValue as TryFrom<V>>::try_from(value)
.map_err(Into::into)?;
self.headers_mut().try_append(name, value)?;
Ok(self)
}
pub fn into_response(
self,
request_id: &str,
) -> hyper::Response<crate::Body> {
let mut builder = hyper::Response::builder();
if let Some(headers) = self.headers {
let builder_headers = builder
.headers_mut()
.expect("a newly created response builder cannot have failed");
*builder_headers = *headers;
}
builder
.status(self.status_code.as_status())
.header(
http::header::CONTENT_TYPE,
super::http_util::CONTENT_TYPE_JSON,
)
.header(super::http_util::HEADER_REQUEST_ID, request_id)
.body(
serde_json::to_string_pretty(&HttpErrorResponseBody {
request_id: request_id.to_string(),
message: self.external_message,
error_code: self.error_code,
})
.unwrap()
.into(),
)
.unwrap()
}
}
impl fmt::Display for HttpError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "HttpError({}): {}", self.status_code, self.external_message)
}
}
impl Error for HttpError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
None
}
}
#[cfg(test)]
mod test {
use crate::HttpErrorResponseBody;
#[test]
fn test_serialize_error_response_body() {
let err = HttpErrorResponseBody {
request_id: "123".to_string(),
error_code: None,
message: "oy!".to_string(),
};
let out = serde_json::to_string(&err).unwrap();
assert_eq!(out, r#"{"request_id":"123","message":"oy!"}"#);
let err = HttpErrorResponseBody {
request_id: "123".to_string(),
error_code: Some("err".to_string()),
message: "oy!".to_string(),
};
let out = serde_json::to_string(&err).unwrap();
assert_eq!(
out,
r#"{"request_id":"123","error_code":"err","message":"oy!"}"#
);
}
}