Skip to main content

oxigdal_services/
error.rs

1//! Error types for OGC web services
2//!
3//! Provides comprehensive error handling for WFS, WCS, WPS, and CSW services
4//! with OGC-compliant exception responses.
5
6use axum::{
7    http::{StatusCode, header},
8    response::{IntoResponse, Response},
9};
10use thiserror::Error;
11
12/// Service errors for OGC web services
13#[derive(Debug, Error)]
14pub enum ServiceError {
15    /// Invalid request parameter
16    #[error("Invalid parameter '{0}': {1}")]
17    InvalidParameter(String, String),
18
19    /// Missing required parameter
20    #[error("Missing required parameter: {0}")]
21    MissingParameter(String),
22
23    /// Feature/layer not found
24    #[error("Resource not found: {0}")]
25    NotFound(String),
26
27    /// Invalid CRS reference
28    #[error("Invalid CRS: {0}")]
29    InvalidCrs(String),
30
31    /// Invalid bounding box
32    #[error("Invalid bounding box: {0}")]
33    InvalidBbox(String),
34
35    /// Unsupported format
36    #[error("Unsupported format: {0}")]
37    UnsupportedFormat(String),
38
39    /// Unsupported operation
40    #[error("Unsupported operation: {0}")]
41    UnsupportedOperation(String),
42
43    /// Invalid XML request
44    #[error("Invalid XML: {0}")]
45    InvalidXml(String),
46
47    /// Invalid GeoJSON
48    #[error("Invalid GeoJSON: {0}")]
49    InvalidGeoJson(String),
50
51    /// Transaction error
52    #[error("Transaction error: {0}")]
53    Transaction(String),
54
55    /// Process execution error
56    #[error("Process execution error: {0}")]
57    ProcessExecution(String),
58
59    /// Coverage access error
60    #[error("Coverage error: {0}")]
61    Coverage(String),
62
63    /// Catalog search error
64    #[error("Catalog error: {0}")]
65    Catalog(String),
66
67    /// IO error
68    #[error("IO error: {0}")]
69    Io(#[from] std::io::Error),
70
71    /// OxiGDAL error
72    #[error("OxiGDAL error: {0}")]
73    OxiGdal(#[from] oxigdal_core::OxiGdalError),
74
75    /// Serialization error
76    #[error("Serialization error: {0}")]
77    Serialization(String),
78
79    /// XML writing error
80    #[error("XML error: {0}")]
81    Xml(String),
82
83    /// Internal server error
84    #[error("Internal server error: {0}")]
85    Internal(String),
86}
87
88impl From<quick_xml::Error> for ServiceError {
89    fn from(err: quick_xml::Error) -> Self {
90        ServiceError::Xml(err.to_string())
91    }
92}
93
94impl From<serde_json::Error> for ServiceError {
95    fn from(err: serde_json::Error) -> Self {
96        ServiceError::Serialization(err.to_string())
97    }
98}
99
100impl From<geojson::Error> for ServiceError {
101    fn from(err: geojson::Error) -> Self {
102        ServiceError::InvalidGeoJson(err.to_string())
103    }
104}
105
106impl IntoResponse for ServiceError {
107    fn into_response(self) -> Response {
108        let (status, exception_code, exception_text) = match &self {
109            ServiceError::InvalidParameter(_, _) | ServiceError::MissingParameter(_) => (
110                StatusCode::BAD_REQUEST,
111                "InvalidParameterValue",
112                self.to_string(),
113            ),
114            ServiceError::NotFound(_) => (StatusCode::NOT_FOUND, "NotFound", self.to_string()),
115            ServiceError::UnsupportedOperation(_) | ServiceError::UnsupportedFormat(_) => (
116                StatusCode::BAD_REQUEST,
117                "OperationNotSupported",
118                self.to_string(),
119            ),
120            ServiceError::InvalidXml(_) | ServiceError::InvalidGeoJson(_) => {
121                (StatusCode::BAD_REQUEST, "InvalidRequest", self.to_string())
122            }
123            ServiceError::InvalidCrs(_) => {
124                (StatusCode::BAD_REQUEST, "InvalidCRS", self.to_string())
125            }
126            ServiceError::InvalidBbox(_) => (
127                StatusCode::BAD_REQUEST,
128                "InvalidBoundingBox",
129                self.to_string(),
130            ),
131            _ => (
132                StatusCode::INTERNAL_SERVER_ERROR,
133                "NoApplicableCode",
134                self.to_string(),
135            ),
136        };
137
138        // Return OGC ServiceExceptionReport format
139        let xml = format!(
140            r#"<?xml version="1.0" encoding="UTF-8"?>
141<ExceptionReport version="2.0.0"
142    xmlns="http://www.opengis.net/ows/2.0"
143    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
144    xsi:schemaLocation="http://www.opengis.net/ows/2.0 http://schemas.opengis.net/ows/2.0/owsExceptionReport.xsd">
145  <Exception exceptionCode="{}">
146    <ExceptionText>{}</ExceptionText>
147  </Exception>
148</ExceptionReport>"#,
149            exception_code, exception_text
150        );
151
152        (status, [(header::CONTENT_TYPE, "application/xml")], xml).into_response()
153    }
154}
155
156/// Result type for service operations
157pub type ServiceResult<T> = Result<T, ServiceError>;
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn test_error_display() {
165        let err = ServiceError::MissingParameter("VERSION".to_string());
166        assert_eq!(err.to_string(), "Missing required parameter: VERSION");
167
168        let err = ServiceError::InvalidParameter("BBOX".to_string(), "malformed".to_string());
169        assert_eq!(err.to_string(), "Invalid parameter 'BBOX': malformed");
170    }
171
172    #[test]
173    fn test_error_conversion() {
174        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
175        let svc_err: ServiceError = io_err.into();
176        assert!(matches!(svc_err, ServiceError::Io(_)));
177    }
178}