Skip to main content

hackerone_api/
error.rs

1//! Error types for the HackerOne API client.
2
3use serde::Deserialize;
4
5/// A single entry from the HackerOne API's `errors` array.
6///
7/// API failures come back as `{"errors":[{"title": …, "detail": …, "status": …}]}`.
8#[derive(Debug, Clone, Deserialize, Default)]
9pub struct ApiError {
10    /// Short summary of the problem.
11    #[serde(default)]
12    pub title: Option<String>,
13    /// Human-readable explanation.
14    #[serde(default)]
15    pub detail: Option<String>,
16    /// HTTP status as a string, e.g. `"404"`.
17    #[serde(default)]
18    pub status: Option<String>,
19}
20
21impl ApiError {
22    fn message(&self) -> Option<String> {
23        match (&self.title, &self.detail) {
24            (Some(t), Some(d)) if t != d => Some(format!("{t}: {d}")),
25            (Some(t), _) => Some(t.clone()),
26            (_, Some(d)) => Some(d.clone()),
27            _ => None,
28        }
29    }
30}
31
32/// Everything that can go wrong talking to the HackerOne API.
33#[derive(Debug, thiserror::Error)]
34pub enum Error {
35    /// The request never produced an HTTP response (DNS, TLS, timeout, …).
36    #[error("transport error: {0}")]
37    Transport(String),
38
39    /// The response body was not the JSON we expected.
40    #[error("decode error: {0}")]
41    Decode(String),
42
43    /// The caller built an invalid request (bad input, serialization failure).
44    #[error("invalid request: {0}")]
45    Invalid(String),
46
47    /// The API returned a non-2xx status.
48    #[error("HackerOne API error (HTTP {status}): {detail}")]
49    Api {
50        /// HTTP status code.
51        status: u16,
52        /// Best-effort human-readable detail, flattened from `errors`.
53        detail: String,
54        /// The raw `errors` array, if the body carried one.
55        errors: Vec<ApiError>,
56    },
57}
58
59impl Error {
60    /// The HTTP status, if this was an `Api` error.
61    pub fn status(&self) -> Option<u16> {
62        match self {
63            Error::Api { status, .. } => Some(*status),
64            _ => None,
65        }
66    }
67
68    /// The structured `errors` array, if present.
69    pub fn api_errors(&self) -> &[ApiError] {
70        match self {
71            Error::Api { errors, .. } => errors,
72            _ => &[],
73        }
74    }
75}
76
77/// Convenience alias.
78pub type Result<T> = std::result::Result<T, Error>;
79
80/// Parse a non-2xx [`Response`](crate::Response) into [`Error::Api`].
81pub(crate) fn from_response(response: &crate::Response) -> Error {
82    let value = response.json().unwrap_or(serde_json::Value::Null);
83    let errors: Vec<ApiError> = value
84        .get("errors")
85        .cloned()
86        .and_then(|v| serde_json::from_value(v).ok())
87        .unwrap_or_default();
88
89    let detail = errors
90        .iter()
91        .filter_map(ApiError::message)
92        .collect::<Vec<_>>()
93        .join("; ");
94
95    let detail = if detail.is_empty() {
96        let text = response.text();
97        if text.trim().is_empty() {
98            "no response body".to_string()
99        } else {
100            text
101        }
102    } else {
103        detail
104    };
105
106    Error::Api {
107        status: response.status,
108        detail,
109        errors,
110    }
111}