Skip to main content

reinhardt_core/exception/
param_error.rs

1//! Parameter Error Context
2//!
3//! This module provides detailed error context for HTTP parameter extraction failures.
4//! It supports various parameter types (JSON, Query, Path, Form, Header, Cookie, Body)
5//! and provides structured error information including field names, expected types,
6//! and raw values for debugging.
7
8/// Parameter type for error context
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum ParamType {
11	/// JSON request body parameter.
12	Json,
13	/// URL query string parameter.
14	Query,
15	/// URL path parameter.
16	Path,
17	/// Form-encoded body parameter.
18	Form,
19	/// HTTP header parameter.
20	Header,
21	/// Cookie parameter.
22	Cookie,
23	/// Raw request body parameter.
24	Body,
25}
26
27impl std::fmt::Display for ParamType {
28	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29		match self {
30			ParamType::Json => write!(f, "Json"),
31			ParamType::Query => write!(f, "Query"),
32			ParamType::Path => write!(f, "Path"),
33			ParamType::Form => write!(f, "Form"),
34			ParamType::Header => write!(f, "Header"),
35			ParamType::Cookie => write!(f, "Cookie"),
36			ParamType::Body => write!(f, "Body"),
37		}
38	}
39}
40
41/// Detailed context for parameter extraction errors
42#[derive(Debug, Clone)]
43pub struct ParamErrorContext {
44	/// Parameter type (Json, Query, Path, Form, Header, etc.)
45	pub param_type: ParamType,
46	/// Field name if identifiable
47	pub field_name: Option<String>,
48	/// Error message
49	pub message: String,
50	/// Original source error (not cloneable, so we store the message)
51	pub source_message: Option<String>,
52	/// Original value (for debugging, sensitive data should be excluded)
53	pub raw_value: Option<String>,
54	/// Expected type name
55	pub expected_type: Option<String>,
56}
57
58impl ParamErrorContext {
59	/// Create a new ParamErrorContext
60	pub fn new(param_type: ParamType, message: impl Into<String>) -> Self {
61		Self {
62			param_type,
63			field_name: None,
64			message: message.into(),
65			source_message: None,
66			raw_value: None,
67			expected_type: None,
68		}
69	}
70
71	/// Set the field name
72	pub fn with_field(mut self, field: impl Into<String>) -> Self {
73		self.field_name = Some(field.into());
74		self
75	}
76
77	/// Set the source error
78	pub fn with_source(mut self, source: Box<dyn std::error::Error + Send + Sync>) -> Self {
79		self.source_message = Some(source.to_string());
80		self
81	}
82
83	/// Set the raw value (truncated if too long)
84	pub fn with_raw_value(mut self, value: impl Into<String>) -> Self {
85		let value = value.into();
86		// Truncate to ~500 bytes max to avoid log spam.
87		// Use char_indices to find a safe truncation point on a char boundary,
88		// preventing panics on multi-byte UTF-8 strings (e.g., Japanese, emoji).
89		if value.len() > 500 {
90			let truncation_point = value
91				.char_indices()
92				.map(|(idx, _)| idx)
93				.take_while(|&idx| idx <= 500)
94				.last()
95				.unwrap_or(0);
96			self.raw_value = Some(format!("{}...[truncated]", &value[..truncation_point]));
97		} else {
98			self.raw_value = Some(value);
99		}
100		self
101	}
102
103	/// Set the expected type
104	pub fn with_expected_type<T>(mut self) -> Self {
105		self.expected_type = Some(std::any::type_name::<T>().to_string());
106		self
107	}
108
109	/// Format error as single line (for Display trait)
110	pub fn format_error(&self) -> String {
111		let mut parts = vec![format!("{} parameter extraction failed", self.param_type)];
112
113		if let Some(ref field) = self.field_name {
114			parts.push(format!("field: '{}'", field));
115		}
116
117		parts.push(format!("error: {}", self.message));
118
119		if let Some(ref expected) = self.expected_type {
120			parts.push(format!("expected type: {}", expected));
121		}
122
123		parts.join(", ")
124	}
125
126	/// Format error as multiple lines (for detailed logging)
127	pub fn format_multiline(&self, include_raw_value: bool) -> String {
128		let mut lines = vec![
129			format!("  {} parameter extraction failed", self.param_type),
130			format!("  Error: {}", self.message),
131		];
132
133		if let Some(ref field) = self.field_name {
134			lines.push(format!("  Field: {}", field));
135		}
136
137		if let Some(ref expected) = self.expected_type {
138			lines.push(format!("  Expected type: {}", expected));
139		}
140
141		if include_raw_value && let Some(ref raw) = self.raw_value {
142			lines.push(format!("  Received: {}", raw));
143		}
144
145		lines.join("\n")
146	}
147}
148
149/// Extract field name from serde_json::Error message
150pub fn extract_field_from_serde_error(err: &serde_json::Error) -> Option<String> {
151	let msg = err.to_string();
152
153	// "missing field `xxx`" pattern
154	if let Some(start) = msg.find("missing field `") {
155		let rest = &msg[start + 15..];
156		if let Some(end) = rest.find('`') {
157			return Some(rest[..end].to_string());
158		}
159	}
160
161	// "unknown field `xxx`" pattern
162	if let Some(start) = msg.find("unknown field `") {
163		let rest = &msg[start + 15..];
164		if let Some(end) = rest.find('`') {
165			return Some(rest[..end].to_string());
166		}
167	}
168
169	// "duplicate field `xxx`" pattern
170	if let Some(start) = msg.find("duplicate field `") {
171		let rest = &msg[start + 17..];
172		if let Some(end) = rest.find('`') {
173			return Some(rest[..end].to_string());
174		}
175	}
176
177	None
178}
179
180/// Extract field name from serde_urlencoded error message
181pub fn extract_field_from_urlencoded_error(err: &serde_urlencoded::de::Error) -> Option<String> {
182	let msg = err.to_string();
183
184	// "missing field `xxx`" pattern
185	if let Some(start) = msg.find("missing field `") {
186		let rest = &msg[start + 15..];
187		if let Some(end) = rest.find('`') {
188			return Some(rest[..end].to_string());
189		}
190	}
191
192	None
193}
194
195#[cfg(test)]
196mod tests {
197	use rstest::rstest;
198
199	use super::*;
200
201	#[rstest]
202	fn with_raw_value_does_not_panic_on_multibyte_utf8() {
203		// Arrange - 500+ bytes of multi-byte Japanese characters
204		// Each character is 3 bytes in UTF-8, so 200 chars = 600 bytes
205		let japanese_str: String = "あ".repeat(200);
206		assert!(japanese_str.len() > 500);
207
208		// Act - must not panic on multi-byte boundary
209		let ctx = ParamErrorContext::new(ParamType::Json, "test").with_raw_value(japanese_str);
210
211		// Assert
212		let raw = ctx.raw_value.unwrap();
213		assert!(raw.ends_with("...[truncated]"));
214	}
215
216	#[rstest]
217	fn with_raw_value_does_not_panic_on_emoji() {
218		// Arrange - emoji are 4 bytes each, 150 emojis = 600 bytes
219		let emoji_str: String = "\u{1F600}".repeat(150);
220		assert!(emoji_str.len() > 500);
221
222		// Act - must not panic on 4-byte char boundary
223		let ctx = ParamErrorContext::new(ParamType::Query, "test").with_raw_value(emoji_str);
224
225		// Assert
226		let raw = ctx.raw_value.unwrap();
227		assert!(raw.ends_with("...[truncated]"));
228		// Verify truncated content is valid UTF-8 (would panic if not)
229		assert!(raw.is_char_boundary(0));
230	}
231
232	#[rstest]
233	fn with_raw_value_does_not_truncate_short_strings() {
234		// Arrange
235		let short = "hello world";
236
237		// Act
238		let ctx = ParamErrorContext::new(ParamType::Path, "test").with_raw_value(short);
239
240		// Assert
241		assert_eq!(ctx.raw_value.unwrap(), "hello world");
242	}
243
244	#[rstest]
245	fn with_raw_value_handles_mixed_multibyte_ascii() {
246		// Arrange - mix of ASCII and multi-byte characters totaling > 500 bytes
247		let mixed: String = "a".repeat(498) + "ああ"; // 498 + 6 = 504 bytes
248		assert!(mixed.len() > 500);
249
250		// Act
251		let ctx = ParamErrorContext::new(ParamType::Form, "test").with_raw_value(mixed);
252
253		// Assert
254		let raw = ctx.raw_value.unwrap();
255		assert!(raw.ends_with("...[truncated]"));
256	}
257
258	#[rstest]
259	fn with_raw_value_preserves_exactly_500_byte_string() {
260		// Arrange - exactly 500 ASCII bytes
261		let exact = "x".repeat(500);
262		assert_eq!(exact.len(), 500);
263
264		// Act
265		let ctx = ParamErrorContext::new(ParamType::Header, "test").with_raw_value(exact.clone());
266
267		// Assert - should NOT be truncated (len is not > 500)
268		assert_eq!(ctx.raw_value.unwrap(), exact);
269	}
270}