Files
rustyip/src/error.rs
T
NetworkCats 5cda385928 fix: start server before DB download to prevent health check timeout
Decouple HTTP server startup from database initialization so the
/health endpoint responds immediately. On first deploy with an empty
volume, the DB download blocked the server from binding, causing
Docker health checks to time out after 60s.
2026-03-07 21:57:16 +08:00

132 lines
3.6 KiB
Rust

use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
pub enum AppError {
IpNotFound,
InvalidIp,
NonPublicIp,
MissingClientIp,
DbNotReady,
DbLookupFailed,
TemplateRenderFailed,
}
impl AppError {
pub fn status_code(&self) -> StatusCode {
match self {
Self::IpNotFound => StatusCode::NOT_FOUND,
Self::InvalidIp | Self::NonPublicIp => StatusCode::BAD_REQUEST,
Self::MissingClientIp => StatusCode::BAD_REQUEST,
Self::DbNotReady => StatusCode::SERVICE_UNAVAILABLE,
Self::DbLookupFailed => StatusCode::INTERNAL_SERVER_ERROR,
Self::TemplateRenderFailed => StatusCode::INTERNAL_SERVER_ERROR,
}
}
pub fn message(&self) -> &'static str {
match self {
Self::IpNotFound => "IP not found in database",
Self::InvalidIp => "Invalid IP address",
Self::NonPublicIp => "Only public IP addresses can be queried",
Self::MissingClientIp => "Missing client IP address",
Self::DbNotReady => "Service is starting up, please try again shortly",
Self::DbLookupFailed => "Database lookup failed",
Self::TemplateRenderFailed => "Template render failed",
}
}
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
(self.status_code(), self.message()).into_response()
}
}
#[cfg(test)]
mod tests {
use super::*;
use http_body_util::BodyExt;
async fn check_error_response(
error: AppError,
expected_status: StatusCode,
expected_body: &str,
) {
let response = error.into_response();
assert_eq!(response.status(), expected_status);
let body = response.into_body().collect().await.unwrap().to_bytes();
let text = String::from_utf8(body.to_vec()).unwrap();
assert_eq!(text, expected_body);
}
#[tokio::test]
async fn ip_not_found_returns_404() {
check_error_response(
AppError::IpNotFound,
StatusCode::NOT_FOUND,
"IP not found in database",
)
.await;
}
#[tokio::test]
async fn invalid_ip_returns_400() {
check_error_response(
AppError::InvalidIp,
StatusCode::BAD_REQUEST,
"Invalid IP address",
)
.await;
}
#[tokio::test]
async fn non_public_ip_returns_400() {
check_error_response(
AppError::NonPublicIp,
StatusCode::BAD_REQUEST,
"Only public IP addresses can be queried",
)
.await;
}
#[tokio::test]
async fn missing_client_ip_returns_400() {
check_error_response(
AppError::MissingClientIp,
StatusCode::BAD_REQUEST,
"Missing client IP address",
)
.await;
}
#[tokio::test]
async fn db_not_ready_returns_503() {
check_error_response(
AppError::DbNotReady,
StatusCode::SERVICE_UNAVAILABLE,
"Service is starting up, please try again shortly",
)
.await;
}
#[tokio::test]
async fn db_lookup_failed_returns_500() {
check_error_response(
AppError::DbLookupFailed,
StatusCode::INTERNAL_SERVER_ERROR,
"Database lookup failed",
)
.await;
}
#[tokio::test]
async fn template_render_failed_returns_500() {
check_error_response(
AppError::TemplateRenderFailed,
StatusCode::INTERNAL_SERVER_ERROR,
"Template render failed",
)
.await;
}
}