use axum::response::{IntoResponse, Response};
use axum::http::StatusCode as ReqwestStatusCode;
use sl_map_apis::map_tiles::{MapError, MapTileCacheError};
use sl_map_apis::region::{CacheError as RegionCacheError, USBNotecardToGridRectangleError};
use sl_types::map::{LocationParseError, USBNotecardLoadError};
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("invalid request: {0}")]
BadRequest(String),
#[error("job not found")]
JobNotFound,
#[error("requested resource not found: {0}")]
NotFound(String),
#[error("job not finished yet")]
JobNotFinished,
#[error("render failed: {0}")]
RenderFailed(String),
#[error("multipart error: {0}")]
Multipart(#[from] axum::extract::multipart::MultipartError),
#[error("could not parse USB notecard: {0}")]
USBNotecardParse(#[from] LocationParseError),
#[error("could not load USB notecard: {0}")]
USBNotecardLoad(#[from] USBNotecardLoadError),
#[error("region cache error: {0}")]
RegionCache(Box<RegionCacheError>),
#[error("USB notecard rectangle resolution error: {0}")]
USBNotecardToGridRectangle(Box<USBNotecardToGridRectangleError>),
#[error("map render error: {0}")]
Map(Box<MapError>),
#[error("map tile cache error: {0}")]
MapTileCache(Box<MapTileCacheError>),
#[error("image encoding error: {0}")]
Image(Box<image::ImageError>),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("unauthenticated")]
Unauthenticated,
#[error("forbidden: {0}")]
Forbidden(String),
#[error("invalid credentials")]
InvalidCredentials,
#[error("invalid or expired token")]
InvalidOrExpiredToken,
#[error("database error")]
Database,
#[error("password hash error: {0}")]
PasswordHash(String),
#[error("rate limit exceeded")]
TooManyRequests {
retry_after_secs: u64,
},
#[error("GLW event cache error: {0}")]
GlwEventCache(#[from] sl_glw::GlwEventCacheError),
#[error("font parse error: {0}")]
FontParse(#[from] ab_glyph::InvalidFont),
}
impl From<MapError> for Error {
fn from(value: MapError) -> Self {
Self::Map(Box::new(value))
}
}
impl From<sl_map_apis::text::FontError> for Error {
fn from(value: sl_map_apis::text::FontError) -> Self {
match value {
sl_map_apis::text::FontError::Read { source, .. } => Self::Io(source),
sl_map_apis::text::FontError::Parse(invalid) => Self::FontParse(invalid),
}
}
}
impl From<MapTileCacheError> for Error {
fn from(value: MapTileCacheError) -> Self {
Self::MapTileCache(Box::new(value))
}
}
impl From<RegionCacheError> for Error {
fn from(value: RegionCacheError) -> Self {
Self::RegionCache(Box::new(value))
}
}
impl From<USBNotecardToGridRectangleError> for Error {
fn from(value: USBNotecardToGridRectangleError) -> Self {
Self::USBNotecardToGridRectangle(Box::new(value))
}
}
impl From<image::ImageError> for Error {
fn from(value: image::ImageError) -> Self {
Self::Image(Box::new(value))
}
}
#[must_use]
pub fn is_fk_violation(err: &sqlx::Error) -> bool {
let sqlx::Error::Database(db) = err else {
return false;
};
if matches!(db.code().as_deref(), Some("787" | "1811")) {
return true;
}
db.message().contains("FOREIGN KEY")
}
impl IntoResponse for Error {
fn into_response(self) -> Response {
let status = match &self {
Self::BadRequest(_)
| Self::Multipart(_)
| Self::USBNotecardParse(_)
| Self::USBNotecardLoad(_)
| Self::InvalidOrExpiredToken
| Self::FontParse(_) => ReqwestStatusCode::BAD_REQUEST,
Self::Unauthenticated | Self::InvalidCredentials => ReqwestStatusCode::UNAUTHORIZED,
Self::Forbidden(_) => ReqwestStatusCode::FORBIDDEN,
Self::JobNotFound | Self::NotFound(_) => ReqwestStatusCode::NOT_FOUND,
Self::JobNotFinished => ReqwestStatusCode::ACCEPTED,
Self::RenderFailed(_) => ReqwestStatusCode::CONFLICT,
Self::TooManyRequests { .. } => ReqwestStatusCode::TOO_MANY_REQUESTS,
Self::USBNotecardToGridRectangle(_)
| Self::RegionCache(_)
| Self::Map(_)
| Self::MapTileCache(_)
| Self::Image(_)
| Self::Io(_)
| Self::Json(_)
| Self::Database
| Self::PasswordHash(_)
| Self::GlwEventCache(_) => ReqwestStatusCode::INTERNAL_SERVER_ERROR,
};
let body_text = match &self {
Self::Io(_)
| Self::Image(_)
| Self::Json(_)
| Self::Map(_)
| Self::MapTileCache(_)
| Self::RegionCache(_)
| Self::USBNotecardToGridRectangle(_)
| Self::PasswordHash(_) => "internal error".to_owned(),
_ => format!("{self}"),
};
let retry_after = match &self {
Self::TooManyRequests { retry_after_secs } => Some(*retry_after_secs),
_ => None,
};
let display = self.to_string();
let safe: String = display.chars().flat_map(char::escape_debug).collect();
if status.is_server_error() {
tracing::warn!("request failed: {safe}");
} else {
tracing::debug!("request failed: {safe}");
}
let body = serde_json::json!({ "error": body_text }).to_string();
let mut response = (status, body).into_response();
if let Ok(value) = axum::http::HeaderValue::from_str("application/json; charset=utf-8") {
drop(
response
.headers_mut()
.insert(axum::http::header::CONTENT_TYPE, value),
);
}
if let Some(secs) = retry_after
&& let Ok(value) = axum::http::HeaderValue::from_str(&secs.to_string())
{
drop(
response
.headers_mut()
.insert(axum::http::header::RETRY_AFTER, value),
);
}
response
}
}