Skip to content

Commit adeba5a

Browse files
authored
refactor: replace stringly-typed error_type with enum (#16)
Replace the `error_type: String` field in `ErrorResponse` with a proper `ErrorType` enum using `#[serde(rename_all = "snake_case")]`. This prevents typos, makes error types discoverable, and maintains backwards compatibility in the JSON API. Closes #15
1 parent db970e8 commit adeba5a

2 files changed

Lines changed: 47 additions & 22 deletions

File tree

src/web/server.rs

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,33 @@ struct InputData {
6565
format: Option<FileFormat>,
6666
}
6767

68+
/// Typed error categories for API error responses.
69+
///
70+
/// Serialized as `snake_case` strings in JSON to maintain backwards compatibility.
71+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
72+
#[serde(rename_all = "snake_case")]
73+
pub enum ErrorType {
74+
FieldLimitExceeded,
75+
FileTooLarge,
76+
TextTooLarge,
77+
InternalError,
78+
InvalidMatchId,
79+
FilenameTooLong,
80+
InvalidFilename,
81+
FormatMismatch,
82+
InvalidContent,
83+
ValidationFailed,
84+
MissingInput,
85+
FormatDetectionFailed,
86+
ParseFailed,
87+
BinaryParseFailed,
88+
}
89+
6890
/// Enhanced error response
6991
#[derive(Serialize)]
7092
pub struct ErrorResponse {
7193
pub error: String,
72-
pub error_type: String,
94+
pub error_type: ErrorType,
7395
pub details: Option<String>,
7496
}
7597

@@ -100,18 +122,18 @@ struct DetailedQueryParams {
100122
/// Create a safe error response that prevents information disclosure
101123
/// while logging detailed errors server-side for debugging
102124
pub fn create_safe_error_response(
103-
error_type: &str,
125+
error_type: ErrorType,
104126
user_message: &str,
105127
internal_error: Option<&str>,
106128
) -> ErrorResponse {
107129
// Log detailed error server-side for debugging (not exposed to client)
108130
if let Some(internal_msg) = internal_error {
109-
tracing::error!("Internal error ({}): {}", error_type, internal_msg);
131+
tracing::error!("Internal error ({:?}): {}", error_type, internal_msg);
110132
}
111133

112134
ErrorResponse {
113135
error: user_message.to_string(),
114-
error_type: error_type.to_string(),
136+
error_type,
115137
details: None, // Never expose internal details to prevent information disclosure
116138
}
117139
}
@@ -489,7 +511,7 @@ async fn handle_detailed_response(
489511
return (
490512
StatusCode::BAD_REQUEST,
491513
Json(create_safe_error_response(
492-
"invalid_match_id",
514+
ErrorType::InvalidMatchId,
493515
"Invalid match ID specified",
494516
Some("Match index out of bounds"),
495517
)),
@@ -807,7 +829,7 @@ async fn extract_request_data(
807829
StatusCode::BAD_REQUEST,
808830
Json(ErrorResponse {
809831
error: "Too many form fields".to_string(),
810-
error_type: "field_limit_exceeded".to_string(),
832+
error_type: ErrorType::FieldLimitExceeded,
811833
details: None, // No internal details for security
812834
}),
813835
)
@@ -851,7 +873,7 @@ async fn extract_request_data(
851873
StatusCode::PAYLOAD_TOO_LARGE,
852874
Json(ErrorResponse {
853875
error: "File size exceeds limit".to_string(),
854-
error_type: "file_too_large".to_string(),
876+
error_type: ErrorType::FileTooLarge,
855877
details: None,
856878
}),
857879
)
@@ -877,7 +899,7 @@ async fn extract_request_data(
877899
return Err((
878900
StatusCode::BAD_REQUEST,
879901
Json(create_safe_error_response(
880-
"filename_too_long",
902+
ErrorType::FilenameTooLong,
881903
"Filename exceeds maximum length limit",
882904
Some("Filename validation failed due to length constraints")
883905
)),
@@ -887,7 +909,7 @@ async fn extract_request_data(
887909
return Err((
888910
StatusCode::BAD_REQUEST,
889911
Json(create_safe_error_response(
890-
"invalid_filename",
912+
ErrorType::InvalidFilename,
891913
"Filename contains invalid or dangerous characters",
892914
Some("Filename validation failed due to invalid characters")
893915
)),
@@ -897,7 +919,7 @@ async fn extract_request_data(
897919
return Err((
898920
StatusCode::BAD_REQUEST,
899921
Json(create_safe_error_response(
900-
"format_mismatch",
922+
ErrorType::FormatMismatch,
901923
"File content does not match the expected format based on filename",
902924
Some("Format validation failed")
903925
)),
@@ -907,7 +929,7 @@ async fn extract_request_data(
907929
return Err((
908930
StatusCode::BAD_REQUEST,
909931
Json(create_safe_error_response(
910-
"invalid_content",
932+
ErrorType::InvalidContent,
911933
"File content appears malformed or corrupted",
912934
None,
913935
)),
@@ -918,7 +940,7 @@ async fn extract_request_data(
918940
return Err((
919941
StatusCode::BAD_REQUEST,
920942
Json(create_safe_error_response(
921-
"validation_failed",
943+
ErrorType::ValidationFailed,
922944
"File validation failed",
923945
None,
924946
)),
@@ -938,7 +960,7 @@ async fn extract_request_data(
938960
StatusCode::PAYLOAD_TOO_LARGE,
939961
Json(ErrorResponse {
940962
error: "Text field size exceeds limit".to_string(),
941-
error_type: "text_too_large".to_string(),
963+
error_type: ErrorType::TextTooLarge,
942964
details: None,
943965
}),
944966
)
@@ -997,7 +1019,7 @@ async fn extract_request_data(
9971019
return Err((
9981020
StatusCode::BAD_REQUEST,
9991021
Json(create_safe_error_response(
1000-
"missing_input",
1022+
ErrorType::MissingInput,
10011023
error_msg,
10021024
None, // Never include details for consistency
10031025
)),
@@ -1036,7 +1058,7 @@ fn parse_input_data(
10361058
(
10371059
StatusCode::BAD_REQUEST,
10381060
Json(create_safe_error_response(
1039-
"format_detection_failed",
1061+
ErrorType::FormatDetectionFailed,
10401062
"Unable to detect file format. Please check the file type and try again.",
10411063
Some("Format detection failed during parsing"),
10421064
)),
@@ -1050,7 +1072,7 @@ fn parse_input_data(
10501072
Err(_) => Err(Box::new((
10511073
StatusCode::BAD_REQUEST,
10521074
Json(create_safe_error_response(
1053-
"parse_failed",
1075+
ErrorType::ParseFailed,
10541076
"Unable to process file content. Please check the file format and try again.",
10551077
Some("File parsing failed during content processing"),
10561078
)),
@@ -1066,7 +1088,7 @@ fn parse_input_data(
10661088
Err(_) => Err(Box::new((
10671089
StatusCode::BAD_REQUEST,
10681090
Json(create_safe_error_response(
1069-
"binary_parse_failed",
1091+
ErrorType::BinaryParseFailed,
10701092
"Unable to process binary file. Please verify the file format and try again.",
10711093
Some("Binary file parsing failed during processing"),
10721094
)),
@@ -1079,7 +1101,7 @@ fn parse_input_data(
10791101
StatusCode::INTERNAL_SERVER_ERROR,
10801102
Json(ErrorResponse {
10811103
error: "Internal error: no input data".to_string(),
1082-
error_type: "internal_error".to_string(),
1104+
error_type: ErrorType::InternalError,
10831105
details: None,
10841106
}),
10851107
)

tests/security_tests.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -218,24 +218,27 @@ fn test_comprehensive_upload_validation() {
218218
/// Test error message sanitization
219219
#[test]
220220
fn test_error_sanitization() {
221-
use ref_solver::web::server::create_safe_error_response;
221+
use ref_solver::web::server::{create_safe_error_response, ErrorType};
222222

223223
// Test that internal error details are not exposed
224224
let error_response = create_safe_error_response(
225-
"test_error",
225+
ErrorType::InternalError,
226226
"User-friendly message",
227227
Some("/internal/path/file.rs:123 - Database connection failed"),
228228
);
229229

230230
assert_eq!(error_response.error, "User-friendly message");
231-
assert_eq!(error_response.error_type, "test_error");
231+
assert_eq!(error_response.error_type, ErrorType::InternalError);
232+
let serialized =
233+
serde_json::to_value(&error_response).expect("Failed to serialize error response");
234+
assert_eq!(serialized["error_type"], "internal_error");
232235
assert!(
233236
error_response.details.is_none(),
234237
"Internal details should never be exposed"
235238
);
236239

237240
// Test that the function handles None internal errors
238-
let error_response = create_safe_error_response("test_error", "User message", None);
241+
let error_response = create_safe_error_response(ErrorType::InternalError, "User message", None);
239242
assert!(error_response.details.is_none());
240243
}
241244

0 commit comments

Comments
 (0)