use crate::api::storage::errors::STORAGE_PUBLIC_ERROR_CODES;
use athena_gateway::GATEWAY_PUBLIC_ERROR_CODES;
use athena_webhooks::WEBHOOK_PUBLIC_ERROR_CODES;
use serde::Serialize;
const DOCS_BASE_URL: &str = "https://docs.athena-cluster.com";
const INTERNAL_ERROR_BASE: u32 = 1000;
const GATEWAY_ERROR_BASE: u32 = 2000;
const STORAGE_ERROR_BASE: u32 = 3000;
const BILLING_ERROR_BASE: u32 = 4000;
const CHAT_ERROR_BASE: u32 = 5000;
const WEBHOOK_ERROR_BASE: u32 = 6000;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct AthenaErrorDescriptor {
pub code: &'static str,
pub error_number: u32,
pub status: u16,
pub description: &'static str,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AthenaErrorContractFields {
pub code: Option<String>,
pub error_number: Option<u32>,
pub docs_url: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AthenaErrorCatalogEntry {
pub domain: &'static str,
pub code: &'static str,
pub error_number: u32,
pub status: u16,
pub description: &'static str,
pub docs_url: String,
}
const INTERNAL_ERROR_DESCRIPTORS: &[(&str, u16, &str)] = &[
(
"unique_violation",
409,
"A uniqueness constraint rejected the request.",
),
(
"foreign_key_violation",
409,
"A foreign-key constraint rejected the request.",
),
(
"not_null_violation",
400,
"A required database field was missing.",
),
(
"check_constraint_violation",
400,
"A database check constraint rejected the request.",
),
(
"invalid_text_encoding",
400,
"The request included invalid text encoding.",
),
(
"undefined_column",
400,
"The request referenced a column that does not exist.",
),
(
"type_operator_mismatch",
422,
"The request used incompatible operand types.",
),
(
"text_uuid_operator_mismatch",
422,
"The request compared text and UUID values without compatible coercion.",
),
("syntax_error", 400, "The SQL or query shape is invalid."),
(
"insufficient_privilege",
403,
"The caller lacks database privileges.",
),
(
"connection_error",
503,
"Athena could not reach the database backend.",
),
(
"pool_timeout",
503,
"Athena timed out waiting for a database connection.",
),
(
"pool_closed",
503,
"The database connection pool is closed.",
),
(
"worker_crashed",
500,
"A database worker crashed during execution.",
),
(
"io_error",
503,
"A database network or I/O failure occurred.",
),
(
"tls_error",
503,
"A secure database connection could not be established.",
),
(
"column_not_found",
500,
"Athena could not find a query result column it expected.",
),
(
"column_index_out_of_bounds",
500,
"Athena attempted to read a query result column beyond the available range.",
),
(
"type_not_found",
500,
"Athena could not resolve a database type.",
),
(
"column_decode_error",
500,
"Athena failed to decode a result column.",
),
("row_not_found", 404, "The requested row does not exist."),
(
"migration_error",
500,
"A database migration operation failed.",
),
(
"configuration_error",
500,
"Athena database configuration is invalid.",
),
(
"database_error",
500,
"Athena hit an unclassified database error.",
),
(
"data_exception",
400,
"The database rejected caller-provided data.",
),
(
"query_syntax",
400,
"The query is invalid for the database backend.",
),
(
"db_error",
500,
"The database returned an unclassified server error.",
),
(
"postgres_driver_error",
502,
"The PostgreSQL driver failed before completion.",
),
];
const BILLING_ERROR_DESCRIPTORS: &[(&str, u16, &str)] = &[
(
"BILLING_WEBHOOK_SECRET_INVALID",
401,
"The billing webhook secret did not match the configured connection secret.",
),
(
"MOLLIE_WEBHOOK_REJECTED",
401,
"Athena rejected the Mollie webhook after signature or payload verification failed.",
),
(
"BILLING_CONNECTION_NOT_FOUND",
404,
"The requested billing provider connection does not exist.",
),
(
"BILLING_PROVIDER_UNSUPPORTED",
400,
"The requested billing provider is not supported by this Athena runtime.",
),
(
"BILLING_PROVIDER_MISMATCH",
400,
"The billing provider path did not match the stored provider connection.",
),
(
"BILLING_CONNECTION_CONFIG_INVALID",
500,
"The stored billing provider connection config is invalid.",
),
(
"BILLING_ADAPTER_FAILED",
502,
"The upstream billing provider adapter failed while fetching or projecting provider state.",
),
(
"BILLING_AUTH_SYNC_FAILED",
502,
"Athena could not apply the projected billing grants to Athena Auth.",
),
(
"BILLING_AUTH_SYNC_CONFIG_INVALID",
500,
"The Athena Auth billing sync runtime configuration is invalid.",
),
(
"STRIPE_WEBHOOK_REJECTED",
401,
"Athena rejected the Stripe webhook after signature or payload verification failed.",
),
(
"BILLING_WEBHOOK_PAYLOAD_UNSUPPORTED",
422,
"The billing webhook payload was valid but Athena does not project that event shape yet.",
),
(
"BILLING_PROVIDER_RESPONSE_INVALID",
502,
"Athena could not parse the upstream billing provider response or webhook payload.",
),
(
"BILLING_PROVIDER_HTTP_FAILED",
502,
"Athena could not complete the required upstream billing provider HTTP request.",
),
];
const CHAT_ERROR_DESCRIPTORS: &[(&str, u16, &str)] = &[
(
"CHAT_AUTH_REQUIRED",
401,
"The chat route requires an authenticated actor.",
),
(
"CHAT_FORBIDDEN",
403,
"The actor is not allowed to perform the requested chat action.",
),
(
"CHAT_NOT_FOUND",
404,
"The requested chat room, member, message, or related resource was not found.",
),
(
"CHAT_CONFLICT",
409,
"The requested chat mutation conflicts with the current durable chat state.",
),
(
"CHAT_VALIDATION_FAILED",
400,
"The chat request payload failed validation.",
),
(
"CHAT_UNAVAILABLE",
503,
"Athena chat is temporarily unavailable.",
),
(
"CHAT_UNSUPPORTED",
400,
"The requested chat operation is not supported by this Athena runtime.",
),
(
"CHAT_INTERNAL",
500,
"Athena chat failed while processing the request.",
),
];
pub fn docs_url_for_error_number(error_number: u32) -> String {
format!("{DOCS_BASE_URL}/{error_number:04}")
}
pub fn contract_fields_for_code(code: Option<&str>) -> AthenaErrorContractFields {
let Some(code) = code else {
return AthenaErrorContractFields {
code: None,
error_number: None,
docs_url: None,
};
};
let descriptor = descriptor_for_code(code);
AthenaErrorContractFields {
code: Some(code.to_string()),
error_number: descriptor.map(|entry| entry.error_number),
docs_url: descriptor.map(|entry| docs_url_for_error_number(entry.error_number)),
}
}
pub fn descriptor_for_code(code: &str) -> Option<AthenaErrorDescriptor> {
internal_descriptor_for_code(code)
.or_else(|| gateway_descriptor_for_code(code))
.or_else(|| storage_descriptor_for_code(code))
.or_else(|| billing_descriptor_for_code(code))
.or_else(|| chat_descriptor_for_code(code))
.or_else(|| webhook_descriptor_for_code(code))
}
pub fn public_error_catalog() -> Vec<AthenaErrorCatalogEntry> {
let mut entries = Vec::new();
entries.extend(INTERNAL_ERROR_DESCRIPTORS.iter().enumerate().map(
|(index, (code, status, description))| AthenaErrorCatalogEntry {
domain: "internal",
code,
error_number: INTERNAL_ERROR_BASE + index as u32,
status: *status,
description,
docs_url: docs_url_for_error_number(INTERNAL_ERROR_BASE + index as u32),
},
));
entries.extend(
GATEWAY_PUBLIC_ERROR_CODES
.iter()
.enumerate()
.map(|(index, descriptor)| AthenaErrorCatalogEntry {
domain: "gateway",
code: descriptor.code,
error_number: GATEWAY_ERROR_BASE + index as u32,
status: descriptor.status,
description: descriptor.description,
docs_url: docs_url_for_error_number(GATEWAY_ERROR_BASE + index as u32),
}),
);
entries.extend(
STORAGE_PUBLIC_ERROR_CODES
.iter()
.enumerate()
.map(|(index, descriptor)| AthenaErrorCatalogEntry {
domain: "storage",
code: descriptor.code,
error_number: STORAGE_ERROR_BASE + index as u32,
status: descriptor.status,
description: descriptor.description,
docs_url: docs_url_for_error_number(STORAGE_ERROR_BASE + index as u32),
}),
);
entries.extend(BILLING_ERROR_DESCRIPTORS.iter().enumerate().map(
|(index, (code, status, description))| AthenaErrorCatalogEntry {
domain: "billing",
code,
error_number: BILLING_ERROR_BASE + index as u32,
status: *status,
description,
docs_url: docs_url_for_error_number(BILLING_ERROR_BASE + index as u32),
},
));
entries.extend(CHAT_ERROR_DESCRIPTORS.iter().enumerate().map(
|(index, (code, status, description))| AthenaErrorCatalogEntry {
domain: "chat",
code,
error_number: CHAT_ERROR_BASE + index as u32,
status: *status,
description,
docs_url: docs_url_for_error_number(CHAT_ERROR_BASE + index as u32),
},
));
entries.extend(
WEBHOOK_PUBLIC_ERROR_CODES
.iter()
.enumerate()
.map(|(index, descriptor)| AthenaErrorCatalogEntry {
domain: "webhook",
code: descriptor.code,
error_number: WEBHOOK_ERROR_BASE + index as u32,
status: descriptor.status,
description: descriptor.description,
docs_url: docs_url_for_error_number(WEBHOOK_ERROR_BASE + index as u32),
}),
);
entries.sort_by_key(|entry| entry.error_number);
entries
}
fn internal_descriptor_for_code(code: &str) -> Option<AthenaErrorDescriptor> {
INTERNAL_ERROR_DESCRIPTORS
.iter()
.enumerate()
.find(|(_, (descriptor_code, _, _))| *descriptor_code == code)
.map(
|(index, (descriptor_code, status, description))| AthenaErrorDescriptor {
code: descriptor_code,
error_number: INTERNAL_ERROR_BASE + index as u32,
status: *status,
description,
},
)
}
fn gateway_descriptor_for_code(code: &str) -> Option<AthenaErrorDescriptor> {
GATEWAY_PUBLIC_ERROR_CODES
.iter()
.enumerate()
.find(|(_, descriptor)| descriptor.code == code)
.map(|(index, descriptor)| AthenaErrorDescriptor {
code: descriptor.code,
error_number: GATEWAY_ERROR_BASE + index as u32,
status: descriptor.status,
description: descriptor.description,
})
}
fn storage_descriptor_for_code(code: &str) -> Option<AthenaErrorDescriptor> {
STORAGE_PUBLIC_ERROR_CODES
.iter()
.enumerate()
.find(|(_, descriptor)| descriptor.code == code)
.map(|(index, descriptor)| AthenaErrorDescriptor {
code: descriptor.code,
error_number: STORAGE_ERROR_BASE + index as u32,
status: descriptor.status,
description: descriptor.description,
})
}
fn billing_descriptor_for_code(code: &str) -> Option<AthenaErrorDescriptor> {
BILLING_ERROR_DESCRIPTORS
.iter()
.enumerate()
.find(|(_, (descriptor_code, _, _))| *descriptor_code == code)
.map(
|(index, (descriptor_code, status, description))| AthenaErrorDescriptor {
code: descriptor_code,
error_number: BILLING_ERROR_BASE + index as u32,
status: *status,
description,
},
)
}
fn chat_descriptor_for_code(code: &str) -> Option<AthenaErrorDescriptor> {
CHAT_ERROR_DESCRIPTORS
.iter()
.enumerate()
.find(|(_, (descriptor_code, _, _))| *descriptor_code == code)
.map(
|(index, (descriptor_code, status, description))| AthenaErrorDescriptor {
code: descriptor_code,
error_number: CHAT_ERROR_BASE + index as u32,
status: *status,
description,
},
)
}
fn webhook_descriptor_for_code(code: &str) -> Option<AthenaErrorDescriptor> {
WEBHOOK_PUBLIC_ERROR_CODES
.iter()
.enumerate()
.find(|(_, descriptor)| descriptor.code == code)
.map(|(index, descriptor)| AthenaErrorDescriptor {
code: descriptor.code,
error_number: WEBHOOK_ERROR_BASE + index as u32,
status: descriptor.status,
description: descriptor.description,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolves_gateway_descriptor_with_docs_number() {
let descriptor =
descriptor_for_code("missing_client_header").expect("gateway code should resolve");
assert_eq!(descriptor.error_number, 2003);
assert_eq!(
docs_url_for_error_number(descriptor.error_number),
"https://docs.athena-cluster.com/2003"
);
}
#[test]
fn resolves_storage_descriptor_with_docs_number() {
let descriptor =
descriptor_for_code("storage_bucket_not_found").expect("storage code should resolve");
assert_eq!(descriptor.status, 404);
assert!(descriptor.error_number >= 3000);
}
#[test]
fn resolves_billing_descriptor_with_docs_number() {
let descriptor =
descriptor_for_code("MOLLIE_WEBHOOK_REJECTED").expect("billing code should resolve");
assert_eq!(descriptor.error_number, 4001);
}
#[test]
fn resolves_extended_billing_descriptor_numbers() {
let descriptor = descriptor_for_code("BILLING_ADAPTER_FAILED")
.expect("extended billing code should resolve");
assert_eq!(descriptor.error_number, 4006);
}
#[test]
fn resolves_chat_descriptor_with_docs_number() {
let descriptor = descriptor_for_code("CHAT_NOT_FOUND").expect("chat code should resolve");
assert_eq!(descriptor.error_number, 5002);
assert_eq!(descriptor.status, 404);
assert_eq!(
docs_url_for_error_number(descriptor.error_number),
"https://docs.athena-cluster.com/5002"
);
}
#[test]
fn resolves_webhook_descriptor_with_docs_number() {
let descriptor = descriptor_for_code("webhook_sink_provision_failed")
.expect("webhook code should resolve");
assert_eq!(descriptor.error_number, 6002);
assert_eq!(descriptor.status, 500);
assert_eq!(
docs_url_for_error_number(descriptor.error_number),
"https://docs.athena-cluster.com/6002"
);
}
#[test]
fn sqlx_known_code_stays_mapped() {
let descriptor =
descriptor_for_code("unique_violation").expect("sqlx unique violation should resolve");
assert_eq!(descriptor.error_number, 1000);
}
#[test]
fn contract_fields_for_code_returns_numbered_docs_metadata() {
let fields = contract_fields_for_code(Some("BILLING_PROVIDER_HTTP_FAILED"));
assert_eq!(fields.code.as_deref(), Some("BILLING_PROVIDER_HTTP_FAILED"));
assert_eq!(fields.error_number, Some(4012));
assert_eq!(
fields.docs_url.as_deref(),
Some("https://docs.athena-cluster.com/4012")
);
}
#[test]
fn public_error_catalog_is_sorted_and_contains_multiple_domains() {
let catalog = public_error_catalog();
assert!(!catalog.is_empty());
assert_eq!(catalog[0].error_number, 1000);
assert!(catalog.iter().any(|entry| entry.domain == "billing"));
assert!(catalog.iter().any(|entry| entry.domain == "chat"));
assert!(
catalog
.windows(2)
.all(|pair| pair[0].error_number <= pair[1].error_number)
);
}
}