ads_client/
error.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2* License, v. 2.0. If a copy of the MPL was not distributed with this
3* file, You can obtain one at http://mozilla.org/MPL/2.0/.
4*/
5
6use error_support::{error, ErrorHandling, GetErrorHandling};
7use viaduct::Response;
8
9pub type ApiResult<T> = std::result::Result<T, ApiError>;
10
11#[derive(Debug, thiserror::Error, uniffi::Error)]
12pub enum ApiError {
13    #[error("Something unexpected occurred.")]
14    Other { reason: String },
15}
16
17#[derive(Debug, thiserror::Error)]
18pub enum ComponentError {
19    #[error("Error requesting ads: {0}")]
20    RequestAds(#[from] RequestAdsError),
21
22    #[error("Error recording a click for a placement: {0}")]
23    RecordClick(#[from] RecordClickError),
24
25    #[error("Error recording an impressions for a placement: {0}")]
26    RecordImpression(#[from] RecordImpressionError),
27
28    #[error("Error reporting an ad: {0}")]
29    ReportAd(#[from] ReportAdError),
30}
31
32impl GetErrorHandling for ComponentError {
33    type ExternalError = ApiError;
34
35    fn get_error_handling(&self) -> ErrorHandling<Self::ExternalError> {
36        ErrorHandling::convert(ApiError::Other {
37            reason: self.to_string(),
38        })
39    }
40}
41
42#[derive(Debug, thiserror::Error)]
43pub enum RequestAdsError {
44    #[error("Error building ad requests from configs: {0}")]
45    BuildRequest(#[from] BuildRequestError),
46
47    #[error("Error requesting ads from MARS: {0}")]
48    FetchAds(#[from] FetchAdsError),
49
50    #[error("Error building placements from ad response: {0}")]
51    BuildPlacements(#[from] BuildPlacementsError),
52}
53
54#[derive(Debug, thiserror::Error)]
55pub enum BuildRequestError {
56    #[error("Could not build request with empty placement configs")]
57    EmptyConfig,
58
59    #[error("Duplicate placement_id found: {placement_id}. Placement_ids must be unique.")]
60    DuplicatePlacementId { placement_id: String },
61}
62
63#[derive(Debug, thiserror::Error)]
64pub enum BuildPlacementsError {
65    #[error("Duplicate placement_id found: {placement_id}. Placement_ids must be unique.")]
66    DuplicatePlacementId { placement_id: String },
67}
68
69#[derive(Debug, thiserror::Error)]
70pub enum FetchAdsError {
71    #[error("URL parse error: {0}")]
72    UrlParse(#[from] url::ParseError),
73
74    #[error("Error sending request: {0}")]
75    Request(#[from] viaduct::Error),
76
77    #[error("JSON error: {0}")]
78    Json(#[from] serde_json::Error),
79
80    #[error("Could not fetch ads, MARS responded with: {0}")]
81    HTTPError(#[from] HTTPError),
82}
83
84#[derive(Debug, thiserror::Error)]
85pub enum EmitTelemetryError {
86    #[error("URL parse error: {0}")]
87    UrlParse(#[from] url::ParseError),
88
89    #[error("Error sending request: {0}")]
90    Request(#[from] viaduct::Error),
91
92    #[error("JSON error: {0}")]
93    Json(#[from] serde_json::Error),
94
95    #[error("Could not fetch ads, MARS responded with: {0}")]
96    HTTPError(#[from] HTTPError),
97}
98
99#[derive(Debug, thiserror::Error)]
100pub enum CallbackRequestError {
101    #[error("URL parse error: {0}")]
102    UrlParse(#[from] url::ParseError),
103
104    #[error("Error sending request: {0}")]
105    Request(#[from] viaduct::Error),
106
107    #[error("JSON error: {0}")]
108    Json(#[from] serde_json::Error),
109
110    #[error("Could not fetch ads, MARS responded with: {0}")]
111    HTTPError(#[from] HTTPError),
112
113    #[error("Callback URL missing: {message}")]
114    MissingCallback { message: String },
115}
116
117#[derive(Debug, thiserror::Error)]
118pub enum RecordImpressionError {
119    #[error("Callback request to MARS failed: {0}")]
120    CallbackRequest(#[from] CallbackRequestError),
121}
122
123#[derive(Debug, thiserror::Error)]
124pub enum RecordClickError {
125    #[error("Callback request to MARS failed: {0}")]
126    CallbackRequest(#[from] CallbackRequestError),
127}
128
129#[derive(Debug, thiserror::Error)]
130pub enum ReportAdError {
131    #[error("Callback request to MARS failed: {0}")]
132    CallbackRequest(#[from] CallbackRequestError),
133}
134
135#[derive(Debug, thiserror::Error)]
136pub enum HTTPError {
137    #[error("Bad request ({code}): {message}")]
138    BadRequest { code: u16, message: String },
139
140    #[error("Server error ({code}): {message}")]
141    Server { code: u16, message: String },
142
143    #[error("Unexpected error ({code}): {message}")]
144    Unexpected { code: u16, message: String },
145}
146
147pub fn check_http_status_for_error(response: &Response) -> Result<(), HTTPError> {
148    let status = response.status;
149
150    if status == 200 {
151        return Ok(());
152    }
153    let error_message = response.text();
154    let error = match status {
155        400 => HTTPError::BadRequest {
156            code: status,
157            message: error_message.to_string(),
158        },
159        500..=599 => HTTPError::Server {
160            code: status,
161            message: error_message.to_string(),
162        },
163        _ => HTTPError::Unexpected {
164            code: status,
165            message: error_message.to_string(),
166        },
167    };
168    Err(error)
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174    use url::Url;
175
176    fn mock_response(status: u16, body: &str) -> Response {
177        Response {
178            request_method: viaduct::Method::Get,
179            url: Url::parse("https://example.com").unwrap(),
180            status,
181            headers: viaduct::Headers::new(),
182            body: body.as_bytes().to_vec(),
183        }
184    }
185
186    #[test]
187    fn test_ok_status_returns_ok() {
188        let response = mock_response(200, "OK");
189        let result = check_http_status_for_error(&response);
190        assert!(result.is_ok());
191    }
192
193    #[test]
194    fn test_bad_request_returns_http_error() {
195        let response = mock_response(400, "Bad input");
196        let result = check_http_status_for_error(&response);
197        assert!(
198            matches!(result, Err(HTTPError::BadRequest { code, message }) if code == 400 && message == "Bad input")
199        );
200    }
201
202    #[test]
203    fn test_server_error_500() {
204        let response = mock_response(500, "Something broke");
205        let result = check_http_status_for_error(&response);
206        assert!(
207            matches!(result, Err(HTTPError::Server { code, message }) if code == 500 && message == "Something broke")
208        );
209    }
210}