1use std::{error::Error as StdError, fmt};
2
3use crate::core::redaction::redact_auth_query_params_in_text;
4
5pub struct RedactedHttpError {
11 message: String,
12}
13
14impl RedactedHttpError {
15 pub(crate) fn new(error: &reqwest::Error) -> Self {
16 Self {
17 message: redact_auth_query_params_in_text(&error.to_string()),
18 }
19 }
20}
21
22impl fmt::Display for RedactedHttpError {
23 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24 f.write_str(&self.message)
25 }
26}
27
28impl fmt::Debug for RedactedHttpError {
29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 fmt::Display::fmt(self, f)
31 }
32}
33
34impl std::error::Error for RedactedHttpError {}
35
36pub struct WebsocketError {
38 source: Box<dyn StdError + Send + Sync + 'static>,
39}
40
41impl WebsocketError {
42 #[cfg(feature = "stream")]
43 pub(crate) fn new(source: tokio_tungstenite::tungstenite::Error) -> Self {
44 Self {
45 source: Box::new(source),
46 }
47 }
48
49 fn as_source(&self) -> &(dyn StdError + 'static) {
50 self.source.as_ref()
51 }
52}
53
54impl fmt::Display for WebsocketError {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 fmt::Display::fmt(&self.source, f)
57 }
58}
59
60impl fmt::Debug for WebsocketError {
61 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62 fmt::Display::fmt(self, f)
63 }
64}
65
66impl StdError for WebsocketError {
67 fn source(&self) -> Option<&(dyn StdError + 'static)> {
68 Some(self.as_source())
69 }
70}
71
72macro_rules! boxed_source_error {
73 (
74 $(#[$meta:meta])*
75 $name:ident
76 ) => {
77 $(#[$meta])*
78 pub struct $name {
79 source: Box<dyn StdError + Send + Sync + 'static>,
80 }
81
82 impl $name {
83 #[cfg(feature = "stream")]
84 fn from_source(source: impl StdError + Send + Sync + 'static) -> Self {
85 Self {
86 source: Box::new(source),
87 }
88 }
89
90 fn as_source(&self) -> &(dyn StdError + 'static) {
91 self.source.as_ref()
92 }
93 }
94
95 impl fmt::Display for $name {
96 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97 fmt::Display::fmt(&self.source, f)
98 }
99 }
100
101 impl fmt::Debug for $name {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 fmt::Display::fmt(self, f)
104 }
105 }
106
107 impl StdError for $name {
108 fn source(&self) -> Option<&(dyn StdError + 'static)> {
109 Some(self.as_source())
110 }
111 }
112 };
113}
114
115macro_rules! opaque_source_error {
116 (
117 $(#[$meta:meta])*
118 $name:ident($source:ty)
119 ) => {
120 $(#[$meta])*
121 pub struct $name {
122 source: $source,
123 }
124
125 impl $name {
126 pub(crate) const fn new(source: $source) -> Self {
127 Self { source }
128 }
129
130 fn as_source(&self) -> &(dyn StdError + 'static) {
131 &self.source
132 }
133 }
134
135 impl fmt::Display for $name {
136 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137 fmt::Display::fmt(&self.source, f)
138 }
139 }
140
141 impl fmt::Debug for $name {
142 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143 fmt::Display::fmt(self, f)
144 }
145 }
146
147 impl StdError for $name {
148 fn source(&self) -> Option<&(dyn StdError + 'static)> {
149 Some(self.as_source())
150 }
151 }
152 };
153}
154
155boxed_source_error!(
156 ProtobufDecodeError
158);
159
160opaque_source_error!(
161 JsonError(serde_json::Error)
163);
164
165boxed_source_error!(
166 Base64DecodeError
168);
169
170opaque_source_error!(
171 UrlParseError(url::ParseError)
173);
174
175#[non_exhaustive]
177#[derive(Debug)]
178pub enum YfError {
179 Http(RedactedHttpError),
181
182 Websocket(WebsocketError),
184
185 Protobuf(ProtobufDecodeError),
187
188 Json(JsonError),
190
191 Base64(Base64DecodeError),
193
194 Url(UrlParseError),
196
197 NotFound {
199 url: String,
201 },
202
203 RateLimited {
205 url: String,
207 },
208
209 ServerError {
211 status: u16,
213 url: String,
215 },
216
217 Status {
219 status: u16,
221 url: String,
223 },
224
225 Api(String),
229
230 Auth(String),
232
233 Scrape(String),
235
236 MissingData(String),
238
239 InvalidData(String),
241
242 OptionUnderlyingTypeUnavailable {
244 symbol: String,
246 quote_type: Option<String>,
248 },
249
250 DataQuality(Box<crate::core::diagnostics::YfWarning>),
252
253 InvalidParams(String),
255
256 RequestNotCloneable,
258
259 Money(paft::money::MoneyError),
261
262 InvalidDates,
264}
265
266impl YfError {
267 #[cfg(feature = "stream")]
268 pub(crate) fn websocket(error: tokio_tungstenite::tungstenite::Error) -> Self {
269 Self::Websocket(WebsocketError::new(error))
270 }
271
272 #[cfg(feature = "stream")]
273 pub(crate) fn protobuf(error: prost::DecodeError) -> Self {
274 Self::Protobuf(ProtobufDecodeError::from_source(error))
275 }
276
277 pub(crate) const fn json(error: serde_json::Error) -> Self {
278 Self::Json(JsonError::new(error))
279 }
280
281 #[cfg(feature = "stream")]
282 pub(crate) fn base64(error: base64::DecodeError) -> Self {
283 Self::Base64(Base64DecodeError::from_source(error))
284 }
285
286 pub(crate) const fn url(error: url::ParseError) -> Self {
287 Self::Url(UrlParseError::new(error))
288 }
289}
290
291impl fmt::Display for YfError {
292 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
293 match self {
294 Self::Http(error) => write!(f, "HTTP error: {error}"),
295 Self::Websocket(error) => write!(f, "WebSocket error: {error}"),
296 Self::Protobuf(error) => write!(f, "Protobuf decoding error: {error}"),
297 Self::Json(error) => write!(f, "JSON parsing error: {error}"),
298 Self::Base64(error) => write!(f, "Base64 decoding error: {error}"),
299 Self::Url(error) => write!(f, "Invalid URL: {error}"),
300 Self::NotFound { url } => write!(f, "Not found at {url}"),
301 Self::RateLimited { url } => write!(f, "Rate limited at {url}"),
302 Self::ServerError { status, url } => write!(f, "Server error {status} at {url}"),
303 Self::Status { status, url } => {
304 write!(f, "Unexpected response status: {status} at {url}")
305 }
306 Self::Api(message) => write!(f, "Yahoo API error: {message}"),
307 Self::Auth(message) => write!(f, "Authentication error: {message}"),
308 Self::Scrape(message) => write!(f, "Web scraping error: {message}"),
309 Self::MissingData(message) => write!(f, "Missing data in response: {message}"),
310 Self::InvalidData(message) => write!(f, "Invalid data in response: {message}"),
311 Self::OptionUnderlyingTypeUnavailable { symbol, .. } => {
312 write!(
313 f,
314 "contracts present, underlying type unavailable for {symbol}"
315 )
316 }
317 Self::DataQuality(warning) => write!(f, "Provider data quality issue: {warning}"),
318 Self::InvalidParams(message) => write!(f, "Invalid parameters: {message}"),
319 Self::RequestNotCloneable => write!(f, "Request cannot be cloned for retry"),
320 Self::Money(error) => write!(f, "Money data error: {error}"),
321 Self::InvalidDates => {
322 f.write_str("Invalid date range: start date must be before end date")
323 }
324 }
325 }
326}
327
328impl StdError for YfError {
329 fn source(&self) -> Option<&(dyn StdError + 'static)> {
330 match self {
331 Self::Websocket(error) => Some(error.as_source()),
332 Self::Protobuf(error) => Some(error.as_source()),
333 Self::Json(error) => Some(error.as_source()),
334 Self::Base64(error) => Some(error.as_source()),
335 Self::Url(error) => Some(error.as_source()),
336 Self::Money(error) => Some(error),
337 _ => None,
338 }
339 }
340}
341
342impl From<reqwest::Error> for YfError {
343 fn from(e: reqwest::Error) -> Self {
344 Self::Http(RedactedHttpError::new(&e))
345 }
346}
347
348impl From<paft::money::MoneyError> for YfError {
349 fn from(error: paft::money::MoneyError) -> Self {
350 Self::Money(error)
351 }
352}
353
354#[cfg(test)]
355mod tests {
356 use std::error::Error as _;
357
358 use super::*;
359
360 fn assert_source<T>(error: &YfError)
361 where
362 T: std::error::Error + 'static,
363 {
364 assert!(error.source().expect("error source").is::<T>());
365 }
366
367 #[test]
368 fn opaque_json_and_url_errors_preserve_foreign_sources() {
369 let json = YfError::json(
370 serde_json::from_str::<serde_json::Value>("{").expect_err("invalid JSON"),
371 );
372 assert!(matches!(json, YfError::Json(_)));
373 assert_source::<serde_json::Error>(&json);
374
375 let url = YfError::url(url::Url::parse("://").expect_err("invalid URL"));
376 assert!(matches!(url, YfError::Url(_)));
377 assert_source::<url::ParseError>(&url);
378 }
379
380 #[cfg(feature = "stream")]
381 #[test]
382 fn opaque_stream_errors_preserve_foreign_sources() {
383 use base64::{Engine as _, engine::general_purpose};
384 use prost::DecodeError;
385
386 let websocket = YfError::websocket(tokio_tungstenite::tungstenite::Error::ConnectionClosed);
387 assert!(matches!(websocket, YfError::Websocket(_)));
388 assert_source::<tokio_tungstenite::tungstenite::Error>(&websocket);
389
390 let protobuf = YfError::protobuf(DecodeError::new("invalid protobuf"));
391 assert!(matches!(protobuf, YfError::Protobuf(_)));
392 assert_source::<prost::DecodeError>(&protobuf);
393
394 let base64 = YfError::base64(
395 general_purpose::STANDARD
396 .decode("!")
397 .expect_err("invalid base64"),
398 );
399 assert!(matches!(base64, YfError::Base64(_)));
400 assert_source::<base64::DecodeError>(&base64);
401 }
402}