use std::{error::Error as StdError, fmt};
use crate::core::redaction::redact_auth_query_params_in_text;
pub struct RedactedHttpError {
message: String,
}
impl RedactedHttpError {
pub(crate) fn new(error: &reqwest::Error) -> Self {
Self {
message: redact_auth_query_params_in_text(&error.to_string()),
}
}
}
impl fmt::Display for RedactedHttpError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)
}
}
impl fmt::Debug for RedactedHttpError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
impl std::error::Error for RedactedHttpError {}
pub struct WebsocketError {
source: Box<dyn StdError + Send + Sync + 'static>,
}
impl WebsocketError {
#[cfg(feature = "stream")]
pub(crate) fn new(source: tokio_tungstenite::tungstenite::Error) -> Self {
Self {
source: Box::new(source),
}
}
fn as_source(&self) -> &(dyn StdError + 'static) {
self.source.as_ref()
}
}
impl fmt::Display for WebsocketError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.source, f)
}
}
impl fmt::Debug for WebsocketError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
impl StdError for WebsocketError {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
Some(self.as_source())
}
}
macro_rules! boxed_source_error {
(
$(#[$meta:meta])*
$name:ident
) => {
$(#[$meta])*
pub struct $name {
source: Box<dyn StdError + Send + Sync + 'static>,
}
impl $name {
#[cfg(feature = "stream")]
fn from_source(source: impl StdError + Send + Sync + 'static) -> Self {
Self {
source: Box::new(source),
}
}
fn as_source(&self) -> &(dyn StdError + 'static) {
self.source.as_ref()
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.source, f)
}
}
impl fmt::Debug for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
impl StdError for $name {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
Some(self.as_source())
}
}
};
}
macro_rules! opaque_source_error {
(
$(#[$meta:meta])*
$name:ident($source:ty)
) => {
$(#[$meta])*
pub struct $name {
source: $source,
}
impl $name {
pub(crate) const fn new(source: $source) -> Self {
Self { source }
}
fn as_source(&self) -> &(dyn StdError + 'static) {
&self.source
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.source, f)
}
}
impl fmt::Debug for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
impl StdError for $name {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
Some(self.as_source())
}
}
};
}
boxed_source_error!(
ProtobufDecodeError
);
opaque_source_error!(
JsonError(serde_json::Error)
);
boxed_source_error!(
Base64DecodeError
);
opaque_source_error!(
UrlParseError(url::ParseError)
);
#[non_exhaustive]
#[derive(Debug)]
pub enum YfError {
Http(RedactedHttpError),
Websocket(WebsocketError),
Protobuf(ProtobufDecodeError),
Json(JsonError),
Base64(Base64DecodeError),
Url(UrlParseError),
NotFound {
url: String,
},
RateLimited {
url: String,
},
ServerError {
status: u16,
url: String,
},
Status {
status: u16,
url: String,
},
Api(String),
Auth(String),
Scrape(String),
MissingData(String),
InvalidData(String),
OptionUnderlyingTypeUnavailable {
symbol: String,
quote_type: Option<String>,
},
DataQuality(Box<crate::core::diagnostics::YfWarning>),
InvalidParams(String),
RequestNotCloneable,
Money(paft::money::MoneyError),
InvalidDates,
}
impl YfError {
#[cfg(feature = "stream")]
pub(crate) fn websocket(error: tokio_tungstenite::tungstenite::Error) -> Self {
Self::Websocket(WebsocketError::new(error))
}
#[cfg(feature = "stream")]
pub(crate) fn protobuf(error: prost::DecodeError) -> Self {
Self::Protobuf(ProtobufDecodeError::from_source(error))
}
pub(crate) const fn json(error: serde_json::Error) -> Self {
Self::Json(JsonError::new(error))
}
#[cfg(feature = "stream")]
pub(crate) fn base64(error: base64::DecodeError) -> Self {
Self::Base64(Base64DecodeError::from_source(error))
}
pub(crate) const fn url(error: url::ParseError) -> Self {
Self::Url(UrlParseError::new(error))
}
}
impl fmt::Display for YfError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Http(error) => write!(f, "HTTP error: {error}"),
Self::Websocket(error) => write!(f, "WebSocket error: {error}"),
Self::Protobuf(error) => write!(f, "Protobuf decoding error: {error}"),
Self::Json(error) => write!(f, "JSON parsing error: {error}"),
Self::Base64(error) => write!(f, "Base64 decoding error: {error}"),
Self::Url(error) => write!(f, "Invalid URL: {error}"),
Self::NotFound { url } => write!(f, "Not found at {url}"),
Self::RateLimited { url } => write!(f, "Rate limited at {url}"),
Self::ServerError { status, url } => write!(f, "Server error {status} at {url}"),
Self::Status { status, url } => {
write!(f, "Unexpected response status: {status} at {url}")
}
Self::Api(message) => write!(f, "Yahoo API error: {message}"),
Self::Auth(message) => write!(f, "Authentication error: {message}"),
Self::Scrape(message) => write!(f, "Web scraping error: {message}"),
Self::MissingData(message) => write!(f, "Missing data in response: {message}"),
Self::InvalidData(message) => write!(f, "Invalid data in response: {message}"),
Self::OptionUnderlyingTypeUnavailable { symbol, .. } => {
write!(
f,
"contracts present, underlying type unavailable for {symbol}"
)
}
Self::DataQuality(warning) => write!(f, "Provider data quality issue: {warning}"),
Self::InvalidParams(message) => write!(f, "Invalid parameters: {message}"),
Self::RequestNotCloneable => write!(f, "Request cannot be cloned for retry"),
Self::Money(error) => write!(f, "Money data error: {error}"),
Self::InvalidDates => {
f.write_str("Invalid date range: start date must be before end date")
}
}
}
}
impl StdError for YfError {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match self {
Self::Websocket(error) => Some(error.as_source()),
Self::Protobuf(error) => Some(error.as_source()),
Self::Json(error) => Some(error.as_source()),
Self::Base64(error) => Some(error.as_source()),
Self::Url(error) => Some(error.as_source()),
Self::Money(error) => Some(error),
_ => None,
}
}
}
impl From<reqwest::Error> for YfError {
fn from(e: reqwest::Error) -> Self {
Self::Http(RedactedHttpError::new(&e))
}
}
impl From<paft::money::MoneyError> for YfError {
fn from(error: paft::money::MoneyError) -> Self {
Self::Money(error)
}
}
#[cfg(test)]
mod tests {
use std::error::Error as _;
use super::*;
fn assert_source<T>(error: &YfError)
where
T: std::error::Error + 'static,
{
assert!(error.source().expect("error source").is::<T>());
}
#[test]
fn opaque_json_and_url_errors_preserve_foreign_sources() {
let json = YfError::json(
serde_json::from_str::<serde_json::Value>("{").expect_err("invalid JSON"),
);
assert!(matches!(json, YfError::Json(_)));
assert_source::<serde_json::Error>(&json);
let url = YfError::url(url::Url::parse("://").expect_err("invalid URL"));
assert!(matches!(url, YfError::Url(_)));
assert_source::<url::ParseError>(&url);
}
#[cfg(feature = "stream")]
#[test]
fn opaque_stream_errors_preserve_foreign_sources() {
use base64::{Engine as _, engine::general_purpose};
use prost::DecodeError;
let websocket = YfError::websocket(tokio_tungstenite::tungstenite::Error::ConnectionClosed);
assert!(matches!(websocket, YfError::Websocket(_)));
assert_source::<tokio_tungstenite::tungstenite::Error>(&websocket);
let protobuf = YfError::protobuf(DecodeError::new("invalid protobuf"));
assert!(matches!(protobuf, YfError::Protobuf(_)));
assert_source::<prost::DecodeError>(&protobuf);
let base64 = YfError::base64(
general_purpose::STANDARD
.decode("!")
.expect_err("invalid base64"),
);
assert!(matches!(base64, YfError::Base64(_)));
assert_source::<base64::DecodeError>(&base64);
}
}