Skip to content

Commit 820599c

Browse files
Return generic error messages to clients for server-side errors
Replace user_message() passthrough of Display output with a match that returns generic messages for 5xx/502/503 errors while keeping brief descriptions for 4xx client errors. Full error details are already logged via log::error! and never lost. Closes #437
1 parent 1281469 commit 820599c

1 file changed

Lines changed: 94 additions & 3 deletions

File tree

crates/common/src/error.rs

Lines changed: 94 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ use http::StatusCode;
1616
#[derive(Debug, Display)]
1717
pub enum TrustedServerError {
1818
/// Client-side input/validation error resulting in a 400 Bad Request.
19+
///
20+
/// **Note:** `message` is returned to clients in the HTTP response body.
21+
/// Keep it free of internal implementation details.
1922
#[display("Bad request: {message}")]
2023
BadRequest { message: String },
2124
/// Configuration errors that prevent the server from starting.
@@ -87,7 +90,10 @@ pub trait IntoHttpResponse {
8790
/// Convert the error into an HTTP status code.
8891
fn status_code(&self) -> StatusCode;
8992

90-
/// Get the error message to show to users (uses the Display implementation).
93+
/// Get a safe, user-facing error message.
94+
///
95+
/// Client errors (4xx) return a brief description; server/integration errors
96+
/// return a generic message. Full error details are preserved in server logs.
9197
fn user_message(&self) -> String;
9298
}
9399

@@ -112,7 +118,92 @@ impl IntoHttpResponse for TrustedServerError {
112118
}
113119

114120
fn user_message(&self) -> String {
115-
// Use the Display implementation which already has the specific error message
116-
self.to_string()
121+
match self {
122+
// Client errors (4xx) — safe to surface a brief description
123+
Self::BadRequest { message } => format!("Bad request: {message}"),
124+
// Consent strings may contain user data; return category only.
125+
Self::GdprConsent { .. } => "GDPR consent error".to_string(),
126+
Self::InvalidHeaderValue { .. } => "Invalid header value".to_string(),
127+
Self::InvalidUtf8 { .. } => "Invalid request data".to_string(),
128+
// Server/integration errors (5xx/502/503) — generic message only.
129+
// Full details are already logged via log::error! in to_error_response.
130+
_ => "An internal error occurred".to_string(),
131+
}
132+
}
133+
}
134+
135+
#[cfg(test)]
136+
mod tests {
137+
use super::*;
138+
139+
#[test]
140+
fn server_errors_return_generic_message() {
141+
let cases = [
142+
TrustedServerError::Configuration {
143+
message: "secret db path".into(),
144+
},
145+
TrustedServerError::KvStore {
146+
store_name: "users".into(),
147+
message: "timeout".into(),
148+
},
149+
TrustedServerError::Proxy {
150+
message: "upstream 10.0.0.1 refused".into(),
151+
},
152+
TrustedServerError::SyntheticId {
153+
message: "seed file missing".into(),
154+
},
155+
TrustedServerError::Template {
156+
message: "render failed".into(),
157+
},
158+
TrustedServerError::Auction {
159+
message: "bid timeout".into(),
160+
},
161+
TrustedServerError::Gam {
162+
message: "api key invalid".into(),
163+
},
164+
TrustedServerError::Prebid {
165+
message: "adapter error".into(),
166+
},
167+
TrustedServerError::Integration {
168+
integration: "foo".into(),
169+
message: "connection refused".into(),
170+
},
171+
TrustedServerError::Settings {
172+
message: "parse failed".into(),
173+
},
174+
TrustedServerError::InsecureDefault {
175+
field: "api_key".into(),
176+
},
177+
];
178+
for error in &cases {
179+
assert_eq!(
180+
error.user_message(),
181+
"An internal error occurred",
182+
"should not leak details for {error:?}",
183+
);
184+
}
185+
}
186+
187+
#[test]
188+
fn client_errors_return_safe_descriptions() {
189+
let error = TrustedServerError::BadRequest {
190+
message: "missing field".into(),
191+
};
192+
assert_eq!(error.user_message(), "Bad request: missing field");
193+
194+
let error = TrustedServerError::GdprConsent {
195+
message: "no consent string".into(),
196+
};
197+
assert_eq!(error.user_message(), "GDPR consent error");
198+
199+
let error = TrustedServerError::InvalidHeaderValue {
200+
message: "non-ascii".into(),
201+
};
202+
assert_eq!(error.user_message(), "Invalid header value");
203+
204+
let error = TrustedServerError::InvalidUtf8 {
205+
message: "byte 0xff".into(),
206+
};
207+
assert_eq!(error.user_message(), "Invalid request data");
117208
}
118209
}

0 commit comments

Comments
 (0)