reinhardt_http/response.rs
1use bytes::Bytes;
2use futures::stream::Stream;
3use hyper::{HeaderMap, StatusCode};
4use serde::Serialize;
5use std::pin::Pin;
6
7/// Returns a safe, client-facing error message based on the HTTP status code.
8///
9/// For 5xx errors, always returns a generic message to prevent information leakage.
10/// For 4xx errors, returns a descriptive but safe category message.
11/// Internal details are never exposed to clients.
12fn safe_error_message(status: StatusCode) -> &'static str {
13 match status.as_u16() {
14 400 => "Bad Request",
15 401 => "Unauthorized",
16 403 => "Forbidden",
17 404 => "Not Found",
18 405 => "Method Not Allowed",
19 406 => "Not Acceptable",
20 408 => "Request Timeout",
21 409 => "Conflict",
22 410 => "Gone",
23 413 => "Payload Too Large",
24 415 => "Unsupported Media Type",
25 422 => "Unprocessable Entity",
26 429 => "Too Many Requests",
27 // All 5xx errors get generic messages
28 500 => "Internal Server Error",
29 502 => "Bad Gateway",
30 503 => "Service Unavailable",
31 504 => "Gateway Timeout",
32 _ if status.is_client_error() => "Client Error",
33 _ if status.is_server_error() => "Server Error",
34 _ => "Error",
35 }
36}
37
38/// Extract a safe, client-facing detail message from an error.
39///
40/// Returns `None` if no safe detail can be extracted.
41/// Only extracts details from error variants where the message is
42/// controlled by application code and safe for client exposure.
43fn safe_client_error_detail(error: &crate::Error) -> Option<String> {
44 use crate::Error;
45 match error {
46 Error::Validation(msg) => Some(msg.clone()),
47 Error::Http(msg) => Some(msg.clone()),
48 Error::Serialization(msg) => Some(msg.clone()),
49 Error::ParseError(_) => Some("Invalid request format".to_string()),
50 Error::BodyAlreadyConsumed => Some("Request body has already been consumed".to_string()),
51 Error::MissingContentType => Some("Missing Content-Type header".to_string()),
52 Error::InvalidPage(msg) => Some(format!("Invalid page: {}", msg)),
53 Error::InvalidCursor(_) => Some("Invalid cursor value".to_string()),
54 Error::InvalidLimit(msg) => Some(format!("Invalid limit: {}", msg)),
55 Error::MissingParameter(name) => Some(format!("Missing parameter: {}", name)),
56 Error::Conflict(msg) => Some(msg.clone()),
57 Error::ParamValidation(ctx) => {
58 Some(format!("{} parameter extraction failed", ctx.param_type))
59 }
60 // For other client errors, return generic message
61 _ => None,
62 }
63}
64
65/// Builder for creating error responses that prevent information leakage.
66///
67/// In production mode (default), error responses contain only safe,
68/// generic messages. In debug mode, full error details are included.
69///
70/// # Examples
71///
72/// ```
73/// use reinhardt_http::response::SafeErrorResponse;
74/// use hyper::StatusCode;
75///
76/// // Production-safe response
77/// let response = SafeErrorResponse::new(StatusCode::INTERNAL_SERVER_ERROR)
78/// .build();
79///
80/// // Debug response with details
81/// let response = SafeErrorResponse::new(StatusCode::BAD_REQUEST)
82/// .with_detail("Missing required field: name")
83/// .build();
84/// ```
85pub struct SafeErrorResponse {
86 status: StatusCode,
87 detail: Option<String>,
88 debug_info: Option<String>,
89 debug_mode: bool,
90}
91
92impl SafeErrorResponse {
93 /// Create a new `SafeErrorResponse` with the given HTTP status code.
94 pub fn new(status: StatusCode) -> Self {
95 Self {
96 status,
97 detail: None,
98 debug_info: None,
99 debug_mode: false,
100 }
101 }
102
103 /// Add a safe, client-facing detail message.
104 ///
105 /// Only included for 4xx errors. Ignored for 5xx errors to prevent
106 /// accidental information leakage.
107 pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
108 self.detail = Some(detail.into());
109 self
110 }
111
112 /// Add debug information (only included when debug_mode is true).
113 ///
114 /// WARNING: Only use in development environments.
115 pub fn with_debug_info(mut self, info: impl Into<String>) -> Self {
116 self.debug_info = Some(info.into());
117 self
118 }
119
120 /// Enable debug mode to include full error details in responses.
121 ///
122 /// WARNING: Only use in development environments. Debug mode exposes
123 /// internal error details that may leak sensitive information.
124 pub fn with_debug_mode(mut self, debug: bool) -> Self {
125 self.debug_mode = debug;
126 self
127 }
128
129 /// Build the `Response` with safe error content.
130 pub fn build(self) -> Response {
131 let message = safe_error_message(self.status);
132 let mut body = serde_json::json!({
133 "error": message,
134 });
135
136 // Only include detail for 4xx errors to prevent info leakage on 5xx
137 if self.status.is_client_error()
138 && let Some(detail) = &self.detail
139 {
140 body["detail"] = serde_json::Value::String(detail.clone());
141 }
142
143 // Include debug info only when explicitly enabled
144 if self.debug_mode {
145 if let Some(debug_info) = &self.debug_info {
146 body["debug"] = serde_json::Value::String(debug_info.clone());
147 }
148 // In debug mode, include detail even for 5xx
149 if self.status.is_server_error()
150 && let Some(detail) = &self.detail
151 {
152 body["detail"] = serde_json::Value::String(detail.clone());
153 }
154 }
155
156 Response::new(self.status)
157 .with_json(&body)
158 .unwrap_or_else(|_| Response::internal_server_error())
159 }
160}
161
162/// Truncate a string for safe inclusion in log messages.
163///
164/// Prevents oversized values from consuming log storage and
165/// limits exposure of sensitive data in error contexts.
166///
167/// # Examples
168///
169/// ```
170/// use reinhardt_http::response::truncate_for_log;
171///
172/// let short = truncate_for_log("hello", 10);
173/// assert_eq!(short, "hello");
174///
175/// let long = truncate_for_log("a]".repeat(100).as_str(), 10);
176/// assert!(long.contains("...[truncated"));
177/// ```
178pub fn truncate_for_log(input: &str, max_length: usize) -> String {
179 if input.len() <= max_length {
180 input.to_string()
181 } else {
182 // Find valid UTF-8 boundary at or before max_length
183 let truncate_at = input
184 .char_indices()
185 .take_while(|&(i, _)| i <= max_length)
186 .last()
187 .map(|(i, _)| i)
188 .unwrap_or(0);
189 format!(
190 "{}...[truncated, {} total bytes]",
191 &input[..truncate_at],
192 input.len()
193 )
194 }
195}
196
197/// HTTP Response representation
198#[derive(Debug, Clone, PartialEq, Eq)]
199pub struct Response {
200 /// The HTTP status code.
201 pub status: StatusCode,
202 /// The response headers.
203 pub headers: HeaderMap,
204 /// The response body as raw bytes.
205 pub body: Bytes,
206 /// Indicates whether the middleware chain should stop processing
207 /// When true, no further middleware or handlers will be executed
208 stop_chain: bool,
209}
210
211/// Streaming HTTP Response
212pub struct StreamingResponse<S> {
213 /// The HTTP status code.
214 pub status: StatusCode,
215 /// The response headers.
216 pub headers: HeaderMap,
217 /// The streaming body source.
218 pub stream: S,
219}
220
221/// Type alias for streaming body
222pub type StreamBody =
223 Pin<Box<dyn Stream<Item = Result<Bytes, Box<dyn std::error::Error + Send + Sync>>> + Send>>;
224
225impl Response {
226 /// Create a new Response with the given status code
227 ///
228 /// # Examples
229 ///
230 /// ```
231 /// use reinhardt_http::Response;
232 /// use hyper::StatusCode;
233 ///
234 /// let response = Response::new(StatusCode::OK);
235 /// assert_eq!(response.status, StatusCode::OK);
236 /// assert!(response.body.is_empty());
237 /// ```
238 pub fn new(status: StatusCode) -> Self {
239 Self {
240 status,
241 headers: HeaderMap::new(),
242 body: Bytes::new(),
243 stop_chain: false,
244 }
245 }
246 /// Create a Response with HTTP 200 OK status
247 ///
248 /// # Examples
249 ///
250 /// ```
251 /// use reinhardt_http::Response;
252 /// use hyper::StatusCode;
253 ///
254 /// let response = Response::ok();
255 /// assert_eq!(response.status, StatusCode::OK);
256 /// ```
257 pub fn ok() -> Self {
258 Self::new(StatusCode::OK)
259 }
260 /// Create a Response with HTTP 201 Created status
261 ///
262 /// # Examples
263 ///
264 /// ```
265 /// use reinhardt_http::Response;
266 /// use hyper::StatusCode;
267 ///
268 /// let response = Response::created();
269 /// assert_eq!(response.status, StatusCode::CREATED);
270 /// ```
271 pub fn created() -> Self {
272 Self::new(StatusCode::CREATED)
273 }
274 /// Create a Response with HTTP 204 No Content status
275 ///
276 /// # Examples
277 ///
278 /// ```
279 /// use reinhardt_http::Response;
280 /// use hyper::StatusCode;
281 ///
282 /// let response = Response::no_content();
283 /// assert_eq!(response.status, StatusCode::NO_CONTENT);
284 /// ```
285 pub fn no_content() -> Self {
286 Self::new(StatusCode::NO_CONTENT)
287 }
288 /// Create a Response with HTTP 400 Bad Request status
289 ///
290 /// # Examples
291 ///
292 /// ```
293 /// use reinhardt_http::Response;
294 /// use hyper::StatusCode;
295 ///
296 /// let response = Response::bad_request();
297 /// assert_eq!(response.status, StatusCode::BAD_REQUEST);
298 /// ```
299 pub fn bad_request() -> Self {
300 Self::new(StatusCode::BAD_REQUEST)
301 }
302 /// Create a Response with HTTP 401 Unauthorized status
303 ///
304 /// # Examples
305 ///
306 /// ```
307 /// use reinhardt_http::Response;
308 /// use hyper::StatusCode;
309 ///
310 /// let response = Response::unauthorized();
311 /// assert_eq!(response.status, StatusCode::UNAUTHORIZED);
312 /// ```
313 pub fn unauthorized() -> Self {
314 Self::new(StatusCode::UNAUTHORIZED)
315 }
316 /// Create a Response with HTTP 403 Forbidden status
317 ///
318 /// # Examples
319 ///
320 /// ```
321 /// use reinhardt_http::Response;
322 /// use hyper::StatusCode;
323 ///
324 /// let response = Response::forbidden();
325 /// assert_eq!(response.status, StatusCode::FORBIDDEN);
326 /// ```
327 pub fn forbidden() -> Self {
328 Self::new(StatusCode::FORBIDDEN)
329 }
330 /// Create a Response with HTTP 404 Not Found status
331 ///
332 /// # Examples
333 ///
334 /// ```
335 /// use reinhardt_http::Response;
336 /// use hyper::StatusCode;
337 ///
338 /// let response = Response::not_found();
339 /// assert_eq!(response.status, StatusCode::NOT_FOUND);
340 /// ```
341 pub fn not_found() -> Self {
342 Self::new(StatusCode::NOT_FOUND)
343 }
344 /// Create a Response with HTTP 500 Internal Server Error status
345 ///
346 /// # Examples
347 ///
348 /// ```
349 /// use reinhardt_http::Response;
350 /// use hyper::StatusCode;
351 ///
352 /// let response = Response::internal_server_error();
353 /// assert_eq!(response.status, StatusCode::INTERNAL_SERVER_ERROR);
354 /// ```
355 pub fn internal_server_error() -> Self {
356 Self::new(StatusCode::INTERNAL_SERVER_ERROR)
357 }
358 /// Create a Response with HTTP 410 Gone status
359 ///
360 /// Used when a resource has been permanently removed.
361 ///
362 /// # Examples
363 ///
364 /// ```
365 /// use reinhardt_http::Response;
366 /// use hyper::StatusCode;
367 ///
368 /// let response = Response::gone();
369 /// assert_eq!(response.status, StatusCode::GONE);
370 /// ```
371 pub fn gone() -> Self {
372 Self::new(StatusCode::GONE)
373 }
374 /// Create a Response with HTTP 301 Moved Permanently (permanent redirect)
375 ///
376 /// # Examples
377 ///
378 /// ```
379 /// use reinhardt_http::Response;
380 /// use hyper::StatusCode;
381 ///
382 /// let response = Response::permanent_redirect("/new-location");
383 /// assert_eq!(response.status, StatusCode::MOVED_PERMANENTLY);
384 /// assert_eq!(
385 /// response.headers.get("location").unwrap().to_str().unwrap(),
386 /// "/new-location"
387 /// );
388 /// ```
389 pub fn permanent_redirect(location: impl AsRef<str>) -> Self {
390 Self::new(StatusCode::MOVED_PERMANENTLY).with_location(location.as_ref())
391 }
392 /// Create a Response with HTTP 302 Found (temporary redirect)
393 ///
394 /// # Examples
395 ///
396 /// ```
397 /// use reinhardt_http::Response;
398 /// use hyper::StatusCode;
399 ///
400 /// let response = Response::temporary_redirect("/temp-location");
401 /// assert_eq!(response.status, StatusCode::FOUND);
402 /// assert_eq!(
403 /// response.headers.get("location").unwrap().to_str().unwrap(),
404 /// "/temp-location"
405 /// );
406 /// ```
407 pub fn temporary_redirect(location: impl AsRef<str>) -> Self {
408 Self::new(StatusCode::FOUND).with_location(location.as_ref())
409 }
410 /// Create a Response with HTTP 307 Temporary Redirect (preserves HTTP method)
411 ///
412 /// Unlike 302, this guarantees the request method is preserved during redirect.
413 ///
414 /// # Examples
415 ///
416 /// ```
417 /// use reinhardt_http::Response;
418 /// use hyper::StatusCode;
419 ///
420 /// let response = Response::temporary_redirect_preserve_method("/temp-location");
421 /// assert_eq!(response.status, StatusCode::TEMPORARY_REDIRECT);
422 /// assert_eq!(
423 /// response.headers.get("location").unwrap().to_str().unwrap(),
424 /// "/temp-location"
425 /// );
426 /// ```
427 pub fn temporary_redirect_preserve_method(location: impl AsRef<str>) -> Self {
428 Self::new(StatusCode::TEMPORARY_REDIRECT).with_location(location.as_ref())
429 }
430 /// Set the response body
431 ///
432 /// # Examples
433 ///
434 /// ```
435 /// use reinhardt_http::Response;
436 /// use bytes::Bytes;
437 ///
438 /// let response = Response::ok().with_body("Hello, World!");
439 /// assert_eq!(response.body, Bytes::from("Hello, World!"));
440 /// ```
441 pub fn with_body(mut self, body: impl Into<Bytes>) -> Self {
442 self.body = body.into();
443 self
444 }
445 /// Try to add a custom header to the response, returning an error on invalid inputs.
446 ///
447 /// # Errors
448 ///
449 /// Returns `Err` if the header name or value is invalid according to HTTP specifications.
450 ///
451 /// # Examples
452 ///
453 /// ```
454 /// use reinhardt_http::Response;
455 ///
456 /// let response = Response::ok().try_with_header("X-Custom-Header", "custom-value").unwrap();
457 /// assert_eq!(
458 /// response.headers.get("X-Custom-Header").unwrap().to_str().unwrap(),
459 /// "custom-value"
460 /// );
461 /// ```
462 ///
463 /// ```
464 /// use reinhardt_http::Response;
465 ///
466 /// // Invalid header names return an error instead of panicking
467 /// let result = Response::ok().try_with_header("Invalid Header", "value");
468 /// assert!(result.is_err());
469 /// ```
470 pub fn try_with_header(mut self, name: &str, value: &str) -> crate::Result<Self> {
471 let header_name = hyper::header::HeaderName::from_bytes(name.as_bytes())
472 .map_err(|e| crate::Error::Http(format!("Invalid header name '{}': {}", name, e)))?;
473 let header_value = hyper::header::HeaderValue::from_str(value).map_err(|e| {
474 crate::Error::Http(format!("Invalid header value for '{}': {}", name, e))
475 })?;
476 self.headers.insert(header_name, header_value);
477 Ok(self)
478 }
479
480 /// Add a custom header to the response only if it is not already present.
481 ///
482 /// If the header already exists, the existing value is preserved.
483 /// Invalid header names or values are silently ignored.
484 /// Use [`try_with_header_if_absent`](Self::try_with_header_if_absent) if you need error
485 /// reporting.
486 ///
487 /// This is useful in middleware that should not overwrite headers already set
488 /// by handlers (e.g., handler-specific CSP headers should survive middleware processing).
489 ///
490 /// # Examples
491 ///
492 /// ```
493 /// use reinhardt_http::Response;
494 ///
495 /// // Inserts the header when it is not already present
496 /// let response = Response::ok().with_header_if_absent("X-Custom", "first");
497 /// assert_eq!(
498 /// response.headers.get("X-Custom").unwrap().to_str().unwrap(),
499 /// "first"
500 /// );
501 /// ```
502 ///
503 /// ```
504 /// use reinhardt_http::Response;
505 ///
506 /// // Preserves the existing header value
507 /// let response = Response::ok()
508 /// .with_header("X-Custom", "original")
509 /// .with_header_if_absent("X-Custom", "overwrite-attempt");
510 /// assert_eq!(
511 /// response.headers.get("X-Custom").unwrap().to_str().unwrap(),
512 /// "original"
513 /// );
514 /// ```
515 ///
516 /// ```
517 /// use reinhardt_http::Response;
518 ///
519 /// // Invalid header names are silently ignored (no panic)
520 /// let response = Response::ok().with_header_if_absent("Invalid Header", "value");
521 /// assert!(response.headers.is_empty());
522 /// ```
523 pub fn with_header_if_absent(mut self, name: &str, value: &str) -> Self {
524 if let Ok(header_name) = hyper::header::HeaderName::from_bytes(name.as_bytes())
525 && !self.headers.contains_key(&header_name)
526 && let Ok(header_value) = hyper::header::HeaderValue::from_str(value)
527 {
528 self.headers.insert(header_name, header_value);
529 }
530 self
531 }
532
533 /// Add a custom header to the response only if it is not already present,
534 /// returning an error for invalid header names or values.
535 ///
536 /// If the header already exists, the existing value is preserved and `Ok(self)`
537 /// is returned without modification.
538 ///
539 /// # Errors
540 ///
541 /// Returns an error if the header name or value is invalid.
542 ///
543 /// # Examples
544 ///
545 /// ```
546 /// use reinhardt_http::Response;
547 ///
548 /// // Inserts when absent
549 /// let response = Response::ok()
550 /// .try_with_header_if_absent("X-Custom", "value")
551 /// .unwrap();
552 /// assert_eq!(
553 /// response.headers.get("X-Custom").unwrap().to_str().unwrap(),
554 /// "value"
555 /// );
556 /// ```
557 ///
558 /// ```
559 /// use reinhardt_http::Response;
560 ///
561 /// // Preserves existing header value
562 /// let response = Response::ok()
563 /// .try_with_header("X-Custom", "original")
564 /// .unwrap()
565 /// .try_with_header_if_absent("X-Custom", "overwrite-attempt")
566 /// .unwrap();
567 /// assert_eq!(
568 /// response.headers.get("X-Custom").unwrap().to_str().unwrap(),
569 /// "original"
570 /// );
571 /// ```
572 ///
573 /// ```
574 /// use reinhardt_http::Response;
575 ///
576 /// // Invalid header names return an error
577 /// let result = Response::ok().try_with_header_if_absent("Invalid Header", "value");
578 /// assert!(result.is_err());
579 /// ```
580 pub fn try_with_header_if_absent(mut self, name: &str, value: &str) -> crate::Result<Self> {
581 let header_name = hyper::header::HeaderName::from_bytes(name.as_bytes())
582 .map_err(|e| crate::Error::Http(format!("Invalid header name '{}': {}", name, e)))?;
583 if !self.headers.contains_key(&header_name) {
584 let header_value = hyper::header::HeaderValue::from_str(value).map_err(|e| {
585 crate::Error::Http(format!("Invalid header value for '{}': {}", name, e))
586 })?;
587 self.headers.insert(header_name, header_value);
588 }
589 Ok(self)
590 }
591
592 /// Add a custom header to the response.
593 ///
594 /// Invalid header names or values are silently ignored.
595 /// Use [`try_with_header`](Self::try_with_header) if you need error reporting.
596 ///
597 /// # Examples
598 ///
599 /// ```
600 /// use reinhardt_http::Response;
601 ///
602 /// let response = Response::ok().with_header("X-Custom-Header", "custom-value");
603 /// assert_eq!(
604 /// response.headers.get("X-Custom-Header").unwrap().to_str().unwrap(),
605 /// "custom-value"
606 /// );
607 /// ```
608 ///
609 /// ```
610 /// use reinhardt_http::Response;
611 ///
612 /// // Invalid header names are silently ignored (no panic)
613 /// let response = Response::ok().with_header("Invalid Header", "value");
614 /// assert!(response.headers.is_empty());
615 /// ```
616 pub fn with_header(mut self, name: &str, value: &str) -> Self {
617 if let Ok(header_name) = hyper::header::HeaderName::from_bytes(name.as_bytes())
618 && let Ok(header_value) = hyper::header::HeaderValue::from_str(value)
619 {
620 self.headers.insert(header_name, header_value);
621 }
622 self
623 }
624
625 /// Append a header value without replacing existing values.
626 ///
627 /// Unlike [`with_header`](Self::with_header) which replaces any existing value
628 /// for the same header name, this method adds the value alongside existing ones.
629 /// Required for headers like `Set-Cookie` where multiple values must coexist
630 /// as separate header lines (RFC 6265 Section 4.1).
631 ///
632 /// # Examples
633 ///
634 /// ```
635 /// use reinhardt_http::Response;
636 ///
637 /// let response = Response::ok()
638 /// .append_header("Set-Cookie", "a=1; Path=/")
639 /// .append_header("Set-Cookie", "b=2; Path=/");
640 /// let cookies: Vec<_> = response.headers.get_all("set-cookie").iter().collect();
641 /// assert_eq!(cookies.len(), 2);
642 /// ```
643 pub fn append_header(mut self, name: &str, value: &str) -> Self {
644 if let Ok(header_name) = hyper::header::HeaderName::from_bytes(name.as_bytes())
645 && let Ok(header_value) = hyper::header::HeaderValue::from_str(value)
646 {
647 self.headers.append(header_name, header_value);
648 }
649 self
650 }
651
652 /// Add a Location header to the response (typically used for redirects)
653 ///
654 /// # Examples
655 ///
656 /// ```
657 /// use reinhardt_http::Response;
658 /// use hyper::StatusCode;
659 ///
660 /// let response = Response::new(StatusCode::FOUND).with_location("/redirect-target");
661 /// assert_eq!(
662 /// response.headers.get("location").unwrap().to_str().unwrap(),
663 /// "/redirect-target"
664 /// );
665 /// ```
666 pub fn with_location(mut self, location: &str) -> Self {
667 if let Ok(value) = hyper::header::HeaderValue::from_str(location) {
668 self.headers.insert(hyper::header::LOCATION, value);
669 }
670 self
671 }
672 /// Set the response body to JSON and add appropriate Content-Type header
673 ///
674 /// # Examples
675 ///
676 /// ```
677 /// use reinhardt_http::Response;
678 /// use serde_json::json;
679 ///
680 /// let data = json!({"message": "Hello, World!"});
681 /// let response = Response::ok().with_json(&data).unwrap();
682 ///
683 /// assert_eq!(
684 /// response.headers.get("content-type").unwrap().to_str().unwrap(),
685 /// "application/json"
686 /// );
687 /// ```
688 pub fn with_json<T: Serialize>(mut self, data: &T) -> crate::Result<Self> {
689 use crate::Error;
690 let json = serde_json::to_vec(data).map_err(|e| Error::Serialization(e.to_string()))?;
691 self.body = Bytes::from(json);
692 self.headers.insert(
693 hyper::header::CONTENT_TYPE,
694 hyper::header::HeaderValue::from_static("application/json"),
695 );
696 Ok(self)
697 }
698 /// Add a custom header using typed HeaderName and HeaderValue
699 ///
700 /// # Examples
701 ///
702 /// ```
703 /// use reinhardt_http::Response;
704 /// use hyper::header::{HeaderName, HeaderValue};
705 ///
706 /// let header_name = HeaderName::from_static("x-custom-header");
707 /// let header_value = HeaderValue::from_static("custom-value");
708 /// let response = Response::ok().with_typed_header(header_name, header_value);
709 ///
710 /// assert_eq!(
711 /// response.headers.get("x-custom-header").unwrap().to_str().unwrap(),
712 /// "custom-value"
713 /// );
714 /// ```
715 pub fn with_typed_header(
716 mut self,
717 key: hyper::header::HeaderName,
718 value: hyper::header::HeaderValue,
719 ) -> Self {
720 self.headers.insert(key, value);
721 self
722 }
723
724 /// Check if this response should stop the middleware chain
725 ///
726 /// When true, no further middleware or handlers will be executed.
727 ///
728 /// # Examples
729 ///
730 /// ```
731 /// use reinhardt_http::Response;
732 ///
733 /// let response = Response::ok();
734 /// assert!(!response.should_stop_chain());
735 ///
736 /// let stopping_response = Response::ok().with_stop_chain(true);
737 /// assert!(stopping_response.should_stop_chain());
738 /// ```
739 pub fn should_stop_chain(&self) -> bool {
740 self.stop_chain
741 }
742
743 /// Set whether this response should stop the middleware chain
744 ///
745 /// When set to true, the middleware chain will stop processing and return
746 /// this response immediately, skipping any remaining middleware and handlers.
747 ///
748 /// This is useful for early returns in middleware, such as:
749 /// - Authentication failures (401 Unauthorized)
750 /// - CORS preflight responses (204 No Content)
751 /// - Rate limiting rejections (429 Too Many Requests)
752 /// - Cache hits (304 Not Modified)
753 ///
754 /// # Examples
755 ///
756 /// ```
757 /// use reinhardt_http::Response;
758 /// use hyper::StatusCode;
759 ///
760 /// // Early return for authentication failure
761 /// let auth_failure = Response::unauthorized()
762 /// .with_body("Authentication required")
763 /// .with_stop_chain(true);
764 /// assert!(auth_failure.should_stop_chain());
765 ///
766 /// // CORS preflight response
767 /// let preflight = Response::no_content()
768 /// .with_header("Access-Control-Allow-Origin", "*")
769 /// .with_stop_chain(true);
770 /// assert!(preflight.should_stop_chain());
771 /// ```
772 pub fn with_stop_chain(mut self, stop: bool) -> Self {
773 self.stop_chain = stop;
774 self
775 }
776}
777
778impl From<crate::Error> for Response {
779 fn from(error: crate::Error) -> Self {
780 let status =
781 StatusCode::from_u16(error.status_code()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
782
783 // Log the full error for server-side debugging
784 tracing::error!(
785 status = status.as_u16(),
786 error = %error,
787 "Request error"
788 );
789
790 let mut response = SafeErrorResponse::new(status);
791
792 // For 4xx client errors, include a safe detail message
793 // that doesn't expose internal implementation details
794 if status.is_client_error()
795 && let Some(detail) = safe_client_error_detail(&error)
796 {
797 response = response.with_detail(detail);
798 }
799
800 response.build()
801 }
802}
803
804impl<S> StreamingResponse<S>
805where
806 S: Stream<Item = Result<Bytes, Box<dyn std::error::Error + Send + Sync>>> + Send + 'static,
807{
808 /// Create a new streaming response with OK status
809 ///
810 /// # Examples
811 ///
812 /// ```
813 /// use reinhardt_http::StreamingResponse;
814 /// use hyper::StatusCode;
815 /// use futures::stream;
816 /// use bytes::Bytes;
817 ///
818 /// let data = vec![Ok(Bytes::from("chunk1")), Ok(Bytes::from("chunk2"))];
819 /// let stream = stream::iter(data);
820 /// let response = StreamingResponse::new(stream);
821 ///
822 /// assert_eq!(response.status, StatusCode::OK);
823 /// ```
824 pub fn new(stream: S) -> Self {
825 Self {
826 status: StatusCode::OK,
827 headers: HeaderMap::new(),
828 stream,
829 }
830 }
831 /// Create a streaming response with a specific status code
832 ///
833 /// # Examples
834 ///
835 /// ```
836 /// use reinhardt_http::StreamingResponse;
837 /// use hyper::StatusCode;
838 /// use futures::stream;
839 /// use bytes::Bytes;
840 ///
841 /// let data = vec![Ok(Bytes::from("data"))];
842 /// let stream = stream::iter(data);
843 /// let response = StreamingResponse::with_status(stream, StatusCode::PARTIAL_CONTENT);
844 ///
845 /// assert_eq!(response.status, StatusCode::PARTIAL_CONTENT);
846 /// ```
847 pub fn with_status(stream: S, status: StatusCode) -> Self {
848 Self {
849 status,
850 headers: HeaderMap::new(),
851 stream,
852 }
853 }
854 /// Set the status code
855 ///
856 /// # Examples
857 ///
858 /// ```
859 /// use reinhardt_http::StreamingResponse;
860 /// use hyper::StatusCode;
861 /// use futures::stream;
862 /// use bytes::Bytes;
863 ///
864 /// let data = vec![Ok(Bytes::from("data"))];
865 /// let stream = stream::iter(data);
866 /// let response = StreamingResponse::new(stream).status(StatusCode::ACCEPTED);
867 ///
868 /// assert_eq!(response.status, StatusCode::ACCEPTED);
869 /// ```
870 pub fn status(mut self, status: StatusCode) -> Self {
871 self.status = status;
872 self
873 }
874 /// Add a header to the streaming response
875 ///
876 /// # Examples
877 ///
878 /// ```
879 /// use reinhardt_http::StreamingResponse;
880 /// use hyper::header::{HeaderName, HeaderValue, CACHE_CONTROL};
881 /// use futures::stream;
882 /// use bytes::Bytes;
883 ///
884 /// let data = vec![Ok(Bytes::from("data"))];
885 /// let stream = stream::iter(data);
886 /// let response = StreamingResponse::new(stream)
887 /// .header(CACHE_CONTROL, HeaderValue::from_static("no-cache"));
888 ///
889 /// assert_eq!(
890 /// response.headers.get(CACHE_CONTROL).unwrap().to_str().unwrap(),
891 /// "no-cache"
892 /// );
893 /// ```
894 pub fn header(
895 mut self,
896 key: hyper::header::HeaderName,
897 value: hyper::header::HeaderValue,
898 ) -> Self {
899 self.headers.insert(key, value);
900 self
901 }
902 /// Set the Content-Type header (media type)
903 ///
904 /// # Examples
905 ///
906 /// ```
907 /// use reinhardt_http::StreamingResponse;
908 /// use hyper::header::CONTENT_TYPE;
909 /// use futures::stream;
910 /// use bytes::Bytes;
911 ///
912 /// let data = vec![Ok(Bytes::from("data"))];
913 /// let stream = stream::iter(data);
914 /// let response = StreamingResponse::new(stream).media_type("video/mp4");
915 ///
916 /// assert_eq!(
917 /// response.headers.get(CONTENT_TYPE).unwrap().to_str().unwrap(),
918 /// "video/mp4"
919 /// );
920 /// ```
921 pub fn media_type(self, media_type: &str) -> Self {
922 self.header(
923 hyper::header::CONTENT_TYPE,
924 hyper::header::HeaderValue::from_str(media_type).unwrap_or_else(|_| {
925 hyper::header::HeaderValue::from_static("application/octet-stream")
926 }),
927 )
928 }
929}
930
931impl<S> StreamingResponse<S> {
932 /// Consume the response and return the underlying stream
933 ///
934 /// # Examples
935 ///
936 /// ```
937 /// use reinhardt_http::StreamingResponse;
938 /// use futures::stream::{self, StreamExt};
939 /// use bytes::Bytes;
940 ///
941 /// # futures::executor::block_on(async {
942 /// let data = vec![Ok(Bytes::from("chunk1")), Ok(Bytes::from("chunk2"))];
943 /// let stream = stream::iter(data);
944 /// let response = StreamingResponse::new(stream);
945 ///
946 /// let mut extracted_stream = response.into_stream();
947 /// let first_chunk = extracted_stream.next().await.unwrap().unwrap();
948 /// assert_eq!(first_chunk, Bytes::from("chunk1"));
949 /// # });
950 /// ```
951 pub fn into_stream(self) -> S {
952 self.stream
953 }
954}
955
956#[cfg(test)]
957mod tests {
958 use super::*;
959 use rstest::rstest;
960
961 #[rstest]
962 #[case(StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error")]
963 #[case(StatusCode::BAD_GATEWAY, "Bad Gateway")]
964 #[case(StatusCode::SERVICE_UNAVAILABLE, "Service Unavailable")]
965 #[case(StatusCode::GATEWAY_TIMEOUT, "Gateway Timeout")]
966 fn test_5xx_errors_never_include_internal_details(
967 #[case] status: StatusCode,
968 #[case] expected_message: &str,
969 ) {
970 // Arrange
971 let sensitive_detail = "Internal path /src/db/connection.rs:42 failed";
972
973 // Act
974 let response = SafeErrorResponse::new(status)
975 .with_detail(sensitive_detail)
976 .build();
977
978 // Assert
979 let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
980 assert_eq!(body["error"], expected_message);
981 // Detail must NOT be included for 5xx errors
982 assert!(body.get("detail").is_none());
983 assert_eq!(response.status, status);
984 }
985
986 #[rstest]
987 #[case(StatusCode::BAD_REQUEST, "Bad Request")]
988 #[case(StatusCode::UNAUTHORIZED, "Unauthorized")]
989 #[case(StatusCode::FORBIDDEN, "Forbidden")]
990 #[case(StatusCode::NOT_FOUND, "Not Found")]
991 #[case(StatusCode::METHOD_NOT_ALLOWED, "Method Not Allowed")]
992 #[case(StatusCode::CONFLICT, "Conflict")]
993 fn test_4xx_errors_include_safe_detail(
994 #[case] status: StatusCode,
995 #[case] expected_message: &str,
996 ) {
997 // Arrange
998 let detail = "Missing required field: name";
999
1000 // Act
1001 let response = SafeErrorResponse::new(status).with_detail(detail).build();
1002
1003 // Assert
1004 let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
1005 assert_eq!(body["error"], expected_message);
1006 assert_eq!(body["detail"], detail);
1007 assert_eq!(response.status, status);
1008 }
1009
1010 #[rstest]
1011 fn test_debug_mode_includes_full_error_info() {
1012 // Arrange
1013 let debug_info = "Error at src/handlers/user.rs:42: column 'email' not found";
1014
1015 // Act
1016 let response = SafeErrorResponse::new(StatusCode::INTERNAL_SERVER_ERROR)
1017 .with_detail("Database query failed")
1018 .with_debug_info(debug_info)
1019 .with_debug_mode(true)
1020 .build();
1021
1022 // Assert
1023 let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
1024 assert_eq!(body["error"], "Internal Server Error");
1025 // In debug mode, detail is included even for 5xx
1026 assert_eq!(body["detail"], "Database query failed");
1027 assert_eq!(body["debug"], debug_info);
1028 }
1029
1030 #[rstest]
1031 fn test_debug_mode_disabled_excludes_debug_info() {
1032 // Arrange
1033 let debug_info = "Sensitive internal detail";
1034
1035 // Act
1036 let response = SafeErrorResponse::new(StatusCode::INTERNAL_SERVER_ERROR)
1037 .with_debug_info(debug_info)
1038 .with_debug_mode(false)
1039 .build();
1040
1041 // Assert
1042 let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
1043 assert!(body.get("debug").is_none());
1044 }
1045
1046 #[rstest]
1047 #[case(StatusCode::BAD_REQUEST, "Bad Request")]
1048 #[case(StatusCode::UNAUTHORIZED, "Unauthorized")]
1049 #[case(StatusCode::FORBIDDEN, "Forbidden")]
1050 #[case(StatusCode::NOT_FOUND, "Not Found")]
1051 #[case(StatusCode::METHOD_NOT_ALLOWED, "Method Not Allowed")]
1052 #[case(StatusCode::NOT_ACCEPTABLE, "Not Acceptable")]
1053 #[case(StatusCode::REQUEST_TIMEOUT, "Request Timeout")]
1054 #[case(StatusCode::CONFLICT, "Conflict")]
1055 #[case(StatusCode::GONE, "Gone")]
1056 #[case(StatusCode::PAYLOAD_TOO_LARGE, "Payload Too Large")]
1057 #[case(StatusCode::UNSUPPORTED_MEDIA_TYPE, "Unsupported Media Type")]
1058 #[case(StatusCode::UNPROCESSABLE_ENTITY, "Unprocessable Entity")]
1059 #[case(StatusCode::TOO_MANY_REQUESTS, "Too Many Requests")]
1060 #[case(StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error")]
1061 #[case(StatusCode::BAD_GATEWAY, "Bad Gateway")]
1062 #[case(StatusCode::SERVICE_UNAVAILABLE, "Service Unavailable")]
1063 #[case(StatusCode::GATEWAY_TIMEOUT, "Gateway Timeout")]
1064 fn test_safe_error_message_returns_correct_messages(
1065 #[case] status: StatusCode,
1066 #[case] expected: &str,
1067 ) {
1068 // Arrange / Act
1069 let message = safe_error_message(status);
1070
1071 // Assert
1072 assert_eq!(message, expected);
1073 }
1074
1075 #[rstest]
1076 fn test_safe_error_message_fallback_client_error() {
1077 // Arrange
1078 // 418 I'm a Teapot (not explicitly mapped)
1079 let status = StatusCode::IM_A_TEAPOT;
1080
1081 // Act
1082 let message = safe_error_message(status);
1083
1084 // Assert
1085 assert_eq!(message, "Client Error");
1086 }
1087
1088 #[rstest]
1089 fn test_safe_error_message_fallback_server_error() {
1090 // Arrange
1091 // 505 HTTP Version Not Supported (not explicitly mapped)
1092 let status = StatusCode::HTTP_VERSION_NOT_SUPPORTED;
1093
1094 // Act
1095 let message = safe_error_message(status);
1096
1097 // Assert
1098 assert_eq!(message, "Server Error");
1099 }
1100
1101 #[rstest]
1102 fn test_truncate_for_log_short_string() {
1103 // Arrange
1104 let input = "hello";
1105
1106 // Act
1107 let result = truncate_for_log(input, 10);
1108
1109 // Assert
1110 assert_eq!(result, "hello");
1111 }
1112
1113 #[rstest]
1114 fn test_truncate_for_log_long_string() {
1115 // Arrange
1116 let input = "a".repeat(100);
1117
1118 // Act
1119 let result = truncate_for_log(&input, 10);
1120
1121 // Assert
1122 assert!(result.starts_with("aaaaaaaaaa"));
1123 assert!(result.contains("...[truncated, 100 total bytes]"));
1124 }
1125
1126 #[rstest]
1127 fn test_truncate_for_log_exact_length() {
1128 // Arrange
1129 let input = "abcde";
1130
1131 // Act
1132 let result = truncate_for_log(input, 5);
1133
1134 // Assert
1135 assert_eq!(result, "abcde");
1136 }
1137
1138 #[rstest]
1139 fn test_truncate_for_log_multi_byte_utf8_does_not_panic() {
1140 // Arrange
1141 // Each CJK character is 3 bytes in UTF-8
1142 let input = "日本語テスト文字列";
1143
1144 // Act - max_length falls in the middle of a multi-byte char
1145 let result = truncate_for_log(input, 4);
1146
1147 // Assert - should truncate at valid char boundary (3 bytes = 1 char)
1148 assert!(result.starts_with("日"));
1149 assert!(result.contains("...[truncated"));
1150 }
1151
1152 #[rstest]
1153 fn test_truncate_for_log_emoji_boundary() {
1154 // Arrange
1155 // Each emoji is 4 bytes in UTF-8
1156 let input = "🦀🐍🐹🐿️";
1157
1158 // Act - max_length 5 falls between first emoji (4 bytes) and second
1159 let result = truncate_for_log(input, 5);
1160
1161 // Assert - should include only the first emoji (4 bytes)
1162 assert!(result.starts_with("🦀"));
1163 assert!(result.contains("...[truncated"));
1164 }
1165
1166 #[rstest]
1167 fn test_truncate_for_log_mixed_ascii_and_multibyte() {
1168 // Arrange
1169 let input = "abc日本語def";
1170
1171 // Act - max_length 5 falls inside second CJK char
1172 let result = truncate_for_log(input, 5);
1173
1174 // Assert - "abc" (3 bytes) + "日" (3 bytes) = 6 bytes > 5, so only "abc"
1175 assert!(result.starts_with("abc"));
1176 assert!(result.contains("...[truncated"));
1177 }
1178
1179 #[rstest]
1180 fn test_truncate_for_log_zero_max_length() {
1181 // Arrange
1182 let input = "hello";
1183
1184 // Act
1185 let result = truncate_for_log(input, 0);
1186
1187 // Assert - should produce empty prefix with truncation notice
1188 assert!(result.starts_with("...[truncated"));
1189 }
1190
1191 #[rstest]
1192 fn test_from_error_produces_safe_output_for_5xx() {
1193 // Arrange
1194 let error = crate::Error::Database(
1195 "Connection to postgres://user:pass@db:5432/mydb failed".to_string(),
1196 );
1197
1198 // Act
1199 let response: Response = error.into();
1200
1201 // Assert
1202 assert_eq!(response.status, StatusCode::INTERNAL_SERVER_ERROR);
1203 let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
1204 assert_eq!(body["error"], "Internal Server Error");
1205 // Must NOT contain internal connection details
1206 let body_str = String::from_utf8_lossy(&response.body);
1207 assert!(!body_str.contains("postgres://"));
1208 assert!(!body_str.contains("user:pass"));
1209 assert!(body.get("detail").is_none());
1210 }
1211
1212 #[rstest]
1213 fn test_from_error_produces_safe_output_for_4xx_validation() {
1214 // Arrange
1215 let error = crate::Error::Validation("Email format is invalid".to_string());
1216
1217 // Act
1218 let response: Response = error.into();
1219
1220 // Assert
1221 assert_eq!(response.status, StatusCode::BAD_REQUEST);
1222 let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
1223 assert_eq!(body["error"], "Bad Request");
1224 assert_eq!(body["detail"], "Email format is invalid");
1225 }
1226
1227 #[rstest]
1228 fn test_from_error_produces_safe_output_for_4xx_parse() {
1229 // Arrange
1230 let error = crate::Error::ParseError(
1231 "invalid digit found in string at src/parser.rs:42".to_string(),
1232 );
1233
1234 // Act
1235 let response: Response = error.into();
1236
1237 // Assert
1238 assert_eq!(response.status, StatusCode::BAD_REQUEST);
1239 let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
1240 assert_eq!(body["error"], "Bad Request");
1241 // Must NOT expose the internal path from the original error
1242 assert_eq!(body["detail"], "Invalid request format");
1243 let body_str = String::from_utf8_lossy(&response.body);
1244 assert!(!body_str.contains("src/parser.rs"));
1245 }
1246
1247 #[rstest]
1248 fn test_from_error_body_already_consumed() {
1249 // Arrange
1250 let error = crate::Error::BodyAlreadyConsumed;
1251
1252 // Act
1253 let response: Response = error.into();
1254
1255 // Assert
1256 assert_eq!(response.status, StatusCode::BAD_REQUEST);
1257 let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
1258 assert_eq!(body["detail"], "Request body has already been consumed");
1259 }
1260
1261 #[rstest]
1262 fn test_from_error_internal_error_hides_details() {
1263 // Arrange
1264 let error =
1265 crate::Error::Internal("panic at /Users/dev/projects/app/src/main.rs:10".to_string());
1266
1267 // Act
1268 let response: Response = error.into();
1269
1270 // Assert
1271 assert_eq!(response.status, StatusCode::INTERNAL_SERVER_ERROR);
1272 let body_str = String::from_utf8_lossy(&response.body);
1273 assert!(!body_str.contains("/Users/dev"));
1274 assert!(!body_str.contains("main.rs"));
1275 }
1276
1277 #[rstest]
1278 fn test_safe_error_response_no_detail_set() {
1279 // Arrange / Act
1280 let response = SafeErrorResponse::new(StatusCode::BAD_REQUEST).build();
1281
1282 // Assert
1283 let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
1284 assert_eq!(body["error"], "Bad Request");
1285 assert!(body.get("detail").is_none());
1286 }
1287
1288 #[rstest]
1289 fn test_safe_error_response_content_type_is_json() {
1290 // Arrange / Act
1291 let response = SafeErrorResponse::new(StatusCode::NOT_FOUND).build();
1292
1293 // Assert
1294 let content_type = response
1295 .headers
1296 .get("content-type")
1297 .unwrap()
1298 .to_str()
1299 .unwrap();
1300 assert_eq!(content_type, "application/json");
1301 }
1302
1303 // =================================================================
1304 // with_header panic prevention tests (Issue #357)
1305 // =================================================================
1306
1307 #[rstest]
1308 fn test_with_header_invalid_name_does_not_panic() {
1309 // Arrange
1310 let response = Response::ok();
1311
1312 // Act - invalid header name with space (previously panicked)
1313 let response = response.with_header("Invalid Header", "value");
1314
1315 // Assert - header is silently ignored, no panic
1316 assert!(response.headers.is_empty());
1317 }
1318
1319 #[rstest]
1320 fn test_with_header_invalid_value_does_not_panic() {
1321 // Arrange
1322 let response = Response::ok();
1323
1324 // Act - header value with non-visible ASCII (previously panicked)
1325 let response = response.with_header("X-Test", "value\x00with\x01control");
1326
1327 // Assert - header is silently ignored, no panic
1328 assert!(response.headers.get("X-Test").is_none());
1329 }
1330
1331 #[rstest]
1332 fn test_with_header_valid_header_works() {
1333 // Arrange
1334 let response = Response::ok();
1335
1336 // Act
1337 let response = response.with_header("X-Custom", "custom-value");
1338
1339 // Assert
1340 assert_eq!(
1341 response.headers.get("X-Custom").unwrap().to_str().unwrap(),
1342 "custom-value"
1343 );
1344 }
1345
1346 #[rstest]
1347 fn test_try_with_header_invalid_name_returns_error() {
1348 // Arrange
1349 let response = Response::ok();
1350
1351 // Act
1352 let result = response.try_with_header("Invalid Header", "value");
1353
1354 // Assert
1355 assert!(result.is_err());
1356 }
1357
1358 #[rstest]
1359 fn test_try_with_header_valid_header_returns_ok() {
1360 // Arrange
1361 let response = Response::ok();
1362
1363 // Act
1364 let result = response.try_with_header("X-Custom", "valid-value");
1365
1366 // Assert
1367 assert!(result.is_ok());
1368 let response = result.unwrap();
1369 assert_eq!(
1370 response.headers.get("X-Custom").unwrap().to_str().unwrap(),
1371 "valid-value"
1372 );
1373 }
1374
1375 #[rstest]
1376 fn test_append_header_adds_multiple_values() {
1377 // Arrange & Act
1378 let response = Response::ok()
1379 .append_header("Set-Cookie", "a=1; Path=/")
1380 .append_header("Set-Cookie", "b=2; Path=/");
1381
1382 // Assert
1383 let cookies: Vec<_> = response.headers.get_all("set-cookie").iter().collect();
1384 assert_eq!(cookies.len(), 2);
1385 assert_eq!(cookies[0].to_str().unwrap(), "a=1; Path=/");
1386 assert_eq!(cookies[1].to_str().unwrap(), "b=2; Path=/");
1387 }
1388
1389 #[rstest]
1390 fn test_append_header_coexists_with_with_header() {
1391 // Arrange & Act
1392 let response = Response::ok()
1393 .with_header("Set-Cookie", "a=1; Path=/")
1394 .append_header("Set-Cookie", "b=2; Path=/");
1395
1396 // Assert
1397 let cookies: Vec<_> = response.headers.get_all("set-cookie").iter().collect();
1398 assert_eq!(cookies.len(), 2);
1399 }
1400}