#![cfg_attr(docsrs, doc(cfg(feature = "axum")))]
use axum::{
http::StatusCode,
response::{IntoResponse, Response}
};
use crate::{AppError, response::ProblemJson};
impl AppError {
#[inline]
pub fn http_status(&self) -> StatusCode {
self.kind.status_code()
}
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let err = self;
let problem = ProblemJson::from_app_error(err);
problem.into_response()
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use axum::http::StatusCode;
use super::*;
use crate::AppCode;
#[test]
fn http_status_maps_from_kind() {
let e = AppError::forbidden("nope");
assert_eq!(e.http_status(), StatusCode::FORBIDDEN);
let e = AppError::validation("bad");
assert_eq!(e.http_status(), StatusCode::UNPROCESSABLE_ENTITY);
}
#[tokio::test]
async fn into_response_builds_problem_json_with_headers() {
use axum::{
body::to_bytes,
http::header::{CONTENT_TYPE, RETRY_AFTER, WWW_AUTHENTICATE},
response::IntoResponse
};
let app_err = AppError::unauthorized("missing token")
.with_retry_after_secs(7)
.with_www_authenticate("Bearer realm=\"api\"");
let resp = app_err.into_response();
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
let content_type = resp
.headers()
.get(CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.expect("content-type header");
assert_eq!(content_type, "application/problem+json");
let retry_after = resp
.headers()
.get(RETRY_AFTER)
.and_then(|value| value.to_str().ok())
.expect("retry-after header");
assert_eq!(retry_after, "7");
let www_authenticate = resp
.headers()
.get(WWW_AUTHENTICATE)
.and_then(|value| value.to_str().ok())
.expect("www-authenticate header");
assert_eq!(www_authenticate, "Bearer realm=\"api\"");
let bytes = to_bytes(resp.into_body(), usize::MAX)
.await
.expect("read body");
let body: serde_json::Value = serde_json::from_slice(&bytes).expect("json body");
assert_eq!(
body.get("status").and_then(|value| value.as_u64()),
Some(401)
);
assert_eq!(
body.get("code")
.and_then(|value| value.as_str())
.map(AppCode::from_str)
.transpose()
.expect("parse app code"),
Some(AppCode::Unauthorized)
);
assert_eq!(
body.get("detail").and_then(|value| value.as_str()),
Some("missing token")
);
assert!(body.get("metadata").is_none());
assert!(body.get("grpc").is_some());
}
#[tokio::test]
async fn redacted_errors_hide_detail() {
use axum::{body::to_bytes, response::IntoResponse};
let app_err = AppError::internal("secret").redactable();
let resp = app_err.into_response();
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
let bytes = to_bytes(resp.into_body(), usize::MAX)
.await
.expect("read body");
let body: serde_json::Value = serde_json::from_slice(&bytes).expect("json body");
assert!(body.get("detail").is_none());
assert!(body.get("metadata").is_none());
}
}