Skip to main content

phemios_core/
network_request.rs

1//! Supported JSON transport and cancellable idempotent retry admission.
2use super::*;
3impl Client {
4    /// Explicit supported JSON operations; never accept a second authority or arbitrary URL.
5    pub fn request(
6        &self,
7        method: &str,
8        path: &str,
9        query: &[(&str, &str)],
10        body: Option<&Value>,
11    ) -> Result<Value, String> {
12        self.request_admitted(method, path, query, body, || Ok(()))
13    }
14    /// Check admission before/after every HTTP attempt; only GET may retry busy responses.
15    /// Mutation bodies, bounded diagnostics and the original authority remain unchanged.
16    pub fn request_admitted(
17        &self,
18        method: &str,
19        path: &str,
20        query: &[(&str, &str)],
21        body: Option<&Value>,
22        current: impl Fn() -> Result<(), String>,
23    ) -> Result<Value, String> {
24        current()?;
25        if !allowed(method, path) {
26            return Err("Opération native non prise en charge.".into());
27        }
28        let url = format!("{}{}", self.server, path);
29        let response = if method == "GET" {
30            read_response(
31                || {
32                    let mut request = self
33                        .agent
34                        .get(&url)
35                        .header("Authorization", format!("Bearer {}", self.token));
36                    for (key, value) in query {
37                        request = request.query(key, value);
38                    }
39                    request
40                        .call()
41                        .map_err(|_| "Impossible de joindre Aède.".to_string())
42                },
43                &current,
44            )
45        } else if method == "DELETE" {
46            let mut request = self
47                .agent
48                .delete(&url)
49                .header("Authorization", format!("Bearer {}", self.token));
50            for (key, value) in query {
51                request = request.query(key, value);
52            }
53            (if path == "/api/me/v1/history" {
54                let data = body.unwrap_or(&Value::Null).to_string();
55                if data.len() > 1024 {
56                    return Err("Confirmation trop grande.".into());
57                }
58                request
59                    .force_send_body()
60                    .header("Content-Type", "application/json")
61                    .send(data.as_bytes())
62            } else {
63                request.call()
64            })
65            .map_err(|_| "Impossible de joindre Aède.".to_string())
66        } else {
67            // Absence is an empty HTTP body, never JSON null (notably task Stop).
68            let data = body.map(Value::to_string).unwrap_or_default();
69            if data.len() > 16 * 1024 {
70                return Err("Requête trop grande.".into());
71            }
72            let mut request = if method == "PATCH" {
73                self.agent.patch(&url)
74            } else if method == "PUT" {
75                self.agent.put(&url)
76            } else {
77                self.agent.post(&url)
78            };
79            request = request
80                .header("Authorization", format!("Bearer {}", self.token))
81                .header("Content-Type", "application/json");
82            for (key, value) in query {
83                request = request.query(key, value);
84            }
85            request
86                .send(data.as_bytes())
87                .map_err(|_| "Impossible de joindre Aède.".to_string())
88        }?;
89        current()?;
90        if !response.status().is_success() {
91            let status = response.status().as_u16();
92            let mut response = response;
93            let detail = response
94                .body_mut()
95                .with_config()
96                .limit(8192)
97                .read_to_vec()
98                .ok()
99                .and_then(|bytes| serde_json::from_slice(&bytes).ok());
100            current()?;
101            return Err(errors::request_failure(
102                status,
103                method,
104                path,
105                body.is_some_and(|v| v["create_only"] == true),
106                detail.as_ref(),
107            ));
108        }
109        let result = decode(&bytes(response, JSON_LIMIT)?);
110        current()?;
111        result
112    }
113}
114
115/// Keep retry admission generic so artwork can retain its own transport diagnostics.
116pub(super) fn read_response<E>(
117    call: impl Fn() -> Result<ureq::http::Response<ureq::Body>, E>,
118    current: impl Fn() -> Result<(), E>,
119) -> Result<ureq::http::Response<ureq::Body>, E> {
120    for attempt in 0..4 {
121        current()?;
122        let response = call();
123        current()?;
124        let response = response?;
125        if !matches!(response.status().as_u16(), 409 | 429 | 503) {
126            return Ok(response);
127        }
128        drop(response);
129        std::thread::sleep(Duration::from_millis(150 << attempt));
130    }
131    current()?;
132    let response = call();
133    current()?;
134    response
135}
136
137#[cfg(test)]
138#[path = "network_request_test_support.rs"]
139mod test_support;
140#[cfg(test)]
141#[path = "network_request_tests.rs"]
142mod tests;