Skip to main content

reinhardt_http/
response.rs

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