From 23ed5cde27df7990b9170bd3d87cafdf95e18747 Mon Sep 17 00:00:00 2001 From: "glm-5.1" Date: Thu, 11 Jun 2026 13:10:32 +0000 Subject: [PATCH] Implement ProxyError enum with plain text error responses and logging --- src/proxy/error.rs | 262 ++++++++++++++++++++++++++++++++++++++++++- src/proxy/handler.rs | 17 ++- 2 files changed, 275 insertions(+), 4 deletions(-) diff --git a/src/proxy/error.rs b/src/proxy/error.rs index 46f8087..08b0786 100644 --- a/src/proxy/error.rs +++ b/src/proxy/error.rs @@ -1,2 +1,260 @@ -#[allow(dead_code)] -pub struct ProxyError; +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; + +#[derive(Debug, thiserror::Error)] +pub enum ProxyError { + #[error("Bad Gateway")] + BadGateway { host: String, upstream: String }, + #[error("Gateway Timeout")] + GatewayTimeout { host: String, upstream: String }, + #[error("Payload Too Large")] + PayloadTooLarge, + #[error("Too Many Requests")] + TooManyRequests { + client_ip: String, + host: String, + path: String, + }, + #[error("Not Found")] + NotFound, + #[error("Bad Request")] + BadRequest, +} + +impl ProxyError { + fn status_code(&self) -> StatusCode { + match self { + Self::BadGateway { .. } => StatusCode::BAD_GATEWAY, + Self::GatewayTimeout { .. } => StatusCode::GATEWAY_TIMEOUT, + Self::PayloadTooLarge => StatusCode::PAYLOAD_TOO_LARGE, + Self::TooManyRequests { .. } => StatusCode::TOO_MANY_REQUESTS, + Self::NotFound => StatusCode::NOT_FOUND, + Self::BadRequest => StatusCode::BAD_REQUEST, + } + } + + fn body(&self) -> &'static str { + match self { + Self::BadGateway { .. } => "Bad Gateway", + Self::GatewayTimeout { .. } => "Gateway Timeout", + Self::PayloadTooLarge => "Payload Too Large", + Self::TooManyRequests { .. } => "Too Many Requests", + Self::NotFound => "Not Found", + Self::BadRequest => "Bad Request", + } + } +} + +impl IntoResponse for ProxyError { + fn into_response(self) -> Response { + match &self { + Self::BadGateway { host, upstream } => { + tracing::warn!( + host = %host, + upstream = %upstream, + status = 502, + "Bad Gateway" + ); + } + Self::GatewayTimeout { host, upstream } => { + tracing::warn!( + host = %host, + upstream = %upstream, + status = 504, + "Gateway Timeout" + ); + } + Self::TooManyRequests { + client_ip, + host, + path, + } => { + tracing::info!( + "RATE_LIMIT client_ip={} host={} path={} status=429", + client_ip, + host, + path + ); + } + _ => {} + } + + ( + [( + axum::http::header::CONTENT_TYPE, + "text/plain; charset=utf-8", + )], + (self.status_code(), self.body()), + ) + .into_response() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use axum::body::Body; + use axum::http::{Response, StatusCode}; + + fn into_response(error: ProxyError) -> Response { + let _guard = tracing_subscriber::fmt() + .with_max_level(tracing::Level::TRACE) + .with_test_writer() + .try_init(); + error.into_response() + } + + #[tokio::test] + async fn bad_gateway_response() { + let resp = into_response(ProxyError::BadGateway { + host: "example.com".to_string(), + upstream: "127.0.0.1:8080".to_string(), + }); + assert_eq!(resp.status(), StatusCode::BAD_GATEWAY); + let body = axum::body::to_bytes(resp.into_body(), 1024).await.unwrap(); + assert_eq!(&body[..], b"Bad Gateway"); + } + + #[tokio::test] + async fn bad_gateway_content_type() { + let resp = into_response(ProxyError::BadGateway { + host: "example.com".to_string(), + upstream: "127.0.0.1:8080".to_string(), + }); + assert_eq!( + resp.headers().get("content-type").unwrap(), + "text/plain; charset=utf-8" + ); + } + + #[tokio::test] + async fn gateway_timeout_response() { + let resp = into_response(ProxyError::GatewayTimeout { + host: "example.com".to_string(), + upstream: "127.0.0.1:8080".to_string(), + }); + assert_eq!(resp.status(), StatusCode::GATEWAY_TIMEOUT); + let body = axum::body::to_bytes(resp.into_body(), 1024).await.unwrap(); + assert_eq!(&body[..], b"Gateway Timeout"); + } + + #[tokio::test] + async fn gateway_timeout_content_type() { + let resp = into_response(ProxyError::GatewayTimeout { + host: "example.com".to_string(), + upstream: "127.0.0.1:8080".to_string(), + }); + assert_eq!( + resp.headers().get("content-type").unwrap(), + "text/plain; charset=utf-8" + ); + } + + #[tokio::test] + async fn payload_too_large_response() { + let resp = into_response(ProxyError::PayloadTooLarge); + assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE); + let body = axum::body::to_bytes(resp.into_body(), 1024).await.unwrap(); + assert_eq!(&body[..], b"Payload Too Large"); + } + + #[tokio::test] + async fn payload_too_large_content_type() { + let resp = into_response(ProxyError::PayloadTooLarge); + assert_eq!( + resp.headers().get("content-type").unwrap(), + "text/plain; charset=utf-8" + ); + } + + #[tokio::test] + async fn too_many_requests_response() { + let resp = into_response(ProxyError::TooManyRequests { + client_ip: "192.168.1.1".to_string(), + host: "example.com".to_string(), + path: "/api".to_string(), + }); + assert_eq!(resp.status(), StatusCode::TOO_MANY_REQUESTS); + let body = axum::body::to_bytes(resp.into_body(), 1024).await.unwrap(); + assert_eq!(&body[..], b"Too Many Requests"); + } + + #[tokio::test] + async fn too_many_requests_content_type() { + let resp = into_response(ProxyError::TooManyRequests { + client_ip: "192.168.1.1".to_string(), + host: "example.com".to_string(), + path: "/api".to_string(), + }); + assert_eq!( + resp.headers().get("content-type").unwrap(), + "text/plain; charset=utf-8" + ); + } + + #[tokio::test] + async fn not_found_response() { + let resp = into_response(ProxyError::NotFound); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + let body = axum::body::to_bytes(resp.into_body(), 1024).await.unwrap(); + assert_eq!(&body[..], b"Not Found"); + } + + #[tokio::test] + async fn not_found_content_type() { + let resp = into_response(ProxyError::NotFound); + assert_eq!( + resp.headers().get("content-type").unwrap(), + "text/plain; charset=utf-8" + ); + } + + #[tokio::test] + async fn bad_request_response() { + let resp = into_response(ProxyError::BadRequest); + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + let body = axum::body::to_bytes(resp.into_body(), 1024).await.unwrap(); + assert_eq!(&body[..], b"Bad Request"); + } + + #[tokio::test] + async fn bad_request_content_type() { + let resp = into_response(ProxyError::BadRequest); + assert_eq!( + resp.headers().get("content-type").unwrap(), + "text/plain; charset=utf-8" + ); + } + + #[test] + fn error_display_matches_body() { + assert_eq!( + ProxyError::BadGateway { + host: String::new(), + upstream: String::new() + } + .to_string(), + "Bad Gateway" + ); + assert_eq!( + ProxyError::GatewayTimeout { + host: String::new(), + upstream: String::new() + } + .to_string(), + "Gateway Timeout" + ); + assert_eq!(ProxyError::PayloadTooLarge.to_string(), "Payload Too Large"); + assert_eq!( + ProxyError::TooManyRequests { + client_ip: String::new(), + host: String::new(), + path: String::new() + } + .to_string(), + "Too Many Requests" + ); + assert_eq!(ProxyError::NotFound.to_string(), "Not Found"); + assert_eq!(ProxyError::BadRequest.to_string(), "Bad Request"); + } +} diff --git a/src/proxy/handler.rs b/src/proxy/handler.rs index 81d09fa..110ef84 100644 --- a/src/proxy/handler.rs +++ b/src/proxy/handler.rs @@ -9,6 +9,7 @@ use axum::Router; use arc_swap::ArcSwap; use crate::config::dynamic_config::DynamicConfig; +use crate::proxy::error::ProxyError; async fn health_handler() -> impl IntoResponse { StatusCode::OK @@ -29,13 +30,13 @@ async fn proxy_handler( let host = match host { Some(h) => h, - None => return StatusCode::BAD_REQUEST.into_response(), + None => return ProxyError::BadRequest.into_response(), }; let config = state.load(); match config.lookup(host) { Some(_site) => StatusCode::OK.into_response(), - None => StatusCode::NOT_FOUND.into_response(), + None => ProxyError::NotFound.into_response(), } } @@ -125,6 +126,12 @@ mod tests { let resp = send_request(&mut router, "GET", "/some/path", None).await; assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + assert_eq!( + resp.headers().get("content-type").unwrap(), + "text/plain; charset=utf-8" + ); + let body = axum::body::to_bytes(resp.into_body(), 1024).await.unwrap(); + assert_eq!(&body[..], b"Bad Request"); } #[tokio::test] @@ -140,6 +147,12 @@ mod tests { let resp = send_request(&mut router, "GET", "/some/path", Some("unknown.host")).await; assert_eq!(resp.status(), StatusCode::NOT_FOUND); + assert_eq!( + resp.headers().get("content-type").unwrap(), + "text/plain; charset=utf-8" + ); + let body = axum::body::to_bytes(resp.into_body(), 1024).await.unwrap(); + assert_eq!(&body[..], b"Not Found"); } #[tokio::test]