1use bytes::Bytes;
2use futures::stream::Stream;
3use hyper::{HeaderMap, StatusCode};
4use reinhardt_core::exception::HttpError;
5use serde::Serialize;
6use std::pin::Pin;
7
8fn safe_error_message(status: StatusCode) -> &'static str {
14 match status.as_u16() {
15 400 => "Bad Request",
16 401 => "Unauthorized",
17 403 => "Forbidden",
18 404 => "Not Found",
19 405 => "Method Not Allowed",
20 406 => "Not Acceptable",
21 408 => "Request Timeout",
22 409 => "Conflict",
23 410 => "Gone",
24 413 => "Payload Too Large",
25 415 => "Unsupported Media Type",
26 422 => "Unprocessable Entity",
27 429 => "Too Many Requests",
28 500 => "Internal Server Error",
30 502 => "Bad Gateway",
31 503 => "Service Unavailable",
32 504 => "Gateway Timeout",
33 _ if status.is_client_error() => "Client Error",
34 _ if status.is_server_error() => "Server Error",
35 _ => "Error",
36 }
37}
38
39fn safe_client_error_detail(error: &crate::Error) -> Option<String> {
45 use crate::Error;
46 match error {
47 Error::Validation(msg) => Some(msg.clone()),
48 Error::Http(msg) => Some(msg.clone()),
49 Error::Serialization(msg) => Some(msg.clone()),
50 Error::ParseError(_) => Some("Invalid request format".to_string()),
51 Error::BodyAlreadyConsumed => Some("Request body has already been consumed".to_string()),
52 Error::MissingContentType => Some("Missing Content-Type header".to_string()),
53 Error::InvalidPage(msg) => Some(format!("Invalid page: {}", msg)),
54 Error::InvalidCursor(_) => Some("Invalid cursor value".to_string()),
55 Error::InvalidLimit(msg) => Some(format!("Invalid limit: {}", msg)),
56 Error::MissingParameter(name) => Some(format!("Missing parameter: {}", name)),
57 Error::Conflict(msg) => Some(msg.clone()),
58 Error::ParamValidation(ctx) => {
59 Some(format!("{} parameter extraction failed", ctx.param_type))
60 }
61 _ => None,
63 }
64}
65
66pub struct SafeErrorResponse {
87 status: StatusCode,
88 detail: Option<String>,
89 debug_info: Option<String>,
90 debug_mode: bool,
91}
92
93impl SafeErrorResponse {
94 pub fn new(status: StatusCode) -> Self {
96 Self {
97 status,
98 detail: None,
99 debug_info: None,
100 debug_mode: false,
101 }
102 }
103
104 pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
109 self.detail = Some(detail.into());
110 self
111 }
112
113 pub fn with_debug_info(mut self, info: impl Into<String>) -> Self {
117 self.debug_info = Some(info.into());
118 self
119 }
120
121 pub fn with_debug_mode(mut self, debug: bool) -> Self {
126 self.debug_mode = debug;
127 self
128 }
129
130 pub fn build(self) -> Response {
132 let message = safe_error_message(self.status);
133 let mut body = serde_json::json!({
134 "error": message,
135 });
136
137 if self.status.is_client_error()
139 && let Some(detail) = &self.detail
140 {
141 body["detail"] = serde_json::Value::String(detail.clone());
142 }
143
144 if self.debug_mode {
146 if let Some(debug_info) = &self.debug_info {
147 body["debug"] = serde_json::Value::String(debug_info.clone());
148 }
149 if self.status.is_server_error()
151 && let Some(detail) = &self.detail
152 {
153 body["detail"] = serde_json::Value::String(detail.clone());
154 }
155 }
156
157 Response::new(self.status)
158 .with_json(&body)
159 .unwrap_or_else(|_| Response::internal_server_error())
160 }
161}
162
163pub fn truncate_for_log(input: &str, max_length: usize) -> String {
180 if input.len() <= max_length {
181 input.to_string()
182 } else {
183 let truncate_at = input
185 .char_indices()
186 .take_while(|&(i, _)| i <= max_length)
187 .last()
188 .map(|(i, _)| i)
189 .unwrap_or(0);
190 format!(
191 "{}...[truncated, {} total bytes]",
192 &input[..truncate_at],
193 input.len()
194 )
195 }
196}
197
198#[derive(Debug, Clone, PartialEq, Eq)]
200pub struct Response {
201 pub status: StatusCode,
203 pub headers: HeaderMap,
205 pub body: Bytes,
207 stop_chain: bool,
210}
211
212pub struct StreamingResponse<S> {
214 pub status: StatusCode,
216 pub headers: HeaderMap,
218 pub stream: S,
220}
221
222pub type StreamBody =
224 Pin<Box<dyn Stream<Item = Result<Bytes, Box<dyn std::error::Error + Send + Sync>>> + Send>>;
225
226impl Response {
227 pub fn new(status: StatusCode) -> Self {
240 Self {
241 status,
242 headers: HeaderMap::new(),
243 body: Bytes::new(),
244 stop_chain: false,
245 }
246 }
247 pub fn ok() -> Self {
259 Self::new(StatusCode::OK)
260 }
261 pub fn created() -> Self {
273 Self::new(StatusCode::CREATED)
274 }
275 pub fn no_content() -> Self {
287 Self::new(StatusCode::NO_CONTENT)
288 }
289 pub fn bad_request() -> Self {
301 Self::new(StatusCode::BAD_REQUEST)
302 }
303 pub fn unauthorized() -> Self {
315 Self::new(StatusCode::UNAUTHORIZED)
316 }
317 pub fn forbidden() -> Self {
329 Self::new(StatusCode::FORBIDDEN)
330 }
331 pub fn not_found() -> Self {
343 Self::new(StatusCode::NOT_FOUND)
344 }
345 pub fn internal_server_error() -> Self {
357 Self::new(StatusCode::INTERNAL_SERVER_ERROR)
358 }
359 pub fn gone() -> Self {
373 Self::new(StatusCode::GONE)
374 }
375 pub fn permanent_redirect(location: impl AsRef<str>) -> Self {
391 Self::new(StatusCode::MOVED_PERMANENTLY).with_location(location.as_ref())
392 }
393 pub fn temporary_redirect(location: impl AsRef<str>) -> Self {
409 Self::new(StatusCode::FOUND).with_location(location.as_ref())
410 }
411 pub fn temporary_redirect_preserve_method(location: impl AsRef<str>) -> Self {
429 Self::new(StatusCode::TEMPORARY_REDIRECT).with_location(location.as_ref())
430 }
431
432 pub fn from_http_error<E>(error: E) -> Self
437 where
438 E: HttpError,
439 {
440 let status = error.status_code();
441 let mut response = SafeErrorResponse::new(status);
442 if status.is_client_error() {
443 response = response.with_detail(error.client_message().into_owned());
444 }
445 response.build()
446 }
447
448 pub fn from_http_error_body<E>(error: E) -> Self
453 where
454 E: HttpError,
455 {
456 let status = error.status_code();
457 let message = error.client_message();
458 let body = serde_json::json!({
459 "error": message.as_ref(),
460 });
461 Self::new(status)
462 .with_json(&body)
463 .unwrap_or_else(|_| Self::internal_server_error())
464 }
465
466 pub fn with_body(mut self, body: impl Into<Bytes>) -> Self {
478 self.body = body.into();
479 self
480 }
481 pub fn try_with_header(mut self, name: &str, value: &str) -> crate::Result<Self> {
507 let header_name = hyper::header::HeaderName::from_bytes(name.as_bytes())
508 .map_err(|e| crate::Error::Http(format!("Invalid header name '{}': {}", name, e)))?;
509 let header_value = hyper::header::HeaderValue::from_str(value).map_err(|e| {
510 crate::Error::Http(format!("Invalid header value for '{}': {}", name, e))
511 })?;
512 self.headers.insert(header_name, header_value);
513 Ok(self)
514 }
515
516 pub fn with_header_if_absent(mut self, name: &str, value: &str) -> Self {
560 if let Ok(header_name) = hyper::header::HeaderName::from_bytes(name.as_bytes())
561 && !self.headers.contains_key(&header_name)
562 && let Ok(header_value) = hyper::header::HeaderValue::from_str(value)
563 {
564 self.headers.insert(header_name, header_value);
565 }
566 self
567 }
568
569 pub fn try_with_header_if_absent(mut self, name: &str, value: &str) -> crate::Result<Self> {
617 let header_name = hyper::header::HeaderName::from_bytes(name.as_bytes())
618 .map_err(|e| crate::Error::Http(format!("Invalid header name '{}': {}", name, e)))?;
619 if !self.headers.contains_key(&header_name) {
620 let header_value = hyper::header::HeaderValue::from_str(value).map_err(|e| {
621 crate::Error::Http(format!("Invalid header value for '{}': {}", name, e))
622 })?;
623 self.headers.insert(header_name, header_value);
624 }
625 Ok(self)
626 }
627
628 pub fn with_header(mut self, name: &str, value: &str) -> Self {
653 if let Ok(header_name) = hyper::header::HeaderName::from_bytes(name.as_bytes())
654 && let Ok(header_value) = hyper::header::HeaderValue::from_str(value)
655 {
656 self.headers.insert(header_name, header_value);
657 }
658 self
659 }
660
661 pub fn append_header(mut self, name: &str, value: &str) -> Self {
680 if let Ok(header_name) = hyper::header::HeaderName::from_bytes(name.as_bytes())
681 && let Ok(header_value) = hyper::header::HeaderValue::from_str(value)
682 {
683 self.headers.append(header_name, header_value);
684 }
685 self
686 }
687
688 pub fn with_location(mut self, location: &str) -> Self {
703 if let Ok(value) = hyper::header::HeaderValue::from_str(location) {
704 self.headers.insert(hyper::header::LOCATION, value);
705 }
706 self
707 }
708 pub fn with_json<T: Serialize>(mut self, data: &T) -> crate::Result<Self> {
725 use crate::Error;
726 let json = serde_json::to_vec(data).map_err(|e| Error::Serialization(e.to_string()))?;
727 self.body = Bytes::from(json);
728 self.headers.insert(
729 hyper::header::CONTENT_TYPE,
730 hyper::header::HeaderValue::from_static("application/json"),
731 );
732 Ok(self)
733 }
734 pub fn with_typed_header(
752 mut self,
753 key: hyper::header::HeaderName,
754 value: hyper::header::HeaderValue,
755 ) -> Self {
756 self.headers.insert(key, value);
757 self
758 }
759
760 pub fn should_stop_chain(&self) -> bool {
776 self.stop_chain
777 }
778
779 pub fn with_stop_chain(mut self, stop: bool) -> Self {
809 self.stop_chain = stop;
810 self
811 }
812}
813
814impl From<crate::Error> for Response {
815 fn from(error: crate::Error) -> Self {
816 let status =
817 StatusCode::from_u16(error.status_code()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
818
819 tracing::error!(
821 status = status.as_u16(),
822 error = %error,
823 "Request error"
824 );
825
826 let mut response = SafeErrorResponse::new(status);
827
828 if status.is_client_error()
831 && let Some(detail) = safe_client_error_detail(&error)
832 {
833 response = response.with_detail(detail);
834 }
835
836 response.build()
837 }
838}
839
840impl<S> StreamingResponse<S>
841where
842 S: Stream<Item = Result<Bytes, Box<dyn std::error::Error + Send + Sync>>> + Send + 'static,
843{
844 pub fn new(stream: S) -> Self {
861 Self {
862 status: StatusCode::OK,
863 headers: HeaderMap::new(),
864 stream,
865 }
866 }
867 pub fn with_status(stream: S, status: StatusCode) -> Self {
884 Self {
885 status,
886 headers: HeaderMap::new(),
887 stream,
888 }
889 }
890 pub fn status(mut self, status: StatusCode) -> Self {
907 self.status = status;
908 self
909 }
910 pub fn header(
931 mut self,
932 key: hyper::header::HeaderName,
933 value: hyper::header::HeaderValue,
934 ) -> Self {
935 self.headers.insert(key, value);
936 self
937 }
938 pub fn media_type(self, media_type: &str) -> Self {
958 self.header(
959 hyper::header::CONTENT_TYPE,
960 hyper::header::HeaderValue::from_str(media_type).unwrap_or_else(|_| {
961 hyper::header::HeaderValue::from_static("application/octet-stream")
962 }),
963 )
964 }
965}
966
967impl<S> StreamingResponse<S> {
968 pub fn into_stream(self) -> S {
988 self.stream
989 }
990}
991
992#[cfg(test)]
993mod tests {
994 use super::*;
995 use rstest::rstest;
996
997 #[derive(Debug)]
998 enum SampleHttpError {
999 Validation,
1000 Internal,
1001 }
1002
1003 impl reinhardt_core::exception::HttpError for SampleHttpError {
1004 fn status_code(&self) -> reinhardt_core::exception::StatusCode {
1005 match self {
1006 Self::Validation => reinhardt_core::exception::StatusCode::BAD_REQUEST,
1007 Self::Internal => reinhardt_core::exception::StatusCode::INTERNAL_SERVER_ERROR,
1008 }
1009 }
1010
1011 fn client_message(&self) -> std::borrow::Cow<'static, str> {
1012 match self {
1013 Self::Validation => std::borrow::Cow::Borrowed("Invalid request"),
1014 Self::Internal => std::borrow::Cow::Borrowed("Provider token expired"),
1015 }
1016 }
1017 }
1018
1019 #[rstest]
1020 fn test_http_error_safe_response_includes_4xx_detail() {
1021 let error = SampleHttpError::Validation;
1023
1024 let response = Response::from_http_error(error);
1026 let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
1027
1028 assert_eq!(response.status, StatusCode::BAD_REQUEST);
1030 assert_eq!(body["error"], "Bad Request");
1031 assert_eq!(body["detail"], "Invalid request");
1032 }
1033
1034 #[rstest]
1035 fn test_http_error_safe_response_hides_5xx_detail() {
1036 let error = SampleHttpError::Internal;
1038
1039 let response = Response::from_http_error(error);
1041 let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
1042
1043 assert_eq!(response.status, StatusCode::INTERNAL_SERVER_ERROR);
1045 assert_eq!(body["error"], "Internal Server Error");
1046 assert_eq!(body.get("detail"), None);
1047 }
1048
1049 #[rstest]
1050 fn test_http_error_body_response_uses_client_message_envelope() {
1051 let error = SampleHttpError::Internal;
1053
1054 let response = Response::from_http_error_body(error);
1056 let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
1057
1058 assert_eq!(response.status, StatusCode::INTERNAL_SERVER_ERROR);
1060 assert_eq!(body, serde_json::json!({"error": "Provider token expired"}));
1061 }
1062
1063 #[rstest]
1064 #[case(StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error")]
1065 #[case(StatusCode::BAD_GATEWAY, "Bad Gateway")]
1066 #[case(StatusCode::SERVICE_UNAVAILABLE, "Service Unavailable")]
1067 #[case(StatusCode::GATEWAY_TIMEOUT, "Gateway Timeout")]
1068 fn test_5xx_errors_never_include_internal_details(
1069 #[case] status: StatusCode,
1070 #[case] expected_message: &str,
1071 ) {
1072 let sensitive_detail = "Internal path /src/db/connection.rs:42 failed";
1074
1075 let response = SafeErrorResponse::new(status)
1077 .with_detail(sensitive_detail)
1078 .build();
1079
1080 let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
1082 assert_eq!(body["error"], expected_message);
1083 assert!(body.get("detail").is_none());
1085 assert_eq!(response.status, status);
1086 }
1087
1088 #[rstest]
1089 #[case(StatusCode::BAD_REQUEST, "Bad Request")]
1090 #[case(StatusCode::UNAUTHORIZED, "Unauthorized")]
1091 #[case(StatusCode::FORBIDDEN, "Forbidden")]
1092 #[case(StatusCode::NOT_FOUND, "Not Found")]
1093 #[case(StatusCode::METHOD_NOT_ALLOWED, "Method Not Allowed")]
1094 #[case(StatusCode::CONFLICT, "Conflict")]
1095 fn test_4xx_errors_include_safe_detail(
1096 #[case] status: StatusCode,
1097 #[case] expected_message: &str,
1098 ) {
1099 let detail = "Missing required field: name";
1101
1102 let response = SafeErrorResponse::new(status).with_detail(detail).build();
1104
1105 let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
1107 assert_eq!(body["error"], expected_message);
1108 assert_eq!(body["detail"], detail);
1109 assert_eq!(response.status, status);
1110 }
1111
1112 #[rstest]
1113 fn test_debug_mode_includes_full_error_info() {
1114 let debug_info = "Error at src/handlers/user.rs:42: column 'email' not found";
1116
1117 let response = SafeErrorResponse::new(StatusCode::INTERNAL_SERVER_ERROR)
1119 .with_detail("Database query failed")
1120 .with_debug_info(debug_info)
1121 .with_debug_mode(true)
1122 .build();
1123
1124 let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
1126 assert_eq!(body["error"], "Internal Server Error");
1127 assert_eq!(body["detail"], "Database query failed");
1129 assert_eq!(body["debug"], debug_info);
1130 }
1131
1132 #[rstest]
1133 fn test_debug_mode_disabled_excludes_debug_info() {
1134 let debug_info = "Sensitive internal detail";
1136
1137 let response = SafeErrorResponse::new(StatusCode::INTERNAL_SERVER_ERROR)
1139 .with_debug_info(debug_info)
1140 .with_debug_mode(false)
1141 .build();
1142
1143 let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
1145 assert!(body.get("debug").is_none());
1146 }
1147
1148 #[rstest]
1149 #[case(StatusCode::BAD_REQUEST, "Bad Request")]
1150 #[case(StatusCode::UNAUTHORIZED, "Unauthorized")]
1151 #[case(StatusCode::FORBIDDEN, "Forbidden")]
1152 #[case(StatusCode::NOT_FOUND, "Not Found")]
1153 #[case(StatusCode::METHOD_NOT_ALLOWED, "Method Not Allowed")]
1154 #[case(StatusCode::NOT_ACCEPTABLE, "Not Acceptable")]
1155 #[case(StatusCode::REQUEST_TIMEOUT, "Request Timeout")]
1156 #[case(StatusCode::CONFLICT, "Conflict")]
1157 #[case(StatusCode::GONE, "Gone")]
1158 #[case(StatusCode::PAYLOAD_TOO_LARGE, "Payload Too Large")]
1159 #[case(StatusCode::UNSUPPORTED_MEDIA_TYPE, "Unsupported Media Type")]
1160 #[case(StatusCode::UNPROCESSABLE_ENTITY, "Unprocessable Entity")]
1161 #[case(StatusCode::TOO_MANY_REQUESTS, "Too Many Requests")]
1162 #[case(StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error")]
1163 #[case(StatusCode::BAD_GATEWAY, "Bad Gateway")]
1164 #[case(StatusCode::SERVICE_UNAVAILABLE, "Service Unavailable")]
1165 #[case(StatusCode::GATEWAY_TIMEOUT, "Gateway Timeout")]
1166 fn test_safe_error_message_returns_correct_messages(
1167 #[case] status: StatusCode,
1168 #[case] expected: &str,
1169 ) {
1170 let message = safe_error_message(status);
1172
1173 assert_eq!(message, expected);
1175 }
1176
1177 #[rstest]
1178 fn test_safe_error_message_fallback_client_error() {
1179 let status = StatusCode::IM_A_TEAPOT;
1182
1183 let message = safe_error_message(status);
1185
1186 assert_eq!(message, "Client Error");
1188 }
1189
1190 #[rstest]
1191 fn test_safe_error_message_fallback_server_error() {
1192 let status = StatusCode::HTTP_VERSION_NOT_SUPPORTED;
1195
1196 let message = safe_error_message(status);
1198
1199 assert_eq!(message, "Server Error");
1201 }
1202
1203 #[rstest]
1204 fn test_truncate_for_log_short_string() {
1205 let input = "hello";
1207
1208 let result = truncate_for_log(input, 10);
1210
1211 assert_eq!(result, "hello");
1213 }
1214
1215 #[rstest]
1216 fn test_truncate_for_log_long_string() {
1217 let input = "a".repeat(100);
1219
1220 let result = truncate_for_log(&input, 10);
1222
1223 assert!(result.starts_with("aaaaaaaaaa"));
1225 assert!(result.contains("...[truncated, 100 total bytes]"));
1226 }
1227
1228 #[rstest]
1229 fn test_truncate_for_log_exact_length() {
1230 let input = "abcde";
1232
1233 let result = truncate_for_log(input, 5);
1235
1236 assert_eq!(result, "abcde");
1238 }
1239
1240 #[rstest]
1241 fn test_truncate_for_log_multi_byte_utf8_does_not_panic() {
1242 let input = "日本語テスト文字列";
1245
1246 let result = truncate_for_log(input, 4);
1248
1249 assert!(result.starts_with("日"));
1251 assert!(result.contains("...[truncated"));
1252 }
1253
1254 #[rstest]
1255 fn test_truncate_for_log_emoji_boundary() {
1256 let input = "🦀🐍🐹🐿️";
1259
1260 let result = truncate_for_log(input, 5);
1262
1263 assert!(result.starts_with("🦀"));
1265 assert!(result.contains("...[truncated"));
1266 }
1267
1268 #[rstest]
1269 fn test_truncate_for_log_mixed_ascii_and_multibyte() {
1270 let input = "abc日本語def";
1272
1273 let result = truncate_for_log(input, 5);
1275
1276 assert!(result.starts_with("abc"));
1278 assert!(result.contains("...[truncated"));
1279 }
1280
1281 #[rstest]
1282 fn test_truncate_for_log_zero_max_length() {
1283 let input = "hello";
1285
1286 let result = truncate_for_log(input, 0);
1288
1289 assert!(result.starts_with("...[truncated"));
1291 }
1292
1293 #[rstest]
1294 fn test_from_error_produces_safe_output_for_5xx() {
1295 let error = crate::Error::Database(
1297 "Connection to postgres://user:pass@db:5432/mydb failed".to_string(),
1298 );
1299
1300 let response: Response = error.into();
1302
1303 assert_eq!(response.status, StatusCode::INTERNAL_SERVER_ERROR);
1305 let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
1306 assert_eq!(body["error"], "Internal Server Error");
1307 let body_str = String::from_utf8_lossy(&response.body);
1309 assert!(!body_str.contains("postgres://"));
1310 assert!(!body_str.contains("user:pass"));
1311 assert!(body.get("detail").is_none());
1312 }
1313
1314 #[rstest]
1315 fn test_from_error_produces_safe_output_for_4xx_validation() {
1316 let error = crate::Error::Validation("Email format is invalid".to_string());
1318
1319 let response: Response = error.into();
1321
1322 assert_eq!(response.status, StatusCode::BAD_REQUEST);
1324 let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
1325 assert_eq!(body["error"], "Bad Request");
1326 assert_eq!(body["detail"], "Email format is invalid");
1327 }
1328
1329 #[rstest]
1330 fn test_from_error_produces_safe_output_for_4xx_parse() {
1331 let error = crate::Error::ParseError(
1333 "invalid digit found in string at src/parser.rs:42".to_string(),
1334 );
1335
1336 let response: Response = error.into();
1338
1339 assert_eq!(response.status, StatusCode::BAD_REQUEST);
1341 let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
1342 assert_eq!(body["error"], "Bad Request");
1343 assert_eq!(body["detail"], "Invalid request format");
1345 let body_str = String::from_utf8_lossy(&response.body);
1346 assert!(!body_str.contains("src/parser.rs"));
1347 }
1348
1349 #[rstest]
1350 fn test_from_error_body_already_consumed() {
1351 let error = crate::Error::BodyAlreadyConsumed;
1353
1354 let response: Response = error.into();
1356
1357 assert_eq!(response.status, StatusCode::BAD_REQUEST);
1359 let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
1360 assert_eq!(body["detail"], "Request body has already been consumed");
1361 }
1362
1363 #[rstest]
1364 fn test_from_error_internal_error_hides_details() {
1365 let error =
1367 crate::Error::Internal("panic at /Users/dev/projects/app/src/main.rs:10".to_string());
1368
1369 let response: Response = error.into();
1371
1372 assert_eq!(response.status, StatusCode::INTERNAL_SERVER_ERROR);
1374 let body_str = String::from_utf8_lossy(&response.body);
1375 assert!(!body_str.contains("/Users/dev"));
1376 assert!(!body_str.contains("main.rs"));
1377 }
1378
1379 #[rstest]
1380 fn test_safe_error_response_no_detail_set() {
1381 let response = SafeErrorResponse::new(StatusCode::BAD_REQUEST).build();
1383
1384 let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
1386 assert_eq!(body["error"], "Bad Request");
1387 assert!(body.get("detail").is_none());
1388 }
1389
1390 #[rstest]
1391 fn test_safe_error_response_content_type_is_json() {
1392 let response = SafeErrorResponse::new(StatusCode::NOT_FOUND).build();
1394
1395 let content_type = response
1397 .headers
1398 .get("content-type")
1399 .unwrap()
1400 .to_str()
1401 .unwrap();
1402 assert_eq!(content_type, "application/json");
1403 }
1404
1405 #[rstest]
1410 fn test_with_header_invalid_name_does_not_panic() {
1411 let response = Response::ok();
1413
1414 let response = response.with_header("Invalid Header", "value");
1416
1417 assert!(response.headers.is_empty());
1419 }
1420
1421 #[rstest]
1422 fn test_with_header_invalid_value_does_not_panic() {
1423 let response = Response::ok();
1425
1426 let response = response.with_header("X-Test", "value\x00with\x01control");
1428
1429 assert!(response.headers.get("X-Test").is_none());
1431 }
1432
1433 #[rstest]
1434 fn test_with_header_valid_header_works() {
1435 let response = Response::ok();
1437
1438 let response = response.with_header("X-Custom", "custom-value");
1440
1441 assert_eq!(
1443 response.headers.get("X-Custom").unwrap().to_str().unwrap(),
1444 "custom-value"
1445 );
1446 }
1447
1448 #[rstest]
1449 fn test_try_with_header_invalid_name_returns_error() {
1450 let response = Response::ok();
1452
1453 let result = response.try_with_header("Invalid Header", "value");
1455
1456 assert!(result.is_err());
1458 }
1459
1460 #[rstest]
1461 fn test_try_with_header_valid_header_returns_ok() {
1462 let response = Response::ok();
1464
1465 let result = response.try_with_header("X-Custom", "valid-value");
1467
1468 assert!(result.is_ok());
1470 let response = result.unwrap();
1471 assert_eq!(
1472 response.headers.get("X-Custom").unwrap().to_str().unwrap(),
1473 "valid-value"
1474 );
1475 }
1476
1477 #[rstest]
1478 fn test_append_header_adds_multiple_values() {
1479 let response = Response::ok()
1481 .append_header("Set-Cookie", "a=1; Path=/")
1482 .append_header("Set-Cookie", "b=2; Path=/");
1483
1484 let cookies: Vec<_> = response.headers.get_all("set-cookie").iter().collect();
1486 assert_eq!(cookies.len(), 2);
1487 assert_eq!(cookies[0].to_str().unwrap(), "a=1; Path=/");
1488 assert_eq!(cookies[1].to_str().unwrap(), "b=2; Path=/");
1489 }
1490
1491 #[rstest]
1492 fn test_append_header_coexists_with_with_header() {
1493 let response = Response::ok()
1495 .with_header("Set-Cookie", "a=1; Path=/")
1496 .append_header("Set-Cookie", "b=2; Path=/");
1497
1498 let cookies: Vec<_> = response.headers.get_all("set-cookie").iter().collect();
1500 assert_eq!(cookies.len(), 2);
1501 }
1502}