Skip to main content

axum_security_oauth2/
error.rs

1use std::{error::Error as StdError, fmt};
2
3use serde::Deserialize;
4use serde_json::{Map, Value};
5
6/// Errors returned by [`OAuth2Client`](crate::OAuth2Client) calls.
7///
8/// Configuration problems (missing endpoints, no HTTP backend) are caught
9/// by [`try_build`](crate::OAuth2ClientBuilder::try_build) as
10/// [`ConfigError`](crate::ConfigError); everything here can only happen at
11/// request time.
12#[derive(Debug)]
13#[non_exhaustive]
14pub enum Error {
15    /// The HTTP request itself failed (connect, TLS, timeout, ...).
16    Http(HttpError),
17    /// The server answered with a well-formed OAuth2 error body
18    /// (RFC 6749 §5.2).
19    Server(ServerError),
20    /// The server response could not be parsed.
21    Parse(ParseError),
22}
23
24impl fmt::Display for Error {
25    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26        match self {
27            Error::Http(error) => error.fmt(f),
28            Error::Server(error) => error.fmt(f),
29            Error::Parse(error) => error.fmt(f),
30        }
31    }
32}
33
34impl StdError for Error {
35    fn source(&self) -> Option<&(dyn StdError + 'static)> {
36        match self {
37            Error::Http(error) => Some(error),
38            Error::Server(error) => Some(error),
39            Error::Parse(error) => Some(error),
40        }
41    }
42}
43
44/// A transport-level failure; [`source`](StdError::source) returns the
45/// backend's error.
46#[derive(Debug)]
47pub struct HttpError(pub(crate) HttpErrorKind);
48
49#[derive(Debug)]
50pub(crate) enum HttpErrorKind {
51    #[cfg(feature = "reqwest")]
52    Reqwest(reqwest::Error),
53}
54
55impl fmt::Display for HttpError {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        let _ = f;
58        match self.0 {
59            #[cfg(feature = "reqwest")]
60            HttpErrorKind::Reqwest(ref error) => write!(f, "http request failed: {error}"),
61        }
62    }
63}
64
65impl StdError for HttpError {
66    fn source(&self) -> Option<&(dyn StdError + 'static)> {
67        match self.0 {
68            #[cfg(feature = "reqwest")]
69            HttpErrorKind::Reqwest(ref error) => Some(error),
70        }
71    }
72}
73
74/// A well-formed OAuth2 error body from the server (RFC 6749 §5.2), plus
75/// the HTTP status it arrived with.
76#[derive(Debug)]
77pub struct ServerError {
78    code: ErrorCode,
79    description: Option<String>,
80    uri: Option<String>,
81    status: u16,
82    extra: Map<String, Value>,
83}
84
85impl ServerError {
86    /// The `error` code from the response body.
87    pub fn code(&self) -> &ErrorCode {
88        &self.code
89    }
90
91    /// The optional human-readable `error_description`.
92    pub fn description(&self) -> Option<&str> {
93        self.description.as_deref()
94    }
95
96    /// The optional `error_uri` pointing at documentation.
97    pub fn uri(&self) -> Option<&str> {
98        self.uri.as_deref()
99    }
100
101    /// The HTTP status code of the response.
102    pub fn status(&self) -> u16 {
103        self.status
104    }
105
106    /// Deserializes a single unrecognized field of the error body.
107    pub fn extra_field<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
108        let value = self.extra.get(key)?;
109        serde_json::from_value(value.clone()).ok()
110    }
111}
112
113impl fmt::Display for ServerError {
114    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115        write!(
116            f,
117            "server returned error \"{}\" (status {})",
118            self.code, self.status
119        )?;
120        if let Some(description) = &self.description {
121            write!(f, ": {description}")?;
122        }
123        Ok(())
124    }
125}
126
127impl StdError for ServerError {}
128
129/// The `error` code of an RFC 6749 §5.2 error body.
130#[derive(Debug, Clone, PartialEq, Eq)]
131#[non_exhaustive]
132pub enum ErrorCode {
133    InvalidRequest,
134    InvalidClient,
135    InvalidGrant,
136    UnauthorizedClient,
137    UnsupportedGrantType,
138    InvalidScope,
139    /// An extension / nonstandard error code.
140    Other(String),
141}
142
143impl ErrorCode {
144    /// The error code as it appeared on the wire.
145    pub fn as_str(&self) -> &str {
146        match self {
147            ErrorCode::InvalidRequest => "invalid_request",
148            ErrorCode::InvalidClient => "invalid_client",
149            ErrorCode::InvalidGrant => "invalid_grant",
150            ErrorCode::UnauthorizedClient => "unauthorized_client",
151            ErrorCode::UnsupportedGrantType => "unsupported_grant_type",
152            ErrorCode::InvalidScope => "invalid_scope",
153            ErrorCode::Other(code) => code,
154        }
155    }
156
157    fn from_wire(code: String) -> Self {
158        match code.as_str() {
159            "invalid_request" => ErrorCode::InvalidRequest,
160            "invalid_client" => ErrorCode::InvalidClient,
161            "invalid_grant" => ErrorCode::InvalidGrant,
162            "unauthorized_client" => ErrorCode::UnauthorizedClient,
163            "unsupported_grant_type" => ErrorCode::UnsupportedGrantType,
164            "invalid_scope" => ErrorCode::InvalidScope,
165            _ => ErrorCode::Other(code),
166        }
167    }
168}
169
170impl fmt::Display for ErrorCode {
171    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172        f.write_str(self.as_str())
173    }
174}
175
176/// The wire shape of an RFC 6749 §5.2 error body.
177#[derive(Deserialize)]
178pub(crate) struct ServerErrorWire {
179    error: String,
180    #[serde(default)]
181    error_description: Option<String>,
182    #[serde(default)]
183    error_uri: Option<String>,
184    #[serde(flatten)]
185    extra: Map<String, Value>,
186}
187
188impl ServerErrorWire {
189    pub(crate) fn into_server_error(self, status: u16) -> ServerError {
190        ServerError {
191            code: ErrorCode::from_wire(self.error),
192            description: self.error_description,
193            uri: self.error_uri,
194            status,
195            extra: self.extra,
196        }
197    }
198}
199
200/// A response body that could not be parsed.
201///
202/// The raw body is retained for diagnostics but deliberately kept out of
203/// `Debug` and `Display` — a failed *success* parse can contain live
204/// tokens. Reading it requires the explicit [`body`](ParseError::body)
205/// accessor.
206pub struct ParseError {
207    pub(crate) url: String,
208    pub(crate) status: u16,
209    pub(crate) content_type: Option<String>,
210    pub(crate) body: Vec<u8>,
211    pub(crate) source: serde_json::Error,
212}
213
214impl ParseError {
215    /// The endpoint URL the response came from.
216    pub fn url(&self) -> &str {
217        &self.url
218    }
219
220    /// The HTTP status code of the response.
221    pub fn status(&self) -> u16 {
222        self.status
223    }
224
225    /// The `Content-Type` of the response, if any.
226    pub fn content_type(&self) -> Option<&str> {
227        self.content_type.as_deref()
228    }
229
230    /// The raw response body. May contain secrets — handle with care.
231    pub fn body(&self) -> &[u8] {
232        &self.body
233    }
234}
235
236impl fmt::Debug for ParseError {
237    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
238        f.debug_struct("ParseError")
239            .field("url", &self.url)
240            .field("status", &self.status)
241            .field("content_type", &self.content_type)
242            .field(
243                "body",
244                &format_args!("[redacted, {} bytes]", self.body.len()),
245            )
246            .field("source", &self.source)
247            .finish()
248    }
249}
250
251impl fmt::Display for ParseError {
252    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
253        write!(
254            f,
255            "could not parse response from \"{}\" (status {}, content-type {}, {} bytes): {}",
256            self.url,
257            self.status,
258            self.content_type.as_deref().unwrap_or("unknown"),
259            self.body.len(),
260            self.source
261        )
262    }
263}
264
265impl StdError for ParseError {
266    fn source(&self) -> Option<&(dyn StdError + 'static)> {
267        Some(&self.source)
268    }
269}