use crate::sql::SqlError;
use crate::supabase::jwt::JwtError;
use axum::Json;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use serde_json::json;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SupaError {
MissingApiKey,
InvalidApiKey,
InvalidJwt(JwtError),
Forbidden(&'static str),
NotImplemented(&'static str),
UnsupportedFilter(String),
BadRequest(String),
AuthProviderUnsupported(String),
Sql(SqlError),
Internal(String),
}
impl SupaError {
pub fn status(&self) -> StatusCode {
match self {
SupaError::MissingApiKey | SupaError::InvalidApiKey => StatusCode::UNAUTHORIZED,
SupaError::InvalidJwt(_) => StatusCode::UNAUTHORIZED,
SupaError::Forbidden(_) => StatusCode::FORBIDDEN,
SupaError::NotImplemented(_) => StatusCode::NOT_IMPLEMENTED,
SupaError::UnsupportedFilter(_) | SupaError::BadRequest(_) => StatusCode::BAD_REQUEST,
SupaError::AuthProviderUnsupported(_) => StatusCode::BAD_REQUEST,
SupaError::Sql(e) => status_for_sqlstate(e.sqlstate()),
SupaError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
pub fn code(&self) -> String {
match self {
SupaError::MissingApiKey => "SUPA_COMPAT_MISSING_API_KEY".to_string(),
SupaError::InvalidApiKey => "SUPA_COMPAT_INVALID_API_KEY".to_string(),
SupaError::InvalidJwt(_) => "SUPA_COMPAT_INVALID_JWT".to_string(),
SupaError::Forbidden(_) => "SUPA_COMPAT_FORBIDDEN".to_string(),
SupaError::NotImplemented(svc) => format!("SUPA_COMPAT_{svc}_NOT_IMPLEMENTED"),
SupaError::UnsupportedFilter(_) => "SUPA_COMPAT_REST_UNSUPPORTED_FILTER".to_string(),
SupaError::BadRequest(_) => "SUPA_COMPAT_REST_BAD_REQUEST".to_string(),
SupaError::AuthProviderUnsupported(_) => {
"SUPA_COMPAT_AUTH_PROVIDER_UNSUPPORTED".to_string()
}
SupaError::Sql(e) => e.sqlstate().to_string(),
SupaError::Internal(_) => "SUPA_COMPAT_INTERNAL".to_string(),
}
}
pub fn message(&self) -> String {
match self {
SupaError::MissingApiKey => {
"No API key found in request. Pass the project apikey header.".to_string()
}
SupaError::InvalidApiKey => "Invalid API key".to_string(),
SupaError::InvalidJwt(e) => format!("Invalid authentication credentials: {e}"),
SupaError::Forbidden(what) => {
format!("{what} requires the service_role key")
}
SupaError::NotImplemented(svc) => format!(
"the {} service is not implemented in this GuardianDB compatibility slice",
svc.to_ascii_lowercase()
),
SupaError::UnsupportedFilter(f) => {
format!("unsupported PostgREST filter operator: {f}")
}
SupaError::BadRequest(m) => m.clone(),
SupaError::AuthProviderUnsupported(p) => {
format!("auth provider \"{p}\" is not supported")
}
SupaError::Sql(e) => e.to_string(),
SupaError::Internal(m) => m.clone(),
}
}
fn hint(&self) -> Option<String> {
match self {
SupaError::NotImplemented(_) => Some("tracked for a later slice".to_string()),
SupaError::UnsupportedFilter(_) => Some(
"supported operators: eq, neq, gt, gte, lt, lte, like, ilike, is, in".to_string(),
),
SupaError::AuthProviderUnsupported(_) => {
Some("only email/password auth is implemented in this slice".to_string())
}
_ => None,
}
}
}
impl std::fmt::Display for SupaError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.code(), self.message())
}
}
impl std::error::Error for SupaError {}
impl IntoResponse for SupaError {
fn into_response(self) -> Response {
if let SupaError::Sql(e) = &self {
return pgrst_error(e);
}
let mut body = json!({
"code": self.code(),
"message": self.message(),
});
if let Some(hint) = self.hint() {
body["hint"] = json!(hint);
}
(self.status(), Json(body)).into_response()
}
}
pub fn pgrst_error(err: &SqlError) -> Response {
let sqlstate = err.sqlstate();
let status = status_for_sqlstate(sqlstate);
let body = json!({
"code": sqlstate,
"message": err.to_string(),
"details": Option::<String>::None,
"hint": Option::<String>::None,
});
(status, Json(body)).into_response()
}
pub fn status_for_sqlstate(sqlstate: &str) -> StatusCode {
match sqlstate {
"23505" => StatusCode::CONFLICT, "23502" | "23514" => StatusCode::BAD_REQUEST, "23503" => StatusCode::CONFLICT, "42P01" | "42703" | "42883" | "42704" | "3F000" => StatusCode::NOT_FOUND,
"42P07" | "42P06" | "42710" => StatusCode::CONFLICT,
"42501" => StatusCode::FORBIDDEN,
s if s.starts_with("42") || s.starts_with("22") => StatusCode::BAD_REQUEST,
"0A000" => StatusCode::NOT_IMPLEMENTED,
"55P03" | "40P01" | "25P02" => StatusCode::CONFLICT,
_ => StatusCode::INTERNAL_SERVER_ERROR,
}
}
pub fn gotrue_error(status: StatusCode, error_code: &str, msg: &str) -> Response {
let body = json!({
"code": status.as_u16(),
"error_code": error_code,
"msg": msg,
});
(status, Json(body)).into_response()
}
pub fn gotrue_oauth_error(status: StatusCode, error: &str, description: &str) -> Response {
let body = json!({
"error": error,
"error_description": description,
});
(status, Json(body)).into_response()
}
#[cfg(test)]
mod tests {
use super::*;
use axum::body::to_bytes;
async fn body_json(resp: Response) -> (StatusCode, serde_json::Value) {
let status = resp.status();
let bytes = to_bytes(resp.into_body(), usize::MAX).await.unwrap();
(status, serde_json::from_slice(&bytes).unwrap())
}
#[tokio::test]
async fn invalid_api_key_shape() {
let (status, body) = body_json(SupaError::InvalidApiKey.into_response()).await;
assert_eq!(status, StatusCode::UNAUTHORIZED);
assert_eq!(body["code"], "SUPA_COMPAT_INVALID_API_KEY");
assert!(body["message"].is_string());
}
#[tokio::test]
async fn not_implemented_shape() {
let (status, body) = body_json(SupaError::NotImplemented("STORAGE").into_response()).await;
assert_eq!(status, StatusCode::NOT_IMPLEMENTED);
assert_eq!(body["code"], "SUPA_COMPAT_STORAGE_NOT_IMPLEMENTED");
assert_eq!(body["hint"], "tracked for a later slice");
}
#[tokio::test]
async fn unsupported_filter_is_400() {
let (status, body) =
body_json(SupaError::UnsupportedFilter("cs".into()).into_response()).await;
assert_eq!(status, StatusCode::BAD_REQUEST);
assert_eq!(body["code"], "SUPA_COMPAT_REST_UNSUPPORTED_FILTER");
}
#[test]
fn sqlstate_status_mapping() {
assert_eq!(status_for_sqlstate("23505"), StatusCode::CONFLICT);
assert_eq!(status_for_sqlstate("42P01"), StatusCode::NOT_FOUND);
assert_eq!(status_for_sqlstate("42601"), StatusCode::BAD_REQUEST);
assert_eq!(status_for_sqlstate("22P02"), StatusCode::BAD_REQUEST);
assert_eq!(status_for_sqlstate("0A000"), StatusCode::NOT_IMPLEMENTED);
}
}