Implement ProxyError enum with plain text error responses and logging

This commit is contained in:
2026-06-11 13:10:32 +00:00
parent 2791070971
commit 23ed5cde27
2 changed files with 275 additions and 4 deletions

View File

@@ -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<Body> {
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");
}
}

View File

@@ -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]