Implement ProxyError enum with plain text error responses and logging
This commit is contained in:
@@ -1,2 +1,260 @@
|
|||||||
#[allow(dead_code)]
|
use axum::http::StatusCode;
|
||||||
pub struct ProxyError;
|
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 arc_swap::ArcSwap;
|
||||||
|
|
||||||
use crate::config::dynamic_config::DynamicConfig;
|
use crate::config::dynamic_config::DynamicConfig;
|
||||||
|
use crate::proxy::error::ProxyError;
|
||||||
|
|
||||||
async fn health_handler() -> impl IntoResponse {
|
async fn health_handler() -> impl IntoResponse {
|
||||||
StatusCode::OK
|
StatusCode::OK
|
||||||
@@ -29,13 +30,13 @@ async fn proxy_handler(
|
|||||||
|
|
||||||
let host = match host {
|
let host = match host {
|
||||||
Some(h) => h,
|
Some(h) => h,
|
||||||
None => return StatusCode::BAD_REQUEST.into_response(),
|
None => return ProxyError::BadRequest.into_response(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let config = state.load();
|
let config = state.load();
|
||||||
match config.lookup(host) {
|
match config.lookup(host) {
|
||||||
Some(_site) => StatusCode::OK.into_response(),
|
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;
|
let resp = send_request(&mut router, "GET", "/some/path", None).await;
|
||||||
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
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]
|
#[tokio::test]
|
||||||
@@ -140,6 +147,12 @@ mod tests {
|
|||||||
|
|
||||||
let resp = send_request(&mut router, "GET", "/some/path", Some("unknown.host")).await;
|
let resp = send_request(&mut router, "GET", "/some/path", Some("unknown.host")).await;
|
||||||
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
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]
|
#[tokio::test]
|
||||||
|
|||||||
Reference in New Issue
Block a user