use std::error::Error as StdError;
use std::fmt;
use reqwest::header::InvalidHeaderValue;
use reqwest::{Error as ReqwestError, Method, Response, StatusCode};
use serde::de::{Deserialize, Deserializer, Error as _};
use url::ParseError as UrlError;
use crate::internal::prelude::*;
use crate::json::*;
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[non_exhaustive]
pub struct DiscordJsonError {
pub code: isize,
pub message: String,
#[serde(default, deserialize_with = "deserialize_errors")]
pub errors: Vec<DiscordJsonSingleError>,
}
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
pub struct DiscordJsonSingleError {
pub code: String,
pub message: String,
pub path: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub struct ErrorResponse {
pub status_code: StatusCode,
pub url: String,
pub method: Method,
pub error: DiscordJsonError,
}
impl ErrorResponse {
pub async fn from_response(r: Response, method: Method) -> Self {
ErrorResponse {
status_code: r.status(),
url: r.url().to_string(),
method,
error: decode_resp(r).await.unwrap_or_else(|e| DiscordJsonError {
code: -1,
message: format!("[Serenity] Could not decode json when receiving error response from discord:, {e}"),
errors: vec![],
}),
}
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum HttpError {
UnsuccessfulRequest(ErrorResponse),
RateLimitI64F64,
RateLimitUtf8,
Url(UrlError),
InvalidWebhook,
InvalidHeader(InvalidHeaderValue),
Request(ReqwestError),
InvalidScheme,
InvalidPort,
ApplicationIdMissing,
}
impl HttpError {
#[must_use]
pub fn is_unsuccessful_request(&self) -> bool {
matches!(self, Self::UnsuccessfulRequest(_))
}
#[must_use]
pub fn is_url_error(&self) -> bool {
matches!(self, Self::Url(_))
}
#[must_use]
pub fn is_invalid_header(&self) -> bool {
matches!(self, Self::InvalidHeader(_))
}
#[must_use]
pub fn status_code(&self) -> Option<StatusCode> {
match self {
Self::UnsuccessfulRequest(res) => Some(res.status_code),
_ => None,
}
}
}
impl From<ErrorResponse> for HttpError {
fn from(error: ErrorResponse) -> Self {
Self::UnsuccessfulRequest(error)
}
}
impl From<ReqwestError> for HttpError {
fn from(error: ReqwestError) -> Self {
Self::Request(error)
}
}
impl From<UrlError> for HttpError {
fn from(error: UrlError) -> Self {
Self::Url(error)
}
}
impl From<InvalidHeaderValue> for HttpError {
fn from(error: InvalidHeaderValue) -> Self {
Self::InvalidHeader(error)
}
}
impl fmt::Display for HttpError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnsuccessfulRequest(e) => {
f.write_str(&e.error.message)?;
let mut errors_iter = e.error.errors.iter();
if let Some(error) = errors_iter.next() {
f.write_str(" (")?;
f.write_str(&error.path)?;
f.write_str(": ")?;
f.write_str(&error.message)?;
for error in errors_iter {
f.write_str(", ")?;
f.write_str(&error.path)?;
f.write_str(": ")?;
f.write_str(&error.message)?;
}
f.write_str(")")?;
}
Ok(())
},
Self::RateLimitI64F64 => f.write_str("Error decoding a header into an i64 or f64"),
Self::RateLimitUtf8 => f.write_str("Error decoding a header from UTF-8"),
Self::Url(_) => f.write_str("Provided URL is incorrect."),
Self::InvalidWebhook => f.write_str("Provided URL is not a valid webhook."),
Self::InvalidHeader(_) => f.write_str("Provided value is an invalid header value."),
Self::Request(_) => f.write_str("Error while sending HTTP request."),
Self::InvalidScheme => f.write_str("Invalid Url scheme."),
Self::InvalidPort => f.write_str("Invalid port."),
Self::ApplicationIdMissing => f.write_str("Application id was expected but missing."),
}
}
}
impl StdError for HttpError {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match self {
Self::Url(inner) => Some(inner),
Self::Request(inner) => Some(inner),
_ => None,
}
}
}
#[allow(clippy::missing_errors_doc)]
pub fn deserialize_errors<'de, D: Deserializer<'de>>(
deserializer: D,
) -> StdResult<Vec<DiscordJsonSingleError>, D::Error> {
let map: Value = Value::deserialize(deserializer)?;
if !map.is_object() {
return Ok(vec![]);
}
let mut errors = Vec::new();
let mut path = Vec::new();
loop_errors(&map, &mut errors, &mut path).map_err(D::Error::custom)?;
Ok(errors)
}
fn make_error(
errors_value: &Value,
errors: &mut Vec<DiscordJsonSingleError>,
path: &[&str],
) -> StdResult<(), &'static str> {
let found_errors = errors_value.as_array().ok_or("expected array")?;
for error in found_errors {
let error_object = error.as_object().ok_or("expected object")?;
errors.push(DiscordJsonSingleError {
code: error_object
.get("code")
.ok_or("expected code")?
.as_str()
.ok_or("expected string")?
.to_owned(),
message: error_object
.get("message")
.ok_or("expected message")?
.as_str()
.ok_or("expected string")?
.to_owned(),
path: path.join("."),
});
}
Ok(())
}
fn loop_errors<'a>(
value: &'a Value,
errors: &mut Vec<DiscordJsonSingleError>,
path: &mut Vec<&'a str>,
) -> StdResult<(), &'static str> {
for (key, value) in value.as_object().ok_or("expected object")? {
if key == "_errors" {
make_error(value, errors, path)?;
} else {
path.push(key);
loop_errors(value, errors, path)?;
path.pop();
}
}
Ok(())
}
#[cfg(test)]
mod test {
use http_crate::response::Builder;
use reqwest::ResponseBuilderExt;
use super::*;
#[tokio::test]
async fn test_error_response_into() {
let error = DiscordJsonError {
code: 43121215,
message: String::from("This is a Ferris error"),
errors: vec![],
};
let mut builder = Builder::new();
builder = builder.status(403);
builder = builder.url(String::from("https://ferris.crab").parse().unwrap());
let body_string = to_string(&error).unwrap();
let response = builder.body(body_string.into_bytes()).unwrap();
let reqwest_response: reqwest::Response = response.into();
let error_response = ErrorResponse::from_response(reqwest_response, Method::POST).await;
let known = ErrorResponse {
status_code: reqwest::StatusCode::from_u16(403).unwrap(),
url: String::from("https://ferris.crab/"),
method: Method::POST,
error,
};
assert_eq!(error_response, known);
}
}