Skip to content

Commit

Permalink
feat(flags): add validation for database reads for rust feature flag …
Browse files Browse the repository at this point in the history
…service (#24089)

Co-authored-by: Neil Kakkar <[email protected]>
Co-authored-by: James Greenhill <[email protected]>
  • Loading branch information
3 people authored Aug 9, 2024
1 parent 0d07fe8 commit 373785b
Show file tree
Hide file tree
Showing 7 changed files with 1,504 additions and 65 deletions.
117 changes: 91 additions & 26 deletions rust/feature-flags/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,66 +13,131 @@ pub enum FlagsResponseCode {
Ok = 1,
}

#[derive(Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(untagged)]
pub enum FlagValue {
Boolean(bool),
String(String),
}

#[derive(Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct FlagsResponse {
pub error_while_computing_flags: bool,
// TODO: better typing here, support bool responses
pub feature_flags: HashMap<String, String>,
pub feature_flags: HashMap<String, FlagValue>,
}

#[derive(Error, Debug)]
pub enum ClientFacingError {
#[error("Invalid request: {0}")]
BadRequest(String),
#[error("Unauthorized: {0}")]
Unauthorized(String),
#[error("Rate limited")]
RateLimited,
#[error("Service unavailable")]
ServiceUnavailable,
}

#[derive(Error, Debug)]
pub enum FlagError {
#[error(transparent)]
ClientFacing(#[from] ClientFacingError),
#[error("Internal error: {0}")]
Internal(String),
#[error("failed to decode request: {0}")]
RequestDecodingError(String),
#[error("failed to parse request: {0}")]
RequestParsingError(#[from] serde_json::Error),

#[error("Empty distinct_id in request")]
EmptyDistinctId,
#[error("No distinct_id in request")]
MissingDistinctId,

#[error("No api_key in request")]
NoTokenError,
#[error("API key is not valid")]
TokenValidationError,

#[error("rate limited")]
RateLimited,

#[error("failed to parse redis cache data")]
DataParsingError,
#[error("failed to update redis cache")]
CacheUpdateError,
#[error("redis unavailable")]
RedisUnavailable,
#[error("database unavailable")]
DatabaseUnavailable,
#[error("Timed out while fetching data")]
TimeoutError,
// TODO: Consider splitting top-level errors (that are returned to the client)
// and FlagMatchingError, like timeouterror which we can gracefully handle.
// This will make the `into_response` a lot clearer as well, since it wouldn't
// have arbitrary errors that actually never make it to the client.
}

impl IntoResponse for FlagError {
fn into_response(self) -> Response {
match self {
FlagError::RequestDecodingError(_)
| FlagError::RequestParsingError(_)
| FlagError::EmptyDistinctId
| FlagError::MissingDistinctId => (StatusCode::BAD_REQUEST, self.to_string()),

FlagError::NoTokenError | FlagError::TokenValidationError => {
(StatusCode::UNAUTHORIZED, self.to_string())
FlagError::ClientFacing(err) => match err {
ClientFacingError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg),
ClientFacingError::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, msg),
ClientFacingError::RateLimited => (StatusCode::TOO_MANY_REQUESTS, "Rate limit exceeded. Please reduce your request frequency and try again later.".to_string()),
ClientFacingError::ServiceUnavailable => (StatusCode::SERVICE_UNAVAILABLE, "Service is currently unavailable. Please try again later.".to_string()),
},
FlagError::Internal(msg) => {
tracing::error!("Internal server error: {}", msg);
(
StatusCode::INTERNAL_SERVER_ERROR,
"An internal server error occurred. Please try again later or contact support if the problem persists.".to_string(),
)
}
FlagError::RequestDecodingError(msg) => {
(StatusCode::BAD_REQUEST, format!("Failed to decode request: {}. Please check your request format and try again.", msg))
}
FlagError::RequestParsingError(err) => {
(StatusCode::BAD_REQUEST, format!("Failed to parse request: {}. Please ensure your request is properly formatted and all required fields are present.", err))
}
FlagError::EmptyDistinctId => {
(StatusCode::BAD_REQUEST, "The distinct_id field cannot be empty. Please provide a valid identifier.".to_string())
}
FlagError::MissingDistinctId => {
(StatusCode::BAD_REQUEST, "The distinct_id field is missing from the request. Please include a valid identifier.".to_string())
}
FlagError::NoTokenError => {
(StatusCode::UNAUTHORIZED, "No API key provided. Please include a valid API key in your request.".to_string())
}
FlagError::TokenValidationError => {
(StatusCode::UNAUTHORIZED, "The provided API key is invalid or has expired. Please check your API key and try again.".to_string())
}
FlagError::DataParsingError => {
tracing::error!("Data parsing error: {:?}", self);
(
StatusCode::SERVICE_UNAVAILABLE,
"Failed to parse internal data. This is likely a temporary issue. Please try again later.".to_string(),
)
}
FlagError::CacheUpdateError => {
tracing::error!("Cache update error: {:?}", self);
(
StatusCode::INTERNAL_SERVER_ERROR,
"Failed to update internal cache. This is likely a temporary issue. Please try again later.".to_string(),
)
}
FlagError::RedisUnavailable => {
tracing::error!("Redis unavailable: {:?}", self);
(
StatusCode::SERVICE_UNAVAILABLE,
"Our cache service is currently unavailable. This is likely a temporary issue. Please try again later.".to_string(),
)
}
FlagError::DatabaseUnavailable => {
tracing::error!("Database unavailable: {:?}", self);
(
StatusCode::SERVICE_UNAVAILABLE,
"Our database service is currently unavailable. This is likely a temporary issue. Please try again later.".to_string(),
)
}
FlagError::TimeoutError => {
tracing::error!("Timeout error: {:?}", self);
(
StatusCode::SERVICE_UNAVAILABLE,
"The request timed out. This could be due to high load or network issues. Please try again later.".to_string(),
)
}

FlagError::RateLimited => (StatusCode::TOO_MANY_REQUESTS, self.to_string()),

FlagError::DataParsingError
| FlagError::RedisUnavailable
| FlagError::DatabaseUnavailable
| FlagError::TimeoutError => (StatusCode::SERVICE_UNAVAILABLE, self.to_string()),
}
.into_response()
}
Expand Down
Loading

0 comments on commit 373785b

Please sign in to comment.