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