use std::fmt;
pub mod error_codes {
pub const UNAUTHENTICATED: i64 = -32001;
pub const UNAUTHORIZED: i64 = -32005;
pub const RATE_LIMIT_EXCEEDED: i64 = -32003;
pub const INVALID_REQUEST: i64 = -32600;
pub const INTERNAL_ERROR: i64 = -32603;
}
#[derive(Debug, Clone, PartialEq)]
pub enum MiddlewareError {
Unauthenticated(String),
Unauthorized(String),
RateLimitExceeded {
message: String,
retry_after: Option<u64>,
},
InvalidRequest(String),
Internal(String),
Custom {
code: String,
message: String,
},
HttpChallenge {
status: u16,
www_authenticate: String,
body: Option<String>,
},
}
impl fmt::Display for MiddlewareError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Unauthenticated(msg) => write!(f, "Authentication required: {}", msg),
Self::Unauthorized(msg) => write!(f, "Unauthorized: {}", msg),
Self::RateLimitExceeded {
message,
retry_after,
} => {
if let Some(seconds) = retry_after {
write!(f, "{} (retry after {} seconds)", message, seconds)
} else {
write!(f, "{}", message)
}
}
Self::InvalidRequest(msg) => write!(f, "Invalid request: {}", msg),
Self::Internal(msg) => write!(f, "Internal middleware error: {}", msg),
Self::Custom { code, message } => write!(f, "{}: {}", code, message),
Self::HttpChallenge {
status,
www_authenticate,
..
} => write!(f, "HTTP {} WWW-Authenticate: {}", status, www_authenticate),
}
}
}
impl std::error::Error for MiddlewareError {}
impl MiddlewareError {
pub fn unauthenticated(msg: impl Into<String>) -> Self {
Self::Unauthenticated(msg.into())
}
pub fn unauthorized(msg: impl Into<String>) -> Self {
Self::Unauthorized(msg.into())
}
pub fn rate_limit(msg: impl Into<String>, retry_after: Option<u64>) -> Self {
Self::RateLimitExceeded {
message: msg.into(),
retry_after,
}
}
pub fn invalid_request(msg: impl Into<String>) -> Self {
Self::InvalidRequest(msg.into())
}
pub fn internal(msg: impl Into<String>) -> Self {
Self::Internal(msg.into())
}
pub fn custom(code: impl Into<String>, message: impl Into<String>) -> Self {
Self::Custom {
code: code.into(),
message: message.into(),
}
}
pub fn http_challenge(status: u16, www_authenticate: impl Into<String>) -> Self {
Self::HttpChallenge {
status,
www_authenticate: www_authenticate.into(),
body: None,
}
}
pub fn http_challenge_with_body(
status: u16,
www_authenticate: impl Into<String>,
body: impl Into<String>,
) -> Self {
Self::HttpChallenge {
status,
www_authenticate: www_authenticate.into(),
body: Some(body.into()),
}
}
}
pub fn map_middleware_error_to_jsonrpc(
err: MiddlewareError,
request_id: turul_rpc::RequestId,
) -> turul_rpc::JsonRpcResponse {
use turul_rpc::error::JsonRpcErrorObject;
let error_obj = match err {
MiddlewareError::Unauthenticated(msg) => JsonRpcErrorObject::server_error(
error_codes::UNAUTHENTICATED,
&msg,
None::<serde_json::Value>,
),
MiddlewareError::Unauthorized(msg) => JsonRpcErrorObject::server_error(
error_codes::UNAUTHORIZED,
&msg,
None::<serde_json::Value>,
),
MiddlewareError::RateLimitExceeded {
message,
retry_after,
} => JsonRpcErrorObject::server_error(
error_codes::RATE_LIMIT_EXCEEDED,
&message,
retry_after.map(|s| serde_json::json!({ "retryAfter": s })),
),
MiddlewareError::InvalidRequest(msg) => {
JsonRpcErrorObject::invalid_request(Some(serde_json::json!({ "reason": msg })))
}
MiddlewareError::Internal(msg) => JsonRpcErrorObject::internal_error(Some(msg)),
MiddlewareError::Custom { message, .. } => {
JsonRpcErrorObject::internal_error(Some(message))
}
MiddlewareError::HttpChallenge { .. } => {
unreachable!("HttpChallenge must be caught at transport level before JSON-RPC dispatch")
}
};
turul_rpc::JsonRpcResponse::Error(turul_rpc::JsonRpcError::new(Some(request_id), error_obj))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_display() {
let err = MiddlewareError::unauthenticated("Missing token");
assert_eq!(err.to_string(), "Authentication required: Missing token");
let err = MiddlewareError::unauthorized("Insufficient permissions");
assert_eq!(err.to_string(), "Unauthorized: Insufficient permissions");
let err = MiddlewareError::rate_limit("Too many requests", Some(60));
assert_eq!(
err.to_string(),
"Too many requests (retry after 60 seconds)"
);
let err = MiddlewareError::rate_limit("Too many requests", None);
assert_eq!(err.to_string(), "Too many requests");
let err = MiddlewareError::invalid_request("Malformed params");
assert_eq!(err.to_string(), "Invalid request: Malformed params");
let err = MiddlewareError::internal("Database connection failed");
assert_eq!(
err.to_string(),
"Internal middleware error: Database connection failed"
);
let err = MiddlewareError::custom("CUSTOM_ERROR", "Something went wrong");
assert_eq!(err.to_string(), "CUSTOM_ERROR: Something went wrong");
}
#[test]
fn every_returnable_variant_maps_to_a_response_without_panicking() {
let id = turul_rpc::RequestId::Number(1);
let cases: Vec<(MiddlewareError, i64)> = vec![
(MiddlewareError::unauthenticated("no token"), -32001),
(MiddlewareError::unauthorized("wrong scope"), -32005),
(MiddlewareError::rate_limit("slow down", Some(60)), -32003),
(MiddlewareError::invalid_request("malformed"), -32600),
(MiddlewareError::internal("db down"), -32603),
(MiddlewareError::custom("APP_CODE", "boom"), -32603),
];
for (err, expected) in cases {
let label = err.to_string();
let response = map_middleware_error_to_jsonrpc(err, id.clone());
let turul_rpc::JsonRpcResponse::Error(e) = response else {
panic!("{label} must map to an error response");
};
assert_eq!(e.error.code, expected, "{label} must answer {expected}");
}
}
#[test]
fn rate_limit_carries_retry_after_but_only_when_given() {
let id = turul_rpc::RequestId::Number(1);
let with = map_middleware_error_to_jsonrpc(
MiddlewareError::rate_limit("slow down", Some(30)),
id.clone(),
);
let turul_rpc::JsonRpcResponse::Error(e) = with else {
panic!("expected an error response");
};
assert_eq!(
e.error.data.as_ref().and_then(|d| d.get("retryAfter")),
Some(&serde_json::json!(30))
);
let without =
map_middleware_error_to_jsonrpc(MiddlewareError::rate_limit("slow down", None), id);
let turul_rpc::JsonRpcResponse::Error(e) = without else {
panic!("expected an error response");
};
assert!(
e.error.data.is_none(),
"no retry_after means no data object: {:?}",
e.error.data
);
}
#[test]
fn middleware_codes_are_frozen_legacy_allocations() {
const FROZEN: [(&str, i64); 3] = [
("UNAUTHENTICATED", -32001),
("UNAUTHORIZED", -32005),
("RATE_LIMIT_EXCEEDED", -32003),
];
assert_eq!(error_codes::UNAUTHENTICATED, FROZEN[0].1);
assert_eq!(error_codes::UNAUTHORIZED, FROZEN[1].1);
assert_eq!(error_codes::RATE_LIMIT_EXCEEDED, FROZEN[2].1);
assert_ne!(
error_codes::UNAUTHORIZED,
-32002,
"UNAUTHORIZED must never regress to -32002 — 2026-07-28 forbids \
implementations of this version from emitting it, and it means \
resource-not-found to every conformant peer"
);
for (name, code) in FROZEN {
assert!(
!(-32099..=-32020).contains(&code),
"{name} emits {code}, inside the spec-reserved -32020..-32099 \
sub-range; implementations must not emit codes there that the \
specification does not define"
);
}
}
#[test]
fn no_source_file_emits_the_forbidden_resource_not_found_code() {
let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let mut offenders = Vec::new();
let mut stack = vec![src];
while let Some(dir) = stack.pop() {
for entry in std::fs::read_dir(&dir).expect("read src dir") {
let path = entry.expect("dir entry").path();
if path.is_dir() {
stack.push(path);
continue;
}
if path.extension().is_none_or(|e| e != "rs") {
continue;
}
if path.file_name().is_some_and(|f| f == "error.rs")
&& path.parent().is_some_and(|d| d.ends_with("middleware"))
{
continue;
}
let text = std::fs::read_to_string(&path).expect("read source");
for (n, line) in text.lines().enumerate() {
let code = line.trim_start();
if code.starts_with("//") {
continue;
}
if code.contains("-32002") {
offenders.push(format!("{}:{}: {}", path.display(), n + 1, code.trim()));
}
}
}
}
assert!(
offenders.is_empty(),
"2026-07-28 forbids implementations of this version from emitting \
-32002, which means resource-not-found to every conformant peer:\n{}",
offenders.join("\n")
);
}
#[test]
fn test_error_equality() {
let err1 = MiddlewareError::unauthenticated("test");
let err2 = MiddlewareError::unauthenticated("test");
assert_eq!(err1, err2);
let err3 = MiddlewareError::rate_limit("test", Some(60));
let err4 = MiddlewareError::rate_limit("test", Some(60));
assert_eq!(err3, err4);
}
#[test]
fn test_http_challenge_variant_display() {
let err = MiddlewareError::http_challenge(401, "Bearer realm=\"mcp\"");
assert_eq!(
err.to_string(),
"HTTP 401 WWW-Authenticate: Bearer realm=\"mcp\""
);
let err = MiddlewareError::http_challenge(403, "Bearer error=\"insufficient_scope\"");
assert_eq!(
err.to_string(),
"HTTP 403 WWW-Authenticate: Bearer error=\"insufficient_scope\""
);
}
#[test]
fn test_http_challenge_constructor() {
let err = MiddlewareError::http_challenge(401, "Bearer realm=\"mcp\"");
match &err {
MiddlewareError::HttpChallenge {
status,
www_authenticate,
body,
} => {
assert_eq!(*status, 401);
assert_eq!(www_authenticate, "Bearer realm=\"mcp\"");
assert!(body.is_none());
}
_ => panic!("Expected HttpChallenge variant"),
}
let err_with_body = MiddlewareError::http_challenge_with_body(
401,
"Bearer realm=\"mcp\"",
r#"{"error":"unauthorized"}"#,
);
match &err_with_body {
MiddlewareError::HttpChallenge {
status,
www_authenticate,
body,
} => {
assert_eq!(*status, 401);
assert_eq!(www_authenticate, "Bearer realm=\"mcp\"");
assert_eq!(body.as_deref(), Some(r#"{"error":"unauthorized"}"#));
}
_ => panic!("Expected HttpChallenge variant"),
}
}
#[test]
fn test_http_challenge_roundtrip_equality() {
let err1 = MiddlewareError::http_challenge(401, "Bearer realm=\"mcp\"");
let err2 = MiddlewareError::http_challenge(401, "Bearer realm=\"mcp\"");
assert_eq!(err1, err2);
let err3 = MiddlewareError::http_challenge(401, "Bearer realm=\"mcp\"");
let err4 = MiddlewareError::http_challenge(403, "Bearer realm=\"mcp\"");
assert_ne!(err3, err4);
}
}