smos_application/errors/upstream_error.rs
1//! Upstream (LLM proxy) errors.
2//!
3//! Returned by the OpenAI-compatible HTTP upstream adapter. The shape mirrors
4//! a typical HTTP client error surface: connection, timeout, status code +
5//! body, stream error during SSE, and (de)serialisation issues.
6
7use std::time::Duration;
8use thiserror::Error;
9
10/// Errors returned by the LLM upstream adapter.
11#[derive(Debug, Error)]
12pub enum UpstreamError {
13 #[error("upstream connect failed: {0}")]
14 ConnectFailed(String),
15
16 #[error("upstream timeout after {0:?}")]
17 Timeout(Duration),
18
19 #[error("upstream returned {status}: {body}")]
20 StatusError { status: u16, body: String },
21
22 #[error("upstream stream error: {0}")]
23 StreamError(String),
24
25 /// Upstream returned a 2xx body that the adapter could not parse (non-JSON
26 /// or malformed). Distinct from `SerializationError` (which is an SMOS-side
27 /// request-encoding bug) so the HTTP layer can map it to 502 (upstream's
28 /// fault) rather than 500 (our fault).
29 #[error("upstream returned an unparseable body: {0}")]
30 BadResponse(String),
31
32 #[error("upstream serialization error: {0}")]
33 SerializationError(String),
34
35 /// Returned by `ReqwestUpstreamRouter::complete` when the named
36 /// provider is missing from the `[[providers]]` map. Should be
37 /// unreachable in practice (the routing layer validates the
38 /// person → provider reference at startup), but is retained for
39 /// defensive depth: a hot config edit could otherwise produce a
40 /// confusing 404-from-the-upstream instead of a clear 502.
41 ///
42 /// Historically this variant was constructed by the round-robin /
43 /// failover pool's failover path when every configured provider
44 /// failed; the new per-person router does not exercise that path,
45 /// so the variant is now produced only by the unknown-provider
46 /// branch above. Kept (rather than removed) because the HTTP error
47 /// mapper still classifies it as 502 and the type is part of the
48 /// public API surface (`pub enum`).
49 #[error("all upstream providers failed; last error: {0}")]
50 AllProvidersFailed(String),
51}
52
53#[cfg(test)]
54mod tests {
55 use super::*;
56
57 #[test]
58 fn status_error_display_shows_code_and_body() {
59 let e = UpstreamError::StatusError {
60 status: 503,
61 body: "service unavailable".into(),
62 };
63 let msg = e.to_string();
64 assert!(msg.contains("503"));
65 assert!(msg.contains("service unavailable"));
66 }
67
68 #[test]
69 fn timeout_display_uses_debug_format() {
70 let e = UpstreamError::Timeout(Duration::from_secs(5));
71 assert!(e.to_string().contains("5s"));
72 }
73}