use crate::{
error::{Error, ErrorKind},
http::{headers::ERROR_CODE, AsyncRawResponse, RawResponse, StatusCode},
};
use serde::Deserialize;
use std::{collections::HashMap, future::Future, str};
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ErrorResponse {
pub error: Option<ErrorDetail>,
}
impl TryFrom<Error> for ErrorResponse {
type Error = Error;
fn try_from(value: Error) -> Result<Self, Self::Error> {
match value.kind() {
ErrorKind::HttpResponse { raw_response, .. } => {
let error_response: Option<crate::Result<ErrorResponse>> = raw_response
.as_ref()
.map(|raw| serde_json::from_slice(raw.body().as_ref()).map_err(Error::from));
match error_response {
Some(result) => Ok(result?),
None => Err(value),
}
}
_ => Err(value),
}
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ErrorDetail {
pub code: Option<String>,
pub message: Option<String>,
pub target: Option<String>,
#[serde(default)]
pub details: Vec<ErrorDetail>,
#[serde(rename = "innererror")]
pub inner_error: Option<InnerError>,
#[serde(flatten)]
pub additional_properties: HashMap<String, crate::Value>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InnerError {
pub code: Option<String>,
#[serde(rename = "innererror")]
pub inner_error: Option<Box<InnerError>>,
}
#[derive(Debug, Deserialize)]
struct ErrorResponseInternal<'a> {
#[serde(borrow)]
error: ErrorDetailsInternal<'a>,
}
#[derive(Debug, Deserialize)]
struct ErrorDetailsInternal<'a> {
code: Option<&'a str>,
message: Option<&'a str>,
}
pub trait Response: crate::private::Sealed {
fn status(&self) -> StatusCode;
fn try_into_raw_response(self) -> impl Future<Output = crate::Result<RawResponse>>;
}
impl crate::private::Sealed for AsyncRawResponse {}
impl crate::private::Sealed for RawResponse {}
impl Response for AsyncRawResponse {
fn status(&self) -> StatusCode {
self.status()
}
fn try_into_raw_response(self) -> impl Future<Output = crate::Result<RawResponse>> {
self.try_into_raw_response()
}
}
impl Response for RawResponse {
fn status(&self) -> StatusCode {
self.status()
}
#[inline]
fn try_into_raw_response(self) -> impl Future<Output = crate::Result<RawResponse>> {
std::future::ready(Ok(self))
}
}
#[derive(Debug, Default)]
pub struct CheckSuccessOptions {
pub success_codes: &'static [u16],
}
pub async fn check_success<T: Response>(
response: T,
options: Option<CheckSuccessOptions>,
) -> crate::Result<T> {
let status = response.status();
if options
.as_ref()
.map(|o| {
if o.success_codes.is_empty() {
status.is_success()
} else {
o.success_codes.contains(&status)
}
})
.unwrap_or_else(|| status.is_success())
{
return Ok(response);
}
let raw_response = response.try_into_raw_response().await?;
if raw_response.body().is_empty() {
let error_code = raw_response
.headers()
.get_optional_str(&ERROR_CODE)
.map(str::to_owned);
let error_kind = ErrorKind::HttpResponse {
status,
error_code,
raw_response: Some(Box::new(raw_response)),
};
return Err(Error::with_message(error_kind, status.to_string()));
}
let internal_response =
serde_json::de::from_slice::<ErrorResponseInternal>(raw_response.body())
.map_err(Error::from);
let internal_response = match internal_response {
Ok(r) => r,
Err(_) => {
let error_code = raw_response
.headers()
.get_optional_str(&ERROR_CODE)
.map_or_else(|| raw_response.status().to_string(), str::to_owned);
let message = str::from_utf8(raw_response.body())
.unwrap_or("(invalid utf-8 in body)")
.to_string();
let error_kind = ErrorKind::HttpResponse {
status,
error_code: Some(error_code),
raw_response: Some(Box::new(raw_response)),
};
return Err(Error::with_message(
error_kind,
format!("{}: {}", status, message),
));
}
};
let error_code = raw_response
.headers()
.get_optional_str(&ERROR_CODE)
.or(internal_response.error.code)
.map(str::to_owned);
let message = internal_response
.error
.message
.map_or_else(|| status.to_string(), str::to_owned);
let error_kind = ErrorKind::HttpResponse {
status,
error_code,
raw_response: Some(Box::new(raw_response)),
};
Err(Error::with_message(error_kind, message))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::http::{headers, headers::Headers, StatusCode};
use crate::Bytes;
#[tokio::test]
async fn matching_against_http_error() {
let mut headers = Headers::new();
headers.insert(headers::CONTENT_TYPE, "application/json".to_string());
let response = AsyncRawResponse::from_bytes(
StatusCode::ImATeapot,
headers,
Bytes::from_static(br#"{"error": {"code":"teapot","message":"I'm a teapot"}}"#),
);
let err = check_success(response, None).await.unwrap_err();
let kind = err.kind();
assert!(matches!(
kind,
ErrorKind::HttpResponse {
status: StatusCode::ImATeapot,
error_code,
raw_response: Some(_),
}
if error_code.as_deref() == Some("teapot")
));
}
#[tokio::test]
async fn matching_against_custom_http_error_empty_set() {
let mut headers = Headers::new();
headers.insert(headers::CONTENT_TYPE, "application/json".to_string());
let response = AsyncRawResponse::from_bytes(
StatusCode::ImATeapot,
headers,
Bytes::from_static(br#"{"error": {"code":"teapot","message":"I'm a teapot"}}"#),
);
let err = check_success(response, Some(CheckSuccessOptions { success_codes: &[] }))
.await
.unwrap_err();
let kind = err.kind();
assert!(matches!(
kind,
ErrorKind::HttpResponse {
status: StatusCode::ImATeapot,
error_code,
raw_response: Some(_),
}
if error_code.as_deref() == Some("teapot")
));
}
#[tokio::test]
async fn matching_against_custom_http_error_in_set() {
let mut headers = Headers::new();
headers.insert(headers::CONTENT_TYPE, "application/json".to_string());
let response = AsyncRawResponse::from_bytes(
StatusCode::ImATeapot,
headers,
Bytes::from_static(br#"{"error": {"code":"teapot","message":"I'm a teapot"}}"#),
);
let _ = check_success(
response,
Some(CheckSuccessOptions {
success_codes: &[418],
}),
)
.await
.expect("Should be a success return");
}
#[tokio::test]
async fn matching_against_custom_http_error_in_set_success_should_fail() {
let mut headers = Headers::new();
headers.insert(headers::CONTENT_TYPE, "application/json".to_string());
let response = AsyncRawResponse::from_bytes(
StatusCode::Ok,
headers,
Bytes::from_static(br#"{"error": {"code":"teapot","message":"I'm a teapot"}}"#),
);
let err = check_success(
response,
Some(CheckSuccessOptions {
success_codes: &[418],
}),
)
.await
.expect_err("Should be a failure return");
let kind = err.kind();
assert!(matches!(
kind,
ErrorKind::HttpResponse {
status: StatusCode::Ok,
error_code,
raw_response: Some(_),
}
if error_code.as_deref() == Some("teapot")
));
}
#[tokio::test]
async fn matching_against_http_error_no_body() {
let mut headers = Headers::new();
headers.insert(headers::ERROR_CODE, "testError".to_string());
let response = AsyncRawResponse::from_bytes(StatusCode::ImATeapot, headers, Bytes::new());
let err = check_success(response, None).await.unwrap_err();
let kind = err.kind();
assert!(matches!(
kind,
ErrorKind::HttpResponse {
status: StatusCode::ImATeapot,
error_code,
raw_response: Some(_),
}
if error_code.as_deref() == Some("testError")
));
}
#[tokio::test]
async fn matching_against_http_error_invalid_body() {
let mut headers = Headers::new();
headers.insert(headers::ERROR_CODE, "testError".to_string());
let response = AsyncRawResponse::from_bytes(
StatusCode::ImATeapot,
headers,
Bytes::from_static(br#"{"json": "error"}"#),
);
let err = check_success(response, None).await.unwrap_err();
let ErrorKind::HttpResponse {
status,
error_code: Some(error_code),
raw_response: Some(raw_response),
} = err.kind()
else {
panic!("expected ErrorKind::HttpResponse");
};
assert!(err.to_string().contains(r#"{"json": "error"}"#));
assert_eq!(status, &StatusCode::ImATeapot);
assert_eq!(error_code, "testError");
assert_eq!(raw_response.status(), StatusCode::ImATeapot);
assert_eq!(raw_response.headers().iter().count(), 1);
assert!(
matches!(str::from_utf8(raw_response.body()), Ok(body) if body == r#"{"json": "error"}"#)
);
}
#[test]
fn deserialize_to_error_response() {
let err : ErrorResponse = serde_json::from_slice (br#"{"error":{"code":"InvalidRequest","message":"The request object is not recognized.","innererror":{"code":"InvalidKey"},"key":"foo"}}"#)
.expect("Parse success.");
err.error.as_ref().expect("error should be set");
println!("{:?}", &err);
assert_eq!(
err.error.as_ref().unwrap().code,
Some("InvalidRequest".to_string())
);
assert_eq!(
err.error.as_ref().unwrap().message,
Some("The request object is not recognized.".to_string())
);
assert!(err.error.as_ref().unwrap().inner_error.is_some());
assert_eq!(
err.error
.as_ref()
.unwrap()
.inner_error
.as_ref()
.unwrap()
.code,
Some("InvalidKey".to_string())
);
assert!(err
.error
.as_ref()
.unwrap()
.additional_properties
.contains_key("key"));
}
#[tokio::test]
async fn convert_error_to_error_response() -> crate::Result<()> {
{
let err: Error = Error::from(ErrorKind::HttpResponse {
status: StatusCode::BadRequest,
error_code: Some("testError".to_string()),
raw_response: None,
});
let _error_response = ErrorResponse::try_from(err)
.expect_err("expected an error because there is no raw_response");
}
{
let buf_response = AsyncRawResponse::from_bytes(
StatusCode::BadRequest,
Headers::new(),
Bytes::from_static(br#"{"error":{"code":"InvalidRequest","message":"The request object is not recognized.","innererror":{"code":"InvalidKey"},"key":"foo"}}"#),
);
let err: Error = Error::from(ErrorKind::HttpResponse {
status: StatusCode::BadRequest,
error_code: Some("testError".to_string()),
raw_response: Some(Box::new(buf_response.try_into_raw_response().await?)),
});
let error_response = ErrorResponse::try_from(err).expect("expected an ErrorResponse");
error_response.error.as_ref().expect("error should be set");
println!("{:?}", &error_response);
assert_eq!(
error_response.error.as_ref().unwrap().code,
Some("InvalidRequest".to_string())
);
}
Ok(())
}
#[tokio::test]
async fn convert_buf_response_to_error_response() -> crate::Result<()> {
{
let buf_response = AsyncRawResponse::from_bytes(
StatusCode::BadRequest,
Headers::new(),
Bytes::from_static(br#"{"error":{"code":"InvalidRequest","message":"The request object is not recognized.","innererror":{"code":"InvalidKey"},"key":"foo"}}"#),
);
let error_response: ErrorResponse = buf_response
.try_into_raw_response()
.await?
.into_body()
.json()
.expect("expected an ErrorResponse");
error_response.error.as_ref().expect("error should be set");
println!("{:?}", &error_response);
assert_eq!(
error_response.error.as_ref().unwrap().code,
Some("InvalidRequest".to_string())
);
}
Ok(())
}
#[tokio::test]
async fn deserialize_to_error_response_internal() {
let err :ErrorResponseInternal = serde_json::from_slice (br#"{"error":{"code":"InvalidRequest","message":"The request object is not recognized.","innererror":{"code":"InvalidKey","key":"foo"}}}"#)
.expect("Parse success.");
println!("{:?}", &err);
assert_eq!(err.error.code, Some("InvalidRequest"));
assert_eq!(
err.error.message,
Some("The request object is not recognized.")
);
}
}