use zenith_api::CanonicalResponse;
use crate::server::ServerError;
#[derive(Debug)]
pub enum WebError {
BadRequest(String),
Unauthorized(String),
Forbidden(String),
NotFound(String),
MethodNotAllowed(String),
Conflict(String),
UnprocessableEntity(String),
TooManyRequests(String),
InternalError(String),
Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
NotImplemented(String),
ServiceUnavailable(String),
Custom {
status: u16,
message: String,
},
}
impl WebError {
pub fn status_code(&self) -> u16 {
match self {
WebError::BadRequest(_) => 400,
WebError::Unauthorized(_) => 401,
WebError::Forbidden(_) => 403,
WebError::NotFound(_) => 404,
WebError::MethodNotAllowed(_) => 405,
WebError::Conflict(_) => 409,
WebError::UnprocessableEntity(_) => 422,
WebError::TooManyRequests(_) => 429,
WebError::InternalError(_) => 500,
WebError::Internal(_) => 500,
WebError::NotImplemented(_) => 501,
WebError::ServiceUnavailable(_) => 503,
WebError::Custom { status, .. } => *status,
}
}
pub fn message(&self) -> &str {
match self {
WebError::BadRequest(msg) => msg,
WebError::Unauthorized(msg) => msg,
WebError::Forbidden(msg) => msg,
WebError::NotFound(msg) => msg,
WebError::MethodNotAllowed(msg) => msg,
WebError::Conflict(msg) => msg,
WebError::UnprocessableEntity(msg) => msg,
WebError::TooManyRequests(msg) => msg,
WebError::InternalError(msg) => msg,
WebError::Internal(_) => "internal server error",
WebError::NotImplemented(msg) => msg,
WebError::ServiceUnavailable(msg) => msg,
WebError::Custom { message, .. } => message,
}
}
pub fn into_response(self) -> CanonicalResponse {
let status = self.status_code();
let message = self.message().to_string();
let mut response = CanonicalResponse::new(status);
let _ = response
.add_header(b"content-type", b"application/json");
let escaped = escape_json_string(&message);
let body = format!(
r#"{{"error":true,"status":{},"message":"{}"}}"#,
status, escaped
);
response.set_body(body.into_bytes());
response
}
}
impl std::fmt::Display for WebError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Error {}: {}", self.status_code(), self.message())
}
}
impl std::error::Error for WebError {}
impl From<WebError> for CanonicalResponse {
fn from(err: WebError) -> Self {
err.into_response()
}
}
impl From<String> for WebError {
fn from(msg: String) -> Self {
WebError::InternalError(msg)
}
}
impl From<&str> for WebError {
fn from(msg: &str) -> Self {
WebError::InternalError(msg.to_string())
}
}
impl From<ServerError> for WebError {
fn from(err: ServerError) -> Self {
match err {
ServerError::Normalize(e) => WebError::BadRequest(e.to_string()),
ServerError::Http1(e) => WebError::BadRequest(e.to_string()),
ServerError::Protocol(m) => WebError::BadRequest(m),
ServerError::ConnectionClosed => {
WebError::BadRequest("connection closed before complete request".to_string())
}
ServerError::Accept(e) => WebError::ServiceUnavailable(e.to_string()),
ServerError::Timeout => {
WebError::ServiceUnavailable("operation timed out".to_string())
}
other => WebError::Internal(Box::new(other)),
}
}
}
#[derive(Debug, Clone)]
pub enum RouterError {
NotFound(String),
MethodNotAllowed(String),
Conflict(String),
Internal(String),
}
impl std::fmt::Display for RouterError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RouterError::NotFound(msg) => write!(f, "Route not found: {}", msg),
RouterError::MethodNotAllowed(msg) => write!(f, "Method not allowed: {}", msg),
RouterError::Conflict(msg) => write!(f, "Route conflict: {}", msg),
RouterError::Internal(msg) => write!(f, "Router internal error: {}", msg),
}
}
}
impl std::error::Error for RouterError {}
impl From<RouterError> for WebError {
fn from(err: RouterError) -> Self {
match err {
RouterError::NotFound(msg) => WebError::NotFound(msg),
RouterError::MethodNotAllowed(msg) => WebError::MethodNotAllowed(msg),
RouterError::Conflict(msg) => WebError::Conflict(msg),
RouterError::Internal(msg) => WebError::InternalError(msg),
}
}
}
impl From<RouterError> for CanonicalResponse {
fn from(err: RouterError) -> Self {
WebError::from(err).into_response()
}
}
#[derive(Debug, Clone)]
pub enum MiddlewareError {
ShortCircuit(Box<CanonicalResponse>),
Internal(String),
Unauthorized(String),
Forbidden(String),
Misdirected(String),
}
impl std::fmt::Display for MiddlewareError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MiddlewareError::ShortCircuit(_) => write!(f, "Middleware short circuit"),
MiddlewareError::Internal(msg) => write!(f, "Middleware internal error: {}", msg),
MiddlewareError::Unauthorized(msg) => write!(f, "Unauthorized: {}", msg),
MiddlewareError::Forbidden(msg) => write!(f, "Forbidden: {}", msg),
MiddlewareError::Misdirected(msg) => write!(f, "Misdirected Request: {}", msg),
}
}
}
impl std::error::Error for MiddlewareError {}
impl From<MiddlewareError> for WebError {
fn from(err: MiddlewareError) -> Self {
match err {
MiddlewareError::ShortCircuit(resp) => {
let status = resp.status_code;
let body = String::from_utf8_lossy(resp.body()).to_string();
WebError::Custom { status, message: body }
}
MiddlewareError::Internal(msg) => WebError::InternalError(msg),
MiddlewareError::Unauthorized(msg) => WebError::Unauthorized(msg),
MiddlewareError::Forbidden(msg) => WebError::Forbidden(msg),
MiddlewareError::Misdirected(msg) => WebError::Custom {
status: 421,
message: msg,
},
}
}
}
impl From<MiddlewareError> for CanonicalResponse {
fn from(err: MiddlewareError) -> Self {
match err {
MiddlewareError::ShortCircuit(resp) => *resp,
other => WebError::from(other).into_response(),
}
}
}
pub(crate) fn escape_json_string(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => {
out.push_str(&format!("\\u{:04x}", c as u32));
}
c => out.push(c),
}
}
out
}
pub fn catch_panic<F>(f: F) -> CanonicalResponse
where
F: FnOnce() -> CanonicalResponse,
{
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
match result {
Ok(response) => response,
Err(_) => WebError::InternalError("Internal server error (panic recovered)".to_string())
.into_response(),
}
}
pub fn bad_request(msg: impl Into<String>) -> WebError {
WebError::BadRequest(msg.into())
}
pub fn unauthorized(msg: impl Into<String>) -> WebError {
WebError::Unauthorized(msg.into())
}
pub fn forbidden(msg: impl Into<String>) -> WebError {
WebError::Forbidden(msg.into())
}
pub fn not_found(msg: impl Into<String>) -> WebError {
WebError::NotFound(msg.into())
}
pub fn method_not_allowed(msg: impl Into<String>) -> WebError {
WebError::MethodNotAllowed(msg.into())
}
pub fn conflict(msg: impl Into<String>) -> WebError {
WebError::Conflict(msg.into())
}
pub fn unprocessable(msg: impl Into<String>) -> WebError {
WebError::UnprocessableEntity(msg.into())
}
pub fn too_many_requests(msg: impl Into<String>) -> WebError {
WebError::TooManyRequests(msg.into())
}
pub fn internal_error(msg: impl Into<String>) -> WebError {
WebError::InternalError(msg.into())
}
pub fn not_implemented(msg: impl Into<String>) -> WebError {
WebError::NotImplemented(msg.into())
}
pub fn service_unavailable(msg: impl Into<String>) -> WebError {
WebError::ServiceUnavailable(msg.into())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_web_error_status_codes() {
assert_eq!(WebError::BadRequest("err".into()).status_code(), 400);
assert_eq!(WebError::Unauthorized("err".into()).status_code(), 401);
assert_eq!(WebError::Forbidden("err".into()).status_code(), 403);
assert_eq!(WebError::NotFound("err".into()).status_code(), 404);
assert_eq!(WebError::MethodNotAllowed("err".into()).status_code(), 405);
assert_eq!(WebError::Conflict("err".into()).status_code(), 409);
assert_eq!(WebError::UnprocessableEntity("err".into()).status_code(), 422);
assert_eq!(WebError::TooManyRequests("err".into()).status_code(), 429);
assert_eq!(WebError::InternalError("err".into()).status_code(), 500);
assert_eq!(WebError::NotImplemented("err".into()).status_code(), 501);
assert_eq!(WebError::ServiceUnavailable("err".into()).status_code(), 503);
}
#[test]
fn test_custom_error() {
let err = WebError::Custom {
status: 418,
message: "I'm a teapot".to_string(),
};
assert_eq!(err.status_code(), 418);
assert_eq!(err.message(), "I'm a teapot");
}
#[test]
fn test_error_to_response() {
let err = WebError::NotFound("User not found".to_string());
let response = err.into_response();
assert_eq!(response.status_code, 404);
assert!(response.find_header("content-type").is_some());
let body = response.body();
let body_str = String::from_utf8_lossy(body);
assert!(body_str.contains("User not found"));
assert!(body_str.contains("404"));
}
#[test]
fn test_error_display() {
let err = WebError::BadRequest("Invalid input".to_string());
let display = format!("{}", err);
assert!(display.contains("400"));
assert!(display.contains("Invalid input"));
}
#[test]
fn test_error_conversion() {
let response: CanonicalResponse = WebError::InternalError("oops".to_string()).into();
assert_eq!(response.status_code, 500);
let err: WebError = "simple error".into();
assert_eq!(err.status_code(), 500);
}
#[test]
fn test_panic_recovery() {
let response = catch_panic(|| {
panic!("test panic");
});
assert_eq!(response.status_code, 500);
let body = response.body();
let body_str = String::from_utf8_lossy(body);
assert!(body_str.contains("panic recovered"));
}
#[test]
fn test_panic_no_panic() {
let response = catch_panic(|| CanonicalResponse::new(200));
assert_eq!(response.status_code, 200);
}
#[test]
fn test_helpers() {
let _ = bad_request("test");
let _ = unauthorized("test");
let _ = forbidden("test");
let _ = not_found("test");
let _ = method_not_allowed("test");
let _ = conflict("test");
let _ = unprocessable("test");
let _ = too_many_requests("test");
let _ = internal_error("test");
let _ = not_implemented("test");
let _ = service_unavailable("test");
}
#[test]
fn test_from_server_error_protocol_is_bad_request() {
let err: WebError = ServerError::Protocol("bad framing".into()).into();
assert_eq!(err.status_code(), 400);
assert_eq!(err.message(), "bad framing");
}
#[test]
fn test_from_server_error_http2_is_internal() {
let err: WebError = ServerError::Http2(
zenith_http2::error::Http2Error::ProtocolError("conn reset".into()),
)
.into();
assert_eq!(err.status_code(), 500);
assert_eq!(err.message(), "internal server error");
}
#[test]
fn test_from_server_error_timeout_is_service_unavailable() {
let err: WebError = ServerError::Timeout.into();
assert_eq!(err.status_code(), 503);
}
#[test]
fn test_internal_variant_does_not_leak_details() {
let err = WebError::Internal(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
"secret internal path",
)));
assert_eq!(err.status_code(), 500);
assert_eq!(err.message(), "internal server error");
let body = String::from_utf8_lossy(err.into_response().body()).to_string();
assert!(!body.contains("secret"), "不应向客户端泄露内部错误细节");
}
}