Skip to main content

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}