use std::sync::Arc;
use axum::Router;
use axum::body::Body;
use axum::extract::State;
use axum::http::{Request, StatusCode, header};
use axum::response::{IntoResponse, Response};
use axum::routing::post;
use crate::adapters::secondary::a2a::A2aMethodTable;
use crate::application::a2a::handler::{A2aError, A2aRequestContext, A2aRequestHandler};
use crate::application::jsonrpc::MethodTable;
use crate::domain::a2a::version::A2aVersion;
pub fn serve_jsonrpc<H>(path: &str, handler: Arc<H>, protocol_version: A2aVersion) -> Router
where
H: A2aRequestHandler + ?Sized + 'static,
{
let state = JsonRpcState {
handler,
protocol_version,
};
Router::new()
.route(path, post(handle_request::<H>))
.with_state(Arc::new(state))
}
struct JsonRpcState<H: A2aRequestHandler + ?Sized + 'static> {
handler: Arc<H>,
protocol_version: A2aVersion,
}
async fn handle_request<H>(
State(state): State<Arc<JsonRpcState<H>>>,
request: Request<Body>,
) -> Response
where
H: A2aRequestHandler + ?Sized + 'static,
{
let content_type = request
.headers()
.get(header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.map(str::to_owned);
let Some(content_type) = content_type else {
return error_response(
None,
A2aError::ContentTypeNotSupported {
received: String::new(),
},
);
};
if !content_type.starts_with("application/json") {
return error_response(
None,
A2aError::ContentTypeNotSupported {
received: content_type,
},
);
}
let Ok(body) = axum::body::to_bytes(request.into_body(), 64 * 1024 * 1024).await else {
return error_response(
None,
A2aError::InternalError {
message: "failed to read request body".to_owned(),
},
);
};
let envelope: serde_json::Value = match serde_json::from_slice(&body) {
Ok(v) => v,
Err(error) => {
return (
StatusCode::BAD_REQUEST,
format!("invalid JSON-RPC envelope: {error}"),
)
.into_response();
}
};
let Some(envelope_obj) = envelope.as_object() else {
return (
StatusCode::BAD_REQUEST,
"JSON-RPC envelope must be an object",
)
.into_response();
};
let id = envelope_obj.get("id").cloned();
let method_name = match envelope_obj
.get("method")
.and_then(serde_json::Value::as_str)
{
Some(m) => m.to_owned(),
None => {
return error_response(
id,
A2aError::InvalidParams {
reason: "missing or non-string `method`".to_owned(),
},
);
}
};
let params = envelope_obj.get("params").cloned();
let Some(method) = A2aMethodTable::TABLE.lookup(&method_name) else {
return error_response(id, A2aError::MethodNotFound);
};
let context = A2aRequestContext {
protocol_version: state.protocol_version,
};
let result = state.handler.dispatch(method, params, &context).await;
match result {
Ok(value) => success_response(id, value),
Err(error) => error_response(id, error),
}
}
fn success_response(id: Option<serde_json::Value>, result: serde_json::Value) -> Response {
let envelope = serde_json::json!({
"jsonrpc": "2.0",
"id": &id,
"result": &result,
});
drop((id, result));
json_response(StatusCode::OK, envelope)
}
fn error_response(id: Option<serde_json::Value>, error: A2aError) -> Response {
let envelope = serde_json::json!({
"jsonrpc": "2.0",
"id": &id,
"error": {
"code": error.code(),
"message": error.message(),
},
});
drop((id, error));
json_response(StatusCode::OK, envelope)
}
fn json_response(status: StatusCode, body: serde_json::Value) -> Response {
let Ok(bytes) = serde_json::to_vec(&body) else {
return (
StatusCode::INTERNAL_SERVER_ERROR,
"failed to serialise response",
)
.into_response();
};
drop(body);
(status, [(header::CONTENT_TYPE, "application/json")], bytes).into_response()
}
#[cfg(test)]
mod tests {
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use axum::body::Body;
use axum::http::{Request, StatusCode};
use tower::ServiceExt;
use super::serve_jsonrpc;
use crate::adapters::secondary::a2a::A2aMethod;
use crate::application::a2a::handler::{A2aError, A2aRequestContext, A2aRequestHandler};
use crate::domain::a2a::version::A2aVersion;
type TestError = Box<dyn std::error::Error>;
type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
struct EchoHandler;
impl A2aRequestHandler for EchoHandler {
fn dispatch(
&self,
method: A2aMethod,
_params: Option<serde_json::Value>,
_context: &A2aRequestContext,
) -> BoxFuture<'_, Result<serde_json::Value, A2aError>> {
Box::pin(async move { Ok(serde_json::json!({ "method": method.as_str() })) })
}
}
struct StubHandler;
impl A2aRequestHandler for StubHandler {
fn dispatch(
&self,
method: A2aMethod,
_params: Option<serde_json::Value>,
_context: &A2aRequestContext,
) -> BoxFuture<'_, Result<serde_json::Value, A2aError>> {
Box::pin(async move { Err(A2aError::MethodNotImplemented { method }) })
}
}
struct FlakeyHandler;
impl A2aRequestHandler for FlakeyHandler {
fn dispatch(
&self,
method: A2aMethod,
_params: Option<serde_json::Value>,
_context: &A2aRequestContext,
) -> BoxFuture<'_, Result<serde_json::Value, A2aError>> {
Box::pin(async move {
if method == A2aMethod::GetTask {
Ok(serde_json::json!({ "task": "ok" }))
} else {
Err(A2aError::InternalError {
message: "flakey".to_owned(),
})
}
})
}
}
fn post(body: &str) -> Result<Request<Body>, TestError> {
Request::builder()
.method("POST")
.uri("/a2a")
.header("content-type", "application/json")
.body(Body::from(body.to_owned()))
.map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })
}
#[tokio::test]
async fn post_send_message_dispatches_to_handler() -> Result<(), TestError> {
let router = serve_jsonrpc(
"/a2a",
Arc::new(EchoHandler) as Arc<dyn A2aRequestHandler>,
A2aVersion::V1_0,
);
let response = router
.oneshot(post(
r#"{"jsonrpc":"2.0","id":1,"method":"SendMessage","params":{}}"#,
)?)
.await
.map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?;
assert_eq!(response.status(), StatusCode::OK);
let bytes = axum::body::to_bytes(response.into_body(), 64 * 1024)
.await
.map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?;
let document: serde_json::Value = serde_json::from_slice(&bytes)
.map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?;
let obj = document
.as_object()
.ok_or("the response must be a JSON object")?;
assert_eq!(
obj.get("jsonrpc").and_then(serde_json::Value::as_str),
Some("2.0")
);
assert_eq!(obj.get("id").and_then(serde_json::Value::as_i64), Some(1));
let result = obj
.get("result")
.and_then(serde_json::Value::as_object)
.ok_or("`result` must be an object")?;
assert_eq!(
result.get("method").and_then(serde_json::Value::as_str),
Some("SendMessage")
);
Ok(())
}
#[tokio::test]
async fn unknown_method_returns_minus_32601() -> Result<(), TestError> {
let router = serve_jsonrpc(
"/a2a",
Arc::new(EchoHandler) as Arc<dyn A2aRequestHandler>,
A2aVersion::V1_0,
);
let response = router
.oneshot(post(
r#"{"jsonrpc":"2.0","id":2,"method":"NotARealMethod","params":{}}"#,
)?)
.await
.map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?;
assert_eq!(response.status(), StatusCode::OK);
let bytes = axum::body::to_bytes(response.into_body(), 64 * 1024)
.await
.map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?;
let document: serde_json::Value = serde_json::from_slice(&bytes)
.map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?;
let error = document
.get("error")
.and_then(serde_json::Value::as_object)
.ok_or("`error` must be a JSON object")?;
assert_eq!(
error.get("code").and_then(serde_json::Value::as_i64),
Some(-32601)
);
Ok(())
}
#[tokio::test]
async fn method_not_implemented_returns_minus_32601() -> Result<(), TestError> {
let router = serve_jsonrpc(
"/a2a",
Arc::new(StubHandler) as Arc<dyn A2aRequestHandler>,
A2aVersion::V1_0,
);
let response = router
.oneshot(post(
r#"{"jsonrpc":"2.0","id":3,"method":"SendMessage","params":{}}"#,
)?)
.await
.map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?;
let bytes = axum::body::to_bytes(response.into_body(), 64 * 1024)
.await
.map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?;
let document: serde_json::Value = serde_json::from_slice(&bytes)
.map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?;
let error = document
.get("error")
.and_then(serde_json::Value::as_object)
.ok_or("`error` must be a JSON object")?;
assert_eq!(
error.get("code").and_then(serde_json::Value::as_i64),
Some(-32601)
);
Ok(())
}
#[tokio::test]
async fn internal_error_returns_minus_32603() -> Result<(), TestError> {
let router = serve_jsonrpc(
"/a2a",
Arc::new(FlakeyHandler) as Arc<dyn A2aRequestHandler>,
A2aVersion::V1_0,
);
let response = router
.oneshot(post(
r#"{"jsonrpc":"2.0","id":4,"method":"SendMessage","params":{}}"#,
)?)
.await
.map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?;
let bytes = axum::body::to_bytes(response.into_body(), 64 * 1024)
.await
.map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?;
let document: serde_json::Value = serde_json::from_slice(&bytes)
.map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?;
let error = document
.get("error")
.and_then(serde_json::Value::as_object)
.ok_or("`error` must be a JSON object")?;
assert_eq!(
error.get("code").and_then(serde_json::Value::as_i64),
Some(-32603)
);
Ok(())
}
#[tokio::test]
async fn non_json_content_type_returns_minus_32000() -> Result<(), TestError> {
let router = serve_jsonrpc(
"/a2a",
Arc::new(EchoHandler) as Arc<dyn A2aRequestHandler>,
A2aVersion::V1_0,
);
let request = Request::builder()
.method("POST")
.uri("/a2a")
.header("content-type", "text/plain")
.body(Body::from("{}"))
.map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?;
let response = router
.oneshot(request)
.await
.map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?;
let bytes = axum::body::to_bytes(response.into_body(), 64 * 1024)
.await
.map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?;
let document: serde_json::Value = serde_json::from_slice(&bytes)
.map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?;
let error = document
.get("error")
.and_then(serde_json::Value::as_object)
.ok_or("`error` must be a JSON object")?;
assert_eq!(
error.get("code").and_then(serde_json::Value::as_i64),
Some(-32000)
);
Ok(())
}
#[tokio::test]
async fn get_returns_method_not_allowed() -> Result<(), TestError> {
let router = serve_jsonrpc(
"/a2a",
Arc::new(EchoHandler) as Arc<dyn A2aRequestHandler>,
A2aVersion::V1_0,
);
let request = Request::builder()
.method("GET")
.uri("/a2a")
.body(Body::empty())
.map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?;
let response = router
.oneshot(request)
.await
.map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?;
assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED);
Ok(())
}
}