oanda_rs/error.rs
1//! Error types returned by the SDK.
2
3use reqwest::StatusCode;
4use serde::de::DeserializeOwned;
5use serde::{Deserialize, Serialize};
6
7/// The unified error type returned by every SDK operation.
8#[derive(Debug, thiserror::Error)]
9#[non_exhaustive]
10pub enum Error {
11 /// The HTTP request could not be performed (connection, TLS, timeout, ...).
12 #[error("transport error: {0}")]
13 Transport(#[from] reqwest::Error),
14
15 /// OANDA answered with a non-success HTTP status.
16 ///
17 /// The parsed error body is available in [`Error::Api::body`]; the raw
18 /// payload of order-reject responses can be recovered with
19 /// [`ApiErrorBody::details`].
20 #[error("OANDA API error (HTTP {status}): {}", body.error_message)]
21 Api {
22 /// HTTP status code of the response.
23 status: StatusCode,
24 /// Value of the `RequestID` response header, when present.
25 request_id: Option<String>,
26 /// Parsed error body.
27 body: ApiErrorBody,
28 },
29
30 /// A 2xx response body could not be deserialized into the expected type.
31 #[error("failed to decode response body: {source}")]
32 Decode {
33 /// The underlying deserialization error.
34 source: serde_json::Error,
35 /// The raw response body, kept verbatim for debugging.
36 body: String,
37 },
38
39 /// A streaming connection violated the expected protocol.
40 #[error("stream protocol error: {0}")]
41 Stream(String),
42
43 /// The client was configured with invalid values.
44 #[error("invalid client configuration: {0}")]
45 Config(String),
46}
47
48impl Error {
49 /// The HTTP status code associated with this error, if any.
50 pub fn status(&self) -> Option<StatusCode> {
51 match self {
52 Error::Api { status, .. } => Some(*status),
53 Error::Transport(e) => e.status(),
54 _ => None,
55 }
56 }
57
58 /// The `RequestID` header OANDA attached to the failing response, if any.
59 pub fn request_id(&self) -> Option<&str> {
60 match self {
61 Error::Api { request_id, .. } => request_id.as_deref(),
62 _ => None,
63 }
64 }
65
66 /// Whether this error was caused by OANDA's rate limiting (HTTP 429).
67 ///
68 /// OANDA allows 120 REST requests per second and 2 new connections per
69 /// second per IP address. The client's built-in rate limiter (see
70 /// [`ClientBuilder`](crate::ClientBuilder)) keeps you below those limits
71 /// by default, so this should only occur when rate limiting was disabled
72 /// or other processes share the same IP.
73 pub fn is_rate_limited(&self) -> bool {
74 self.status() == Some(StatusCode::TOO_MANY_REQUESTS)
75 }
76}
77
78/// The JSON body OANDA returns for error responses.
79///
80/// All error responses carry `errorMessage`; some also carry `errorCode`
81/// and/or `rejectReason`. Order-related rejections include additional
82/// top-level fields (e.g. `orderRejectTransaction`, `relatedTransactionIDs`,
83/// `lastTransactionID`), which are preserved in [`ApiErrorBody::extra`] and
84/// can be decoded into a typed struct with [`ApiErrorBody::details`].
85#[derive(Debug, Clone, Serialize, Deserialize)]
86#[non_exhaustive]
87pub struct ApiErrorBody {
88 /// Human-readable description of the error.
89 #[serde(rename = "errorMessage")]
90 pub error_message: String,
91 /// Machine-readable error code, when provided.
92 #[serde(rename = "errorCode", skip_serializing_if = "Option::is_none")]
93 pub error_code: Option<String>,
94 /// The reason the request was rejected, when provided.
95 #[serde(rename = "rejectReason", skip_serializing_if = "Option::is_none")]
96 pub reject_reason: Option<String>,
97 /// Any additional top-level fields of the error body.
98 #[serde(flatten)]
99 pub extra: serde_json::Map<String, serde_json::Value>,
100}
101
102impl ApiErrorBody {
103 /// Constructs an error body from a plain-text (non-JSON) payload.
104 pub(crate) fn from_text(text: String) -> Self {
105 ApiErrorBody {
106 error_message: text,
107 error_code: None,
108 reject_reason: None,
109 extra: serde_json::Map::new(),
110 }
111 }
112
113 /// Attempts to decode the full error body into a typed view.
114 ///
115 /// Useful for order endpoints, whose 400/404 responses carry reject
116 /// transactions, e.g.
117 /// [`CreateOrderRejectBody`](crate::endpoints::orders::CreateOrderRejectBody).
118 pub fn details<T: DeserializeOwned>(&self) -> Option<T> {
119 serde_json::to_value(self)
120 .ok()
121 .and_then(|v| serde_json::from_value(v).ok())
122 }
123}