1use std::time::Duration;
12
13use reqwest::{Client, Method, StatusCode};
14use serde_json::Value;
15
16use crate::error::{GotifyError, Result};
17use crate::GotifyConfig;
18
19pub fn build_client(cfg: &GotifyConfig) -> Result<Client> {
25 reqwest::ClientBuilder::new()
26 .timeout(cfg.request_timeout)
27 .build()
28 .map_err(GotifyError::ClientBuild)
29}
30
31#[tracing::instrument(skip(client, token, query, body), fields(url))]
53#[allow(clippy::too_many_arguments)]
54pub async fn request_json(
55 client: &Client,
56 base_url: &str,
57 token: Option<&str>,
58 action: &str,
59 method: Method,
60 path: &str,
61 query: Option<&[(&str, String)]>,
62 body: Option<&Value>,
63) -> Result<Value> {
64 let url = format!(
65 "{}/{}",
66 base_url.trim_end_matches('/'),
67 path.trim_start_matches('/')
68 );
69 tracing::Span::current().record("url", tracing::field::display(&url));
70 let result = send_request(client, &url, token, method, query, body).await;
71 match &result {
72 Ok(value) => {
73 let count = value
74 .get("messages")
75 .or(Some(value))
76 .and_then(Value::as_array)
77 .map(Vec::len);
78 tracing::debug!(action, count, "upstream call ok");
79 }
80 Err(error) => tracing::warn!(action, %error, "upstream call failed"),
81 }
82 result
83}
84
85async fn send_request(
86 client: &Client,
87 url: &str,
88 token: Option<&str>,
89 method: Method,
90 query: Option<&[(&str, String)]>,
91 body: Option<&Value>,
92) -> Result<Value> {
93 let url = url.to_string();
94 let mut request = client.request(method.clone(), &url);
95 if let Some(token) = token {
96 request = request.header("X-Gotify-Key", token);
97 }
98 if let Some(query) = query {
99 request = request.query(query);
100 }
101 if let Some(body) = body {
102 request = request.json(body);
103 }
104
105 let response = request
106 .send()
107 .await
108 .map_err(|source| map_transport_error(&method, &url, source))?;
109 let status = response.status();
110
111 if status.as_u16() == 204 {
113 return Ok(serde_json::json!({ "status": "ok" }));
114 }
115
116 match status {
117 StatusCode::UNAUTHORIZED => return Err(GotifyError::Unauthorized(url)),
118 StatusCode::NOT_FOUND => {
119 return Err(GotifyError::NotFound {
120 method: method.to_string(),
121 url,
122 })
123 }
124 StatusCode::TOO_MANY_REQUESTS => {
125 let retry_after = response
126 .headers()
127 .get(reqwest::header::RETRY_AFTER)
128 .and_then(|value| value.to_str().ok())
129 .and_then(|value| value.parse::<u64>().ok())
130 .map(Duration::from_secs);
131 return Err(GotifyError::RateLimited {
132 method: method.to_string(),
133 url,
134 retry_after,
135 });
136 }
137 _ => {}
138 }
139
140 let bytes = response
141 .bytes()
142 .await
143 .map_err(|source| map_transport_error(&method, &url, source))?;
144
145 if !status.is_success() {
152 let body = if bytes.is_empty() {
153 Value::Null
154 } else {
155 serde_json::from_slice::<Value>(&bytes)
156 .unwrap_or_else(|_| Value::String(String::from_utf8_lossy(&bytes).into_owned()))
157 };
158 return Err(GotifyError::UnexpectedStatus {
159 status: status.as_u16(),
160 url,
161 body: Box::new(body),
162 });
163 }
164
165 if bytes.is_empty() {
166 return Ok(serde_json::json!({ "status": "ok" }));
167 }
168 serde_json::from_slice::<Value>(&bytes).map_err(|source| GotifyError::Decode { url, source })
169}
170
171fn map_transport_error(method: &Method, url: &str, source: reqwest::Error) -> GotifyError {
172 if source.is_timeout() {
173 GotifyError::Timeout {
174 method: method.to_string(),
175 url: url.to_string(),
176 source,
177 }
178 } else if source.is_connect() {
179 GotifyError::Connect {
180 method: method.to_string(),
181 url: url.to_string(),
182 source,
183 }
184 } else {
185 GotifyError::Request {
186 method: method.to_string(),
187 url: url.to_string(),
188 source,
189 }
190 }
191}