Implement ProxyError enum with plain text error responses and logging
This commit is contained in:
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user