1use serde::{Deserialize, Serialize};
26use serde_json::json;
27use std::fmt;
28use thiserror::Error;
29use tracing::{error, warn};
30
31#[derive(Error, Debug, Clone)]
53pub enum WebError {
54 #[error("Invalid request: {message}")]
57 InvalidRequest {
58 message: String,
60 },
61
62 #[error("Unauthorized: {reason}")]
65 Unauthorized {
66 reason: String,
68 },
69
70 #[error("Forbidden: {reason}")]
73 Forbidden {
74 reason: String,
76 },
77
78 #[error("Not found: {resource}")]
81 NotFound {
82 resource: String,
84 },
85
86 #[error("Content too large: {size} bytes exceeds maximum of {max} bytes")]
89 ContentTooLarge {
90 size: usize,
92 max: usize,
94 },
95
96 #[error("Rate limited: retry after {retry_after_secs} seconds")]
99 RateLimited {
100 retry_after_secs: u64,
102 },
103
104 #[error("Processing error: {0}")]
107 ProcessingError(String),
108
109 #[error("Internal error: {message}")]
112 InternalError {
113 message: String,
115 },
116
117 #[error("Service unavailable: {reason}")]
120 ServiceUnavailable {
121 reason: String,
123 },
124
125 #[error("Gateway timeout after {timeout_ms}ms")]
128 GatewayTimeout {
129 timeout_ms: u64,
131 },
132}
133
134impl WebError {
135 pub fn invalid_request(message: impl Into<String>) -> Self {
141 Self::InvalidRequest {
142 message: message.into(),
143 }
144 }
145
146 pub fn missing_field(field: &str) -> Self {
148 Self::InvalidRequest {
149 message: format!("Missing required field: {}", field),
150 }
151 }
152
153 pub fn invalid_field(field: &str, reason: &str) -> Self {
155 Self::InvalidRequest {
156 message: format!("Invalid value for field '{}': {}", field, reason),
157 }
158 }
159
160 pub fn unauthorized(reason: impl Into<String>) -> Self {
162 Self::Unauthorized {
163 reason: reason.into(),
164 }
165 }
166
167 pub fn forbidden(reason: impl Into<String>) -> Self {
169 Self::Forbidden {
170 reason: reason.into(),
171 }
172 }
173
174 pub fn not_found(resource: impl Into<String>) -> Self {
176 Self::NotFound {
177 resource: resource.into(),
178 }
179 }
180
181 pub fn content_too_large(size: usize, max: usize) -> Self {
183 Self::ContentTooLarge { size, max }
184 }
185
186 pub fn rate_limited(retry_after_secs: u64) -> Self {
188 Self::RateLimited { retry_after_secs }
189 }
190
191 pub fn processing<E: std::fmt::Display>(source: E) -> Self {
193 Self::ProcessingError(source.to_string())
194 }
195
196 pub fn internal(message: impl Into<String>) -> Self {
198 Self::InternalError {
199 message: message.into(),
200 }
201 }
202
203 pub fn internal_from<E: std::error::Error>(err: E) -> Self {
205 error!(error = %err, "Internal error occurred");
207 Self::InternalError {
208 message: "An unexpected error occurred".to_string(),
209 }
210 }
211
212 pub fn service_unavailable(reason: impl Into<String>) -> Self {
214 Self::ServiceUnavailable {
215 reason: reason.into(),
216 }
217 }
218
219 pub fn gateway_timeout(timeout_ms: u64) -> Self {
221 Self::GatewayTimeout { timeout_ms }
222 }
223
224 pub fn status_code(&self) -> u16 {
230 match self {
231 Self::InvalidRequest { .. } => 400,
232 Self::Unauthorized { .. } => 401,
233 Self::Forbidden { .. } => 403,
234 Self::NotFound { .. } => 404,
235 Self::ContentTooLarge { .. } => 413,
236 Self::RateLimited { .. } => 429,
237 Self::ProcessingError(_) => 500,
238 Self::InternalError { .. } => 500,
239 Self::ServiceUnavailable { .. } => 503,
240 Self::GatewayTimeout { .. } => 504,
241 }
242 }
243
244 pub fn error_code(&self) -> &'static str {
246 match self {
247 Self::InvalidRequest { .. } => "INVALID_REQUEST",
248 Self::Unauthorized { .. } => "UNAUTHORIZED",
249 Self::Forbidden { .. } => "FORBIDDEN",
250 Self::NotFound { .. } => "NOT_FOUND",
251 Self::ContentTooLarge { .. } => "CONTENT_TOO_LARGE",
252 Self::RateLimited { .. } => "RATE_LIMITED",
253 Self::ProcessingError(_) => "PROCESSING_ERROR",
254 Self::InternalError { .. } => "INTERNAL_ERROR",
255 Self::ServiceUnavailable { .. } => "SERVICE_UNAVAILABLE",
256 Self::GatewayTimeout { .. } => "GATEWAY_TIMEOUT",
257 }
258 }
259
260 pub fn to_json(&self) -> serde_json::Value {
262 json!({
263 "error": self.to_string(),
264 "code": self.error_code()
265 })
266 }
267
268 pub fn to_json_with_request_id(&self, request_id: &str) -> serde_json::Value {
270 json!({
271 "error": self.to_string(),
272 "code": self.error_code(),
273 "request_id": request_id
274 })
275 }
276
277 pub fn log(&self, request_id: Option<&str>) {
279 let request_id = request_id.unwrap_or("unknown");
280
281 match self {
282 Self::InvalidRequest { message } => {
283 warn!(
284 request_id = %request_id,
285 error_code = %self.error_code(),
286 message = %message,
287 "Invalid request"
288 );
289 }
290 Self::Unauthorized { reason } => {
291 warn!(
292 request_id = %request_id,
293 error_code = %self.error_code(),
294 reason = %reason,
295 "Unauthorized access attempt"
296 );
297 }
298 Self::Forbidden { reason } => {
299 warn!(
300 request_id = %request_id,
301 error_code = %self.error_code(),
302 reason = %reason,
303 "Forbidden access"
304 );
305 }
306 Self::NotFound { resource } => {
307 warn!(
308 request_id = %request_id,
309 error_code = %self.error_code(),
310 resource = %resource,
311 "Resource not found"
312 );
313 }
314 Self::ContentTooLarge { size, max } => {
315 warn!(
316 request_id = %request_id,
317 error_code = %self.error_code(),
318 size = %size,
319 max = %max,
320 "Content too large"
321 );
322 }
323 Self::RateLimited { retry_after_secs } => {
324 warn!(
325 request_id = %request_id,
326 error_code = %self.error_code(),
327 retry_after_secs = %retry_after_secs,
328 "Rate limited"
329 );
330 }
331 Self::ProcessingError(err) => {
332 error!(
333 request_id = %request_id,
334 error_code = %self.error_code(),
335 error = %err,
336 "Processing error"
337 );
338 }
339 Self::InternalError { message } => {
340 error!(
341 request_id = %request_id,
342 error_code = %self.error_code(),
343 message = %message,
344 "Internal error"
345 );
346 }
347 Self::ServiceUnavailable { reason } => {
348 error!(
349 request_id = %request_id,
350 error_code = %self.error_code(),
351 reason = %reason,
352 "Service unavailable"
353 );
354 }
355 Self::GatewayTimeout { timeout_ms } => {
356 error!(
357 request_id = %request_id,
358 error_code = %self.error_code(),
359 timeout_ms = %timeout_ms,
360 "Gateway timeout"
361 );
362 }
363 }
364 }
365
366 pub fn is_retryable(&self) -> bool {
368 matches!(
369 self,
370 Self::RateLimited { .. }
371 | Self::ServiceUnavailable { .. }
372 | Self::GatewayTimeout { .. }
373 )
374 }
375
376 pub fn retry_after(&self) -> Option<u64> {
378 match self {
379 Self::RateLimited { retry_after_secs } => Some(*retry_after_secs),
380 _ => None,
381 }
382 }
383}
384
385#[derive(Debug, Clone, Serialize, Deserialize)]
387pub struct ErrorResponse {
388 pub error: String,
390 pub code: String,
392 #[serde(skip_serializing_if = "Option::is_none")]
394 pub request_id: Option<String>,
395 #[serde(skip_serializing_if = "Option::is_none")]
397 pub details: Option<serde_json::Value>,
398}
399
400impl ErrorResponse {
401 pub fn new(error: impl Into<String>, code: impl Into<String>) -> Self {
403 Self {
404 error: error.into(),
405 code: code.into(),
406 request_id: None,
407 details: None,
408 }
409 }
410
411 pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
413 self.request_id = Some(request_id.into());
414 self
415 }
416
417 pub fn with_details(mut self, details: serde_json::Value) -> Self {
419 self.details = Some(details);
420 self
421 }
422}
423
424impl From<&WebError> for ErrorResponse {
425 fn from(err: &WebError) -> Self {
426 Self {
427 error: err.to_string(),
428 code: err.error_code().to_string(),
429 request_id: None,
430 details: None,
431 }
432 }
433}
434
435#[derive(Error, Debug)]
441pub enum Error {
442 #[error("{0}")]
444 Web(#[from] WebError),
445
446 #[error("Browser error: {0}")]
448 Browser(#[from] BrowserError),
449
450 #[error("MCP error: {0}")]
452 Mcp(#[from] McpError),
453
454 #[error("Extraction error: {0}")]
456 Extraction(#[from] ExtractionError),
457
458 #[error("Navigation error: {0}")]
460 Navigation(#[from] NavigationError),
461
462 #[error("Capture error: {0}")]
464 Capture(#[from] CaptureError),
465
466 #[error("I/O error: {0}")]
468 Io(#[from] std::io::Error),
469
470 #[error("JSON error: {0}")]
472 Json(#[from] serde_json::Error),
473
474 #[error("CDP error: {0}")]
476 Cdp(String),
477
478 #[error("{0}")]
480 Generic(String),
481}
482
483#[derive(Error, Debug)]
485pub enum BrowserError {
486 #[error("Failed to launch browser: {0}")]
488 LaunchFailed(String),
489
490 #[error("Invalid browser configuration: {0}")]
492 ConfigError(String),
493
494 #[error("Browser connection lost")]
496 ConnectionLost,
497
498 #[error("Failed to create page: {0}")]
500 PageCreationFailed(String),
501
502 #[error("Browser already closed")]
504 AlreadyClosed,
505
506 #[error("Browser operation timed out after {0}ms")]
508 Timeout(u64),
509}
510
511#[derive(Error, Debug)]
513pub enum McpError {
514 #[error("Invalid JSON-RPC request: {0}")]
516 InvalidRequest(String),
517
518 #[error("Unknown method: {0}")]
520 UnknownMethod(String),
521
522 #[error("Invalid parameters: {0}")]
524 InvalidParams(String),
525
526 #[error("Tool not found: {0}")]
528 ToolNotFound(String),
529
530 #[error("Tool execution failed: {0}")]
532 ToolExecutionFailed(String),
533
534 #[error("Protocol version mismatch: expected {expected}, got {actual}")]
536 VersionMismatch {
537 expected: String,
539 actual: String,
541 },
542
543 #[error("Parse error: {0}")]
545 ParseError(String),
546}
547
548#[derive(Error, Debug)]
550pub enum ExtractionError {
551 #[error("Element not found: {0}")]
553 ElementNotFound(String),
554
555 #[error("Invalid selector: {0}")]
557 InvalidSelector(String),
558
559 #[error("Extraction failed: {0}")]
561 ExtractionFailed(String),
562
563 #[error("Content parsing failed: {0}")]
565 ParsingFailed(String),
566
567 #[error("JavaScript execution failed: {0}")]
569 JsExecutionFailed(String),
570}
571
572#[derive(Error, Debug)]
574pub enum NavigationError {
575 #[error("Invalid URL: {0}")]
577 InvalidUrl(String),
578
579 #[error("Navigation timed out after {0}ms")]
581 Timeout(u64),
582
583 #[error("Page load failed: {0}")]
585 LoadFailed(String),
586
587 #[error("SSL/TLS error: {0}")]
589 SslError(String),
590
591 #[error("Network error: {0}")]
593 NetworkError(String),
594
595 #[error("HTTP error {status}: {message}")]
597 HttpError {
598 status: u16,
600 message: String,
602 },
603}
604
605#[derive(Error, Debug)]
607pub enum CaptureError {
608 #[error("Screenshot capture failed: {0}")]
610 ScreenshotFailed(String),
611
612 #[error("PDF generation failed: {0}")]
614 PdfFailed(String),
615
616 #[error("MHTML capture failed: {0}")]
618 MhtmlFailed(String),
619
620 #[error("HTML capture failed: {0}")]
622 HtmlFailed(String),
623
624 #[error("Invalid capture format: {0}")]
626 InvalidFormat(String),
627
628 #[error("Capture timed out after {0}ms")]
630 Timeout(u64),
631}
632
633pub type Result<T> = std::result::Result<T, Error>;
635
636pub type WebResult<T> = std::result::Result<T, WebError>;
638
639impl Error {
640 pub fn generic<S: Into<String>>(msg: S) -> Self {
642 Error::Generic(msg.into())
643 }
644
645 pub fn cdp<S: Into<String>>(msg: S) -> Self {
647 Error::Cdp(msg.into())
648 }
649
650 pub fn into_web_error(self) -> WebError {
652 match self {
653 Error::Web(e) => e,
654 Error::Browser(e) => match e {
655 BrowserError::Timeout(ms) => WebError::GatewayTimeout { timeout_ms: ms },
656 BrowserError::ConnectionLost => {
657 WebError::service_unavailable("Browser connection lost")
658 }
659 BrowserError::AlreadyClosed => {
660 WebError::service_unavailable("Browser session closed")
661 }
662 _ => WebError::internal(e.to_string()),
663 },
664 Error::Mcp(e) => match e {
665 McpError::InvalidRequest(msg) => WebError::invalid_request(msg),
666 McpError::InvalidParams(msg) => WebError::invalid_request(msg),
667 McpError::ToolNotFound(tool) => {
668 WebError::not_found(format!("Tool not found: {}", tool))
669 }
670 _ => WebError::internal(e.to_string()),
671 },
672 Error::Navigation(e) => match e {
673 NavigationError::InvalidUrl(url) => {
674 WebError::invalid_request(format!("Invalid URL: {}", url))
675 }
676 NavigationError::Timeout(ms) => WebError::GatewayTimeout { timeout_ms: ms },
677 NavigationError::HttpError { status, message } => {
678 if status == 404 {
679 WebError::not_found(message)
680 } else if status == 401 {
681 WebError::unauthorized(message)
682 } else if status == 403 {
683 WebError::forbidden(message)
684 } else if status == 429 {
685 WebError::rate_limited(60) } else {
687 WebError::internal(format!("HTTP {}: {}", status, message))
688 }
689 }
690 _ => WebError::internal(e.to_string()),
691 },
692 Error::Extraction(e) => match e {
693 ExtractionError::ElementNotFound(selector) => {
694 WebError::not_found(format!("Element not found: {}", selector))
695 }
696 ExtractionError::InvalidSelector(sel) => {
697 WebError::invalid_request(format!("Invalid selector: {}", sel))
698 }
699 _ => WebError::internal(e.to_string()),
700 },
701 Error::Capture(e) => match e {
702 CaptureError::Timeout(ms) => WebError::GatewayTimeout { timeout_ms: ms },
703 CaptureError::InvalidFormat(fmt) => {
704 WebError::invalid_request(format!("Invalid format: {}", fmt))
705 }
706 _ => WebError::internal(e.to_string()),
707 },
708 Error::Io(e) => WebError::internal(format!("I/O error: {}", e)),
709 Error::Json(e) => WebError::invalid_request(format!("JSON error: {}", e)),
710 Error::Cdp(msg) => WebError::internal(format!("CDP error: {}", msg)),
711 Error::Generic(msg) => WebError::internal(msg),
712 }
713 }
714}
715
716impl From<chromiumoxide::error::CdpError> for Error {
718 fn from(err: chromiumoxide::error::CdpError) -> Self {
719 Error::Cdp(err.to_string())
720 }
721}
722
723impl From<std::io::Error> for WebError {
728 fn from(err: std::io::Error) -> Self {
729 WebError::internal(format!("I/O error: {}", err))
730 }
731}
732
733impl From<serde_json::Error> for WebError {
734 fn from(err: serde_json::Error) -> Self {
735 WebError::invalid_request(format!("JSON error: {}", err))
736 }
737}
738
739impl From<anyhow::Error> for WebError {
740 fn from(err: anyhow::Error) -> Self {
741 WebError::ProcessingError(err.to_string())
742 }
743}
744
745pub fn generate_request_id() -> String {
751 use rand::Rng;
752 let mut rng = rand::rng();
753 let id: u64 = rng.random();
754 format!("req_{:016x}", id)
755}
756
757#[derive(Debug, Clone)]
759pub struct RequestContext {
760 pub request_id: String,
762 pub start_time: std::time::Instant,
764}
765
766impl RequestContext {
767 pub fn new() -> Self {
769 Self {
770 request_id: generate_request_id(),
771 start_time: std::time::Instant::now(),
772 }
773 }
774
775 pub fn with_id(request_id: impl Into<String>) -> Self {
777 Self {
778 request_id: request_id.into(),
779 start_time: std::time::Instant::now(),
780 }
781 }
782
783 pub fn elapsed_ms(&self) -> u64 {
785 self.start_time.elapsed().as_millis() as u64
786 }
787
788 pub fn log_error(&self, error: &WebError) {
790 error.log(Some(&self.request_id));
791 }
792}
793
794impl Default for RequestContext {
795 fn default() -> Self {
796 Self::new()
797 }
798}
799
800impl fmt::Display for RequestContext {
801 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
802 write!(f, "[{}]", self.request_id)
803 }
804}
805
806#[cfg(test)]
811mod tests {
812 use super::*;
813
814 #[test]
815 fn test_web_error_status_codes() {
816 assert_eq!(WebError::invalid_request("test").status_code(), 400);
817 assert_eq!(WebError::unauthorized("test").status_code(), 401);
818 assert_eq!(WebError::forbidden("test").status_code(), 403);
819 assert_eq!(WebError::not_found("test").status_code(), 404);
820 assert_eq!(WebError::content_too_large(100, 50).status_code(), 413);
821 assert_eq!(WebError::rate_limited(60).status_code(), 429);
822 assert_eq!(WebError::internal("test").status_code(), 500);
823 assert_eq!(WebError::service_unavailable("test").status_code(), 503);
824 assert_eq!(WebError::gateway_timeout(5000).status_code(), 504);
825 }
826
827 #[test]
828 fn test_web_error_codes() {
829 assert_eq!(
830 WebError::invalid_request("test").error_code(),
831 "INVALID_REQUEST"
832 );
833 assert_eq!(WebError::unauthorized("test").error_code(), "UNAUTHORIZED");
834 assert_eq!(WebError::forbidden("test").error_code(), "FORBIDDEN");
835 assert_eq!(WebError::not_found("test").error_code(), "NOT_FOUND");
836 assert_eq!(
837 WebError::content_too_large(100, 50).error_code(),
838 "CONTENT_TOO_LARGE"
839 );
840 assert_eq!(WebError::rate_limited(60).error_code(), "RATE_LIMITED");
841 assert_eq!(WebError::internal("test").error_code(), "INTERNAL_ERROR");
842 }
843
844 #[test]
845 fn test_web_error_json() {
846 let err = WebError::invalid_request("Missing name field");
847 let json = err.to_json();
848
849 assert_eq!(json["code"], "INVALID_REQUEST");
850 assert!(json["error"]
851 .as_str()
852 .unwrap()
853 .contains("Missing name field"));
854 }
855
856 #[test]
857 fn test_web_error_json_with_request_id() {
858 let err = WebError::rate_limited(120);
859 let json = err.to_json_with_request_id("req_abc123");
860
861 assert_eq!(json["code"], "RATE_LIMITED");
862 assert_eq!(json["request_id"], "req_abc123");
863 }
864
865 #[test]
866 fn test_web_error_factory_methods() {
867 let err = WebError::missing_field("email");
868 assert!(err.to_string().contains("Missing required field: email"));
869
870 let err = WebError::invalid_field("age", "must be positive");
871 assert!(err.to_string().contains("Invalid value for field 'age'"));
872 }
873
874 #[test]
875 fn test_web_error_retryable() {
876 assert!(!WebError::invalid_request("test").is_retryable());
877 assert!(!WebError::unauthorized("test").is_retryable());
878 assert!(WebError::rate_limited(60).is_retryable());
879 assert!(WebError::service_unavailable("test").is_retryable());
880 assert!(WebError::gateway_timeout(5000).is_retryable());
881 }
882
883 #[test]
884 fn test_web_error_retry_after() {
885 assert_eq!(WebError::rate_limited(120).retry_after(), Some(120));
886 assert_eq!(WebError::invalid_request("test").retry_after(), None);
887 }
888
889 #[test]
890 fn test_error_display() {
891 let err = Error::Browser(BrowserError::LaunchFailed("no chrome".to_string()));
892 assert!(err.to_string().contains("Failed to launch browser"));
893 assert!(err.to_string().contains("no chrome"));
894 }
895
896 #[test]
897 fn test_mcp_error() {
898 let err = McpError::ToolNotFound("unknown_tool".to_string());
899 assert_eq!(err.to_string(), "Tool not found: unknown_tool");
900 }
901
902 #[test]
903 fn test_extraction_error() {
904 let err = ExtractionError::ElementNotFound("#missing".to_string());
905 assert!(err.to_string().contains("Element not found"));
906 }
907
908 #[test]
909 fn test_navigation_error() {
910 let err = NavigationError::HttpError {
911 status: 404,
912 message: "Not Found".to_string(),
913 };
914 assert!(err.to_string().contains("404"));
915 assert!(err.to_string().contains("Not Found"));
916 }
917
918 #[test]
919 fn test_generic_error() {
920 let err = Error::generic("something went wrong");
921 assert_eq!(err.to_string(), "something went wrong");
922 }
923
924 #[test]
925 fn test_error_into_web_error() {
926 let err = Error::Navigation(NavigationError::InvalidUrl("bad-url".to_string()));
927 let web_err = err.into_web_error();
928 assert_eq!(web_err.status_code(), 400);
929 assert!(web_err.to_string().contains("Invalid URL"));
930
931 let err = Error::Navigation(NavigationError::Timeout(5000));
932 let web_err = err.into_web_error();
933 assert_eq!(web_err.status_code(), 504);
934 }
935
936 #[test]
937 fn test_error_response_serialization() {
938 let response = ErrorResponse::new("Test error", "TEST_ERROR")
939 .with_request_id("req_123")
940 .with_details(json!({"field": "name"}));
941
942 let json = serde_json::to_string(&response).unwrap();
943 assert!(json.contains("TEST_ERROR"));
944 assert!(json.contains("req_123"));
945 assert!(json.contains("name"));
946 }
947
948 #[test]
949 fn test_request_context() {
950 let ctx = RequestContext::new();
951 assert!(ctx.request_id.starts_with("req_"));
952 assert_eq!(ctx.request_id.len(), 20); let ctx = RequestContext::with_id("custom-id-123");
955 assert_eq!(ctx.request_id, "custom-id-123");
956 }
957
958 #[test]
959 fn test_generate_request_id() {
960 let id1 = generate_request_id();
961 let id2 = generate_request_id();
962 assert_ne!(id1, id2);
963 assert!(id1.starts_with("req_"));
964 }
965
966 #[test]
967 fn test_web_error_from_io_error() {
968 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
969 let web_err: WebError = io_err.into();
970 assert_eq!(web_err.status_code(), 500);
971 }
972
973 #[test]
974 fn test_content_too_large_display() {
975 let err = WebError::content_too_large(1024 * 1024 * 10, 1024 * 1024);
976 assert!(err.to_string().contains("10485760"));
977 assert!(err.to_string().contains("1048576"));
978 }
979}