1
0
mirror of https://github.com/bitwarden/server synced 2026-02-06 11:44:06 +00:00
Files
server/akd/crates/reader/src/error.rs
2026-01-22 12:32:58 -08:00

228 lines
8.4 KiB
Rust

use akd::errors::AkdError;
use axum::http::StatusCode;
use serde::{Deserialize, Serialize};
use thiserror::Error;
/// Main error type for the Reader API
///
/// Note: Base64 validation for input fields happens automatically during JSON
/// deserialization via the `bitwarden_encoding::B64` type, so invalid base64
/// will be rejected before reaching the handlers with a 400 Bad Request error.
#[derive(Error, Debug)]
pub enum ReaderError {
// Application-level validation errors (4xx)
#[error("Invalid epoch range: start_epoch ({start_epoch}) must be <= end_epoch ({end_epoch})")]
InvalidEpochRange { start_epoch: u64, end_epoch: u64 },
#[error("Empty batch request")]
EmptyBatch,
#[error("Batch size limit exceeded: {limit}")]
BatchTooLarge { limit: usize },
// AKD library errors
#[error("AKD error: {0}")]
Akd(#[from] AkdError),
}
/// Error response structure sent to clients
#[derive(Debug, Serialize, Deserialize)]
pub struct ErrorResponse {
/// Machine-readable error code for client-side matching
pub code: ErrorCode,
/// Human-readable error message
pub message: String,
}
/// Machine-readable error codes for reliable client-side parsing
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ErrorCode {
// Application-level validation errors (400-level)
InvalidEpochRange,
EmptyBatch,
BatchTooLarge,
// AKD-specific errors
AkdTreeNode,
AkdVerification,
AkdInvalidEpoch,
AkdInvalidVersion,
AkdReadOnlyDirectory,
AkdPublish,
AkdAzks,
AkdVrf,
AkdStorageNotFound,
AkdStorageTransaction,
AkdStorageConnection,
AkdStorageOther,
AkdAudit,
AkdParallelism,
// Server errors (5xx)
InternalError,
}
impl ReaderError {
/// Map error to appropriate HTTP status code
pub fn status_code(&self) -> StatusCode {
match self {
// 400-level errors
ReaderError::InvalidEpochRange { .. } => StatusCode::BAD_REQUEST,
ReaderError::EmptyBatch => StatusCode::BAD_REQUEST,
ReaderError::BatchTooLarge { .. } => StatusCode::BAD_REQUEST,
// AKD errors - nuanced mapping
ReaderError::Akd(akd_err) => match akd_err {
AkdError::Storage(storage_err) => match storage_err {
akd::errors::StorageError::NotFound(_) => StatusCode::NOT_FOUND,
akd::errors::StorageError::Transaction(_) => StatusCode::INTERNAL_SERVER_ERROR,
akd::errors::StorageError::Connection(_) => StatusCode::SERVICE_UNAVAILABLE,
akd::errors::StorageError::Other(_) => StatusCode::INTERNAL_SERVER_ERROR,
},
AkdError::Directory(dir_err) => match dir_err {
akd::errors::DirectoryError::InvalidEpoch(_) => StatusCode::BAD_REQUEST,
akd::errors::DirectoryError::InvalidVersion(_) => StatusCode::BAD_REQUEST,
akd::errors::DirectoryError::Verification(_) => {
StatusCode::UNPROCESSABLE_ENTITY
}
akd::errors::DirectoryError::ReadOnlyDirectory(_) => StatusCode::FORBIDDEN,
akd::errors::DirectoryError::Publish(_) => StatusCode::INTERNAL_SERVER_ERROR,
},
AkdError::TreeNode(_) => StatusCode::INTERNAL_SERVER_ERROR,
AkdError::AzksErr(_) => StatusCode::INTERNAL_SERVER_ERROR,
AkdError::Vrf(_) => StatusCode::INTERNAL_SERVER_ERROR,
AkdError::AuditErr(_) => StatusCode::INTERNAL_SERVER_ERROR,
AkdError::Parallelism(_) => StatusCode::INTERNAL_SERVER_ERROR,
AkdError::TestErr(_) => StatusCode::INTERNAL_SERVER_ERROR,
},
}
}
/// Convert error to error code for client parsing
pub fn error_code(&self) -> ErrorCode {
match self {
ReaderError::InvalidEpochRange { .. } => ErrorCode::InvalidEpochRange,
ReaderError::EmptyBatch => ErrorCode::EmptyBatch,
ReaderError::BatchTooLarge { .. } => ErrorCode::BatchTooLarge,
ReaderError::Akd(akd_err) => Self::akd_error_code(akd_err),
}
}
/// Map AkdError to specific error code
fn akd_error_code(err: &AkdError) -> ErrorCode {
match err {
AkdError::TreeNode(_) => ErrorCode::AkdTreeNode,
AkdError::Directory(dir_err) => match dir_err {
akd::errors::DirectoryError::Verification(_) => ErrorCode::AkdVerification,
akd::errors::DirectoryError::InvalidEpoch(_) => ErrorCode::AkdInvalidEpoch,
akd::errors::DirectoryError::InvalidVersion(_) => ErrorCode::AkdInvalidVersion,
akd::errors::DirectoryError::ReadOnlyDirectory(_) => {
ErrorCode::AkdReadOnlyDirectory
}
akd::errors::DirectoryError::Publish(_) => ErrorCode::AkdPublish,
},
AkdError::AzksErr(_) => ErrorCode::AkdAzks,
AkdError::Vrf(_) => ErrorCode::AkdVrf,
AkdError::Storage(storage_err) => match storage_err {
akd::errors::StorageError::NotFound(_) => ErrorCode::AkdStorageNotFound,
akd::errors::StorageError::Transaction(_) => ErrorCode::AkdStorageTransaction,
akd::errors::StorageError::Connection(_) => ErrorCode::AkdStorageConnection,
akd::errors::StorageError::Other(_) => ErrorCode::AkdStorageOther,
},
AkdError::AuditErr(_) => ErrorCode::AkdAudit,
AkdError::Parallelism(_) => ErrorCode::AkdParallelism,
AkdError::TestErr(_) => ErrorCode::InternalError,
}
}
/// Convert error to ErrorResponse
pub fn to_error_response(&self) -> ErrorResponse {
ErrorResponse {
code: self.error_code(),
message: self.to_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_akd_not_found_maps_to_404() {
let storage_err = akd::errors::StorageError::NotFound("test".to_string());
let akd_err = AkdError::Storage(storage_err);
let reader_err = ReaderError::from(akd_err);
assert_eq!(reader_err.status_code(), StatusCode::NOT_FOUND);
assert!(matches!(
reader_err.error_code(),
ErrorCode::AkdStorageNotFound
));
}
#[test]
fn test_empty_batch_maps_to_400() {
let err = ReaderError::EmptyBatch;
assert_eq!(err.status_code(), StatusCode::BAD_REQUEST);
assert!(matches!(err.error_code(), ErrorCode::EmptyBatch));
}
#[test]
fn test_batch_too_large_maps_to_400() {
let err = ReaderError::BatchTooLarge { limit: 1000 };
assert_eq!(err.status_code(), StatusCode::BAD_REQUEST);
assert!(matches!(err.error_code(), ErrorCode::BatchTooLarge));
}
#[test]
fn test_invalid_epoch_range_maps_to_400() {
let err = ReaderError::InvalidEpochRange {
start_epoch: 0,
end_epoch: 0,
};
assert_eq!(err.status_code(), StatusCode::BAD_REQUEST);
assert!(matches!(err.error_code(), ErrorCode::InvalidEpochRange));
}
#[test]
fn test_error_response_serialization() {
let err = ReaderError::InvalidEpochRange {
start_epoch: 0,
end_epoch: 0,
};
let response = err.to_error_response();
assert!(matches!(response.code, ErrorCode::InvalidEpochRange));
eprintln!("Error message: {}", response.message);
assert!(response.message.contains("Invalid epoch range"));
}
#[test]
fn test_akd_invalid_epoch_error_maps_to_400() {
let dir_err = akd::errors::DirectoryError::InvalidEpoch("invalid epoch".to_string());
let akd_err = AkdError::Directory(dir_err);
let reader_err = ReaderError::from(akd_err);
assert_eq!(reader_err.status_code(), StatusCode::BAD_REQUEST);
assert!(matches!(
reader_err.error_code(),
ErrorCode::AkdInvalidEpoch
));
}
#[test]
fn test_akd_connection_error_maps_to_503() {
let storage_err = akd::errors::StorageError::Connection("connection failed".to_string());
let akd_err = AkdError::Storage(storage_err);
let reader_err = ReaderError::from(akd_err);
assert_eq!(reader_err.status_code(), StatusCode::SERVICE_UNAVAILABLE);
assert!(matches!(
reader_err.error_code(),
ErrorCode::AkdStorageConnection
));
}
}