Skip to main content

sie_sdk/wire/
mod.rs

1//! Server error envelopes: decoding them, and mapping status + code onto [`Error`].
2
3pub(crate) mod msg;
4pub mod ndarray;
5pub mod sse;
6
7use serde_json::Value;
8
9use crate::error::{Error, ModelLoadErrorClass, Result, codes};
10use crate::http::{HttpResponse, headers, metadata};
11
12/// The `{code, message}` pair carried by an error response.
13#[derive(Debug, Clone, Default)]
14pub(crate) struct ErrorEnvelope {
15    pub code: Option<String>,
16    pub message: Option<String>,
17    /// The nested error object itself, for the fields only some errors carry.
18    pub detail: Option<Value>,
19}
20
21/// The gateway spells `PROVISIONING` in lower case on one legacy path.
22fn normalize_error_code(code: Option<String>) -> Option<String> {
23    match code.as_deref() {
24        Some("provisioning") => Some(codes::PROVISIONING.to_string()),
25        _ => code,
26    }
27}
28
29fn envelope_from_value(data: &Value) -> ErrorEnvelope {
30    // `error` first, then `detail`; either may be an object or a bare string.
31    for key in ["error", "detail"] {
32        let Some(node) = data.get(key) else { continue };
33        return match node {
34            Value::Object(_) => ErrorEnvelope {
35                code: node.get("code").and_then(Value::as_str).map(str::to_string),
36                message: node
37                    .get("message")
38                    .and_then(Value::as_str)
39                    .map(str::to_string),
40                detail: Some(node.clone()),
41            },
42            Value::String(text) => ErrorEnvelope {
43                code: None,
44                message: Some(text.clone()),
45                detail: None,
46            },
47            other => ErrorEnvelope {
48                code: None,
49                message: Some(other.to_string()),
50                detail: None,
51            },
52        };
53    }
54    ErrorEnvelope::default()
55}
56
57/// Decode an error response into its envelope, falling back to raw text.
58pub(crate) fn parse_envelope(response: &HttpResponse) -> ErrorEnvelope {
59    let mut envelope = match response.decode_value() {
60        Some(data) => envelope_from_value(&data),
61        None => ErrorEnvelope {
62            message: Some(response.text()).filter(|text| !text.is_empty()),
63            ..Default::default()
64        },
65    };
66
67    // The header is authoritative: it survives a body the SDK could not decode.
68    if let Some(header_code) = response
69        .header(headers::ERROR_CODE)
70        .filter(|code| !code.is_empty())
71    {
72        envelope.code = Some(header_code.to_string());
73    } else {
74        envelope.code = normalize_error_code(envelope.code);
75    }
76    envelope
77}
78
79/// The error code for a response, header first.
80pub(crate) fn error_code(response: &HttpResponse) -> Option<String> {
81    parse_envelope(response).code
82}
83
84fn message_or_status(envelope: &ErrorEnvelope, status: u16) -> String {
85    envelope
86        .message
87        .clone()
88        .unwrap_or_else(|| format!("HTTP {status}"))
89}
90
91/// A 502 `MODEL_LOAD_FAILED` is terminal and must be surfaced before any retry budget.
92pub(crate) fn check_model_load_failed(
93    response: &HttpResponse,
94    model: Option<&str>,
95    retries: u32,
96) -> Result<()> {
97    if response.status != 502 {
98        return Ok(());
99    }
100    let envelope = parse_envelope(response);
101    if envelope.code.as_deref() != Some(codes::MODEL_LOAD_FAILED) {
102        return Ok(());
103    }
104    let detail = envelope.detail.clone().unwrap_or(Value::Null);
105    let attempts = detail
106        .get("attempts")
107        .and_then(|value| {
108            value
109                .as_u64()
110                .or_else(|| value.as_f64().map(|f| f as u64))
111                .or_else(|| value.as_str().and_then(|s| s.parse().ok()))
112        })
113        .unwrap_or(1)
114        .max(1);
115
116    Err(Error::ModelLoadFailed {
117        message: message_or_status(&envelope, response.status),
118        model: detail
119            .get("model")
120            .and_then(Value::as_str)
121            .map(str::to_string)
122            .or_else(|| model.map(str::to_string)),
123        error_class: ModelLoadErrorClass::parse(detail.get("error_class").and_then(Value::as_str)),
124        permanent: detail
125            .get("permanent")
126            .and_then(Value::as_bool)
127            .unwrap_or(true),
128        attempts: attempts.min(u64::from(u32::MAX)) as u32,
129        request: metadata::parse(&response.headers, None, retries).map(Box::new),
130    })
131}
132
133/// `/v1/estimate` reports an unroutable identity as a 503 with a capacity code.
134pub(crate) fn check_estimate_unroutable(response: &HttpResponse, retries: u32) -> Result<()> {
135    if response.status != 503 {
136        return Ok(());
137    }
138    let envelope = parse_envelope(response);
139    let code = envelope.code.as_deref().unwrap_or_default();
140    if code != codes::QUEUE_UNAVAILABLE && code != codes::PROVISIONING {
141        return Ok(());
142    }
143    Err(Error::EstimateUnroutable {
144        message: message_or_status(&envelope, response.status),
145        code: envelope.code,
146        request: metadata::parse(&response.headers, None, retries).map(Box::new),
147    })
148}
149
150/// Terminal dispatcher: turn any `>= 400` response into the right [`Error`].
151pub(crate) fn handle_error(response: &HttpResponse, model: Option<&str>, retries: u32) -> Error {
152    let envelope = parse_envelope(response);
153    let message = message_or_status(&envelope, response.status);
154    let request = metadata::parse(&response.headers, None, retries).map(Box::new);
155    let status = response.status;
156    let code = envelope.code.as_deref();
157
158    if status == 503 && code == Some(codes::PROVISIONING) {
159        return Error::Provisioning {
160            message,
161            gpu: None,
162            retry_after: crate::retry::backoff::retry_after(&response.headers),
163        };
164    }
165    if status == 400 && code == Some(codes::INPUT_TOO_LONG) {
166        return Error::InputTooLong {
167            message,
168            model: model.map(str::to_string),
169            request,
170        };
171    }
172    if status >= 500 {
173        return Error::Server {
174            message,
175            code: envelope.code,
176            status,
177            request,
178        };
179    }
180    Error::Request {
181        message,
182        code: envelope.code,
183        status,
184        request,
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use bytes::Bytes;
192    use reqwest::header::HeaderMap;
193
194    fn response(
195        status: u16,
196        body: &str,
197        content_type: &str,
198        extra: &[(&str, &str)],
199    ) -> HttpResponse {
200        let mut headers = HeaderMap::new();
201        headers.insert(reqwest::header::CONTENT_TYPE, content_type.parse().unwrap());
202        for (name, value) in extra {
203            headers.insert(
204                reqwest::header::HeaderName::from_bytes(name.as_bytes()).unwrap(),
205                value.parse().unwrap(),
206            );
207        }
208        HttpResponse {
209            status,
210            headers,
211            body: Bytes::copy_from_slice(body.as_bytes()),
212        }
213    }
214
215    #[test]
216    fn parses_error_and_detail_objects() {
217        let envelope = parse_envelope(&response(
218            500,
219            r#"{"error": {"code": "INTERNAL_ERROR", "message": "boom"}}"#,
220            "application/json",
221            &[],
222        ));
223        assert_eq!(envelope.code.as_deref(), Some("INTERNAL_ERROR"));
224        assert_eq!(envelope.message.as_deref(), Some("boom"));
225
226        let envelope = parse_envelope(&response(
227            400,
228            r#"{"detail": {"code": "BAD", "message": "nope"}}"#,
229            "application/json",
230            &[],
231        ));
232        assert_eq!(envelope.code.as_deref(), Some("BAD"));
233        assert_eq!(envelope.message.as_deref(), Some("nope"));
234    }
235
236    #[test]
237    fn bare_string_error_becomes_the_message() {
238        let envelope = parse_envelope(&response(
239            400,
240            r#"{"detail": "plain text"}"#,
241            "application/json",
242            &[],
243        ));
244        assert!(envelope.code.is_none());
245        assert_eq!(envelope.message.as_deref(), Some("plain text"));
246    }
247
248    #[test]
249    fn undecodable_body_falls_back_to_text() {
250        let envelope = parse_envelope(&response(502, "<html>gateway</html>", "text/html", &[]));
251        assert_eq!(envelope.message.as_deref(), Some("<html>gateway</html>"));
252    }
253
254    #[test]
255    fn header_code_beats_body_code() {
256        let envelope = parse_envelope(&response(
257            503,
258            r#"{"error": {"code": "SOMETHING_ELSE", "message": "m"}}"#,
259            "application/json",
260            &[(headers::ERROR_CODE, codes::RESOURCE_EXHAUSTED)],
261        ));
262        assert_eq!(envelope.code.as_deref(), Some(codes::RESOURCE_EXHAUSTED));
263    }
264
265    #[test]
266    fn lowercase_provisioning_is_normalized() {
267        let envelope = parse_envelope(&response(
268            503,
269            r#"{"error": {"code": "provisioning", "message": "m"}}"#,
270            "application/json",
271            &[],
272        ));
273        assert_eq!(envelope.code.as_deref(), Some(codes::PROVISIONING));
274    }
275
276    #[test]
277    fn status_dispatch_matches_python() {
278        let too_long = handle_error(
279            &response(
280                400,
281                r#"{"error": {"code": "INPUT_TOO_LONG", "message": "m"}}"#,
282                "application/json",
283                &[],
284            ),
285            Some("bge"),
286            0,
287        );
288        assert!(matches!(too_long, Error::InputTooLong { .. }));
289
290        let server = handle_error(&response(500, "{}", "application/json", &[]), None, 0);
291        assert!(matches!(server, Error::Server { status: 500, .. }));
292
293        let request = handle_error(&response(404, "{}", "application/json", &[]), None, 0);
294        assert!(matches!(request, Error::Request { status: 404, .. }));
295        assert_eq!(request.to_string(), "HTTP 404");
296    }
297
298    #[test]
299    fn model_load_failed_is_terminal_with_detail() {
300        let err = check_model_load_failed(
301            &response(
302                502,
303                r#"{"error": {"code": "MODEL_LOAD_FAILED", "message": "gated", "error_class": "GATED",
304                    "permanent": true, "attempts": "3", "model": "org/m"}}"#,
305                "application/json",
306                &[],
307            ),
308            None,
309            0,
310        )
311        .unwrap_err();
312        match err {
313            Error::ModelLoadFailed {
314                error_class,
315                permanent,
316                attempts,
317                model,
318                ..
319            } => {
320                assert_eq!(error_class, ModelLoadErrorClass::Gated);
321                assert!(permanent);
322                assert_eq!(attempts, 3);
323                assert_eq!(model.as_deref(), Some("org/m"));
324            }
325            other => panic!("unexpected: {other:?}"),
326        }
327
328        // A 502 with a different code is not this error.
329        assert!(
330            check_model_load_failed(&response(502, "{}", "application/json", &[]), None, 0).is_ok()
331        );
332    }
333
334    #[test]
335    fn msgpack_error_bodies_decode() {
336        let body = rmp_serde::to_vec_named(&serde_json::json!({
337            "error": {"code": "INTERNAL_ERROR", "message": "packed"}
338        }))
339        .unwrap();
340        let mut headers = HeaderMap::new();
341        headers.insert(
342            reqwest::header::CONTENT_TYPE,
343            "application/msgpack".parse().unwrap(),
344        );
345        let response = HttpResponse {
346            status: 500,
347            headers,
348            body: Bytes::from(body),
349        };
350        let envelope = parse_envelope(&response);
351        assert_eq!(envelope.message.as_deref(), Some("packed"));
352        assert_eq!(envelope.code.as_deref(), Some("INTERNAL_ERROR"));
353    }
354}