use axum::{
extract::rejection::JsonRejection,
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use lora_database::{LoraError, LoraErrorCategory, LoraErrorCode};
use serde::Serialize;
#[derive(Debug, Serialize)]
pub struct ErrorResponse {
pub error: ErrorBody,
}
#[derive(Debug, Serialize)]
pub struct ErrorBody {
pub code: &'static str,
pub message: String,
pub category: &'static str,
}
impl ErrorResponse {
fn from_lora(err: &LoraError) -> Self {
Self {
error: ErrorBody {
code: err.code().as_str(),
message: err.public_message(),
category: err.category().as_str(),
},
}
}
pub(crate) fn from_parts(code: LoraErrorCode, message: impl Into<String>) -> Self {
Self {
error: ErrorBody {
code: code.as_str(),
message: message.into(),
category: code.category().as_str(),
},
}
}
}
pub(crate) fn json_rejection_error(rejection: JsonRejection) -> LoraError {
LoraError::new(
LoraErrorCode::InvalidParams,
format!("invalid JSON request body: {}", rejection.body_text()),
)
}
fn status_for(err: &LoraError) -> StatusCode {
match err.code() {
LoraErrorCode::WalPoisoned | LoraErrorCode::Connection => StatusCode::SERVICE_UNAVAILABLE,
LoraErrorCode::Io
| LoraErrorCode::WalCorruption
| LoraErrorCode::SnapshotCodec
| LoraErrorCode::SnapshotCrypto
| LoraErrorCode::Internal => StatusCode::INTERNAL_SERVER_ERROR,
LoraErrorCode::Timeout => StatusCode::REQUEST_TIMEOUT,
LoraErrorCode::NotFound => StatusCode::NOT_FOUND,
LoraErrorCode::ChangesTruncated => StatusCode::GONE,
LoraErrorCode::InvalidParams | LoraErrorCode::InvalidVector | LoraErrorCode::Validation => {
StatusCode::UNPROCESSABLE_ENTITY
}
LoraErrorCode::ConstraintViolation
| LoraErrorCode::Locked
| LoraErrorCode::ChangesLagged
| LoraErrorCode::UniqueConstraint
| LoraErrorCode::NotNullConstraint
| LoraErrorCode::ForeignKeyViolation
| LoraErrorCode::TransactionFailure => StatusCode::CONFLICT,
LoraErrorCode::Parse
| LoraErrorCode::Semantic
| LoraErrorCode::ReadOnlyViolation
| LoraErrorCode::DatabaseName
| LoraErrorCode::Config => StatusCode::BAD_REQUEST,
}
}
pub(crate) fn lora_error_response(err: impl Into<LoraError>) -> Response {
let lora = err.into();
let status = status_for(&lora);
match lora.category() {
LoraErrorCategory::Client => {
tracing::warn!(
code = lora.code().as_str(),
category = lora.category().as_str(),
public_message = %lora.public_message(),
diagnostic = %lora.debug_context(),
"database request failed"
);
}
LoraErrorCategory::Server => {
tracing::error!(
code = lora.code().as_str(),
category = lora.category().as_str(),
public_message = %lora.public_message(),
diagnostic = %lora.debug_context(),
"database request failed"
);
}
}
(status, Json(ErrorResponse::from_lora(&lora))).into_response()
}