use std::convert::Infallible;
use bytes::Bytes;
use http_body_util::combinators::BoxBody;
use super::response::{build_json_response, internal_error_response};
use crate::error::ServerError;
pub fn error_json_response(
status: u16,
message: &str,
) -> hyper::Response<BoxBody<Bytes, Infallible>> {
let mut body = serde_json::json!({
"error": {
"code": status,
"message": message
}
});
if let Some(name) = canonical_status_name(status) {
body["error"]["status"] = serde_json::Value::from(name);
}
serde_json::to_vec(&body).map_or_else(
|_| internal_error_response(),
|bytes| build_json_response(status, bytes),
)
}
const fn canonical_status_name(status: u16) -> Option<&'static str> {
Some(match status {
400 => "INVALID_ARGUMENT",
401 => "UNAUTHENTICATED",
403 => "PERMISSION_DENIED",
404 => "NOT_FOUND",
409 => "ABORTED",
429 => "RESOURCE_EXHAUSTED",
499 => "CANCELLED",
500 => "INTERNAL",
501 => "UNIMPLEMENTED",
503 => "UNAVAILABLE",
504 => "DEADLINE_EXCEEDED",
_ => return None,
})
}
pub(super) fn not_found_response() -> hyper::Response<BoxBody<Bytes, Infallible>> {
error_json_response(404, "not found")
}
pub fn server_error_to_response(err: &ServerError) -> hyper::Response<BoxBody<Bytes, Infallible>> {
let a2a_err = err.to_a2a_error();
let status = err.http_status();
let grpc_status = err.status_name();
let details = a2a_err.error_info_data(None);
let mut error_obj = serde_json::json!({
"error": {
"code": status,
"status": grpc_status,
"message": a2a_err.message
}
});
if !details.is_null() {
error_obj["error"]["details"] = details;
}
serde_json::to_vec(&error_obj).map_or_else(
|_| internal_error_response(),
|body| {
let mut resp = build_json_response(status, body);
crate::dispatch::add_auth_challenge(resp.headers_mut(), err);
resp
},
)
}
#[cfg(test)]
mod tests {
use super::*;
use http_body_util::BodyExt;
#[tokio::test]
async fn error_json_response_status_and_body() {
let resp = error_json_response(400, "bad request");
assert_eq!(resp.status().as_u16(), 400);
let body = resp.into_body().collect().await.unwrap().to_bytes();
let val: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(val["error"]["message"], "bad request");
assert_eq!(val["error"]["code"], 400);
}
#[tokio::test]
async fn error_json_response_names_each_mapped_code() {
let table = [
(400, "INVALID_ARGUMENT"),
(401, "UNAUTHENTICATED"),
(403, "PERMISSION_DENIED"),
(404, "NOT_FOUND"),
(409, "ABORTED"),
(429, "RESOURCE_EXHAUSTED"),
(499, "CANCELLED"),
(500, "INTERNAL"),
(501, "UNIMPLEMENTED"),
(503, "UNAVAILABLE"),
(504, "DEADLINE_EXCEEDED"),
];
for (code, name) in table {
let body = error_json_response(code, "x")
.into_body()
.collect()
.await
.unwrap()
.to_bytes();
let val: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(val["error"]["status"], name, "status {code}");
}
let body = error_json_response(413, "x")
.into_body()
.collect()
.await
.unwrap()
.to_bytes();
let val: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert!(
val["error"].get("status").is_none(),
"413 has no name: {val}"
);
}
#[tokio::test]
async fn error_json_response_has_a2a_content_type() {
let resp = error_json_response(404, "not found");
assert_eq!(
resp.headers()
.get("content-type")
.and_then(|v| v.to_str().ok()),
Some(a2a_protocol_types::JSON_CONTENT_TYPE),
);
}
#[tokio::test]
async fn not_found_response_is_404() {
let resp = not_found_response();
assert_eq!(resp.status().as_u16(), 404);
let body = resp.into_body().collect().await.unwrap().to_bytes();
let val: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(val["error"]["message"], "not found");
}
#[tokio::test]
async fn server_error_task_not_found_maps_to_404() {
let err = ServerError::TaskNotFound("t1".into());
let resp = server_error_to_response(&err);
assert_eq!(resp.status().as_u16(), 404);
}
#[tokio::test]
async fn server_error_method_not_found_maps_to_404() {
let err = ServerError::MethodNotFound("foo".into());
let resp = server_error_to_response(&err);
assert_eq!(resp.status().as_u16(), 404);
}
#[tokio::test]
async fn server_error_task_not_cancelable_maps_to_400() {
let err = ServerError::TaskNotCancelable("t1".into());
let resp = server_error_to_response(&err);
assert_eq!(resp.status().as_u16(), 400);
}
#[tokio::test]
async fn server_error_invalid_params_maps_to_400() {
let err = ServerError::InvalidParams("bad".into());
let resp = server_error_to_response(&err);
assert_eq!(resp.status().as_u16(), 400);
}
#[tokio::test]
async fn server_error_push_not_supported_maps_to_400() {
let err = ServerError::PushNotSupported;
let resp = server_error_to_response(&err);
assert_eq!(resp.status().as_u16(), 400);
}
#[tokio::test]
async fn server_error_internal_maps_to_500() {
let err = ServerError::Internal("oops".into());
let resp = server_error_to_response(&err);
assert_eq!(resp.status().as_u16(), 500);
}
#[tokio::test]
async fn error_json_response_various_statuses() {
for status in [400, 403, 404, 422, 500, 503] {
let resp = error_json_response(status, &format!("error {status}"));
assert_eq!(resp.status().as_u16(), status);
}
}
#[tokio::test]
async fn server_error_serialization_maps_to_400() {
let err = ServerError::Serialization(serde_json::from_str::<()>("bad").unwrap_err());
let resp = server_error_to_response(&err);
assert_eq!(resp.status().as_u16(), 400);
}
#[tokio::test]
async fn server_error_transport_maps_to_500() {
let err = ServerError::Transport("transport broke".into());
let resp = server_error_to_response(&err);
assert_eq!(resp.status().as_u16(), 500);
}
#[tokio::test]
async fn server_error_payload_too_large_maps_to_413() {
let err = ServerError::PayloadTooLarge("too big".into());
let resp = server_error_to_response(&err);
assert_eq!(resp.status().as_u16(), 413);
}
#[tokio::test]
async fn server_error_overloaded_maps_to_503() {
let err = ServerError::Overloaded("at capacity".into());
let resp = server_error_to_response(&err);
assert_eq!(resp.status().as_u16(), 503);
}
#[tokio::test]
async fn server_error_http_client_maps_to_500() {
let err = ServerError::HttpClient("connection refused".into());
let resp = server_error_to_response(&err);
assert_eq!(resp.status().as_u16(), 500);
}
#[tokio::test]
async fn server_error_invalid_state_transition_maps_to_400() {
use a2a_protocol_types::task::TaskState;
let err = ServerError::InvalidStateTransition {
task_id: "t1".into(),
from: TaskState::Completed,
to: TaskState::Working,
};
let resp = server_error_to_response(&err);
assert_eq!(resp.status().as_u16(), 400);
}
#[tokio::test]
async fn server_error_protocol_maps_to_500() {
let err = ServerError::Protocol(a2a_protocol_types::error::A2aError::internal("proto err"));
let resp = server_error_to_response(&err);
assert_eq!(resp.status().as_u16(), 500);
let body = resp.into_body().collect().await.unwrap().to_bytes();
let val: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert!(
val["error"]["message"]
.as_str()
.unwrap_or("")
.contains("proto err")
);
}
}