mirror of
https://github.com/NetworkCats/rustyip.git
synced 2026-11-13 16:17:46 +00:00
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.
132 lines
3.6 KiB
Rust
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;
|
|
}
|
|
}
|