1pub(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#[derive(Debug, Clone, Default)]
14pub(crate) struct ErrorEnvelope {
15 pub code: Option<String>,
16 pub message: Option<String>,
17 pub detail: Option<Value>,
19}
20
21fn 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 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
57pub(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 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
79pub(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
91pub(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
133pub(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
150pub(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 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}