Skip to main content

rig_core/client/
verify.rs

1use crate::{http_client, provider_response, wasm_compat::WasmCompatSend};
2use thiserror::Error;
3
4/// Errors from provider client verification.
5///
6/// Inspect provider failures with [`Self::provider_response_body`],
7/// [`Self::provider_response_json`], and [`Self::provider_response_status`].
8///
9/// Note: no provider path currently constructs [`Self::ProviderResponse`] for
10/// verification; real verify failures surface as [`Self::HttpError`], which
11/// the helpers read. The variant is kept for symmetry with the other capability
12/// errors and for future provider paths that preserve a 2xx error envelope.
13#[derive(Debug, Error)]
14pub enum VerifyError {
15    #[error("invalid authentication")]
16    InvalidAuthentication,
17    #[error("provider error: {0}")]
18    ProviderError(String),
19    /// Raw error response preserved from the provider
20    #[error("provider response error: {0}")]
21    ProviderResponse(provider_response::ProviderResponseError),
22    #[error("http error: {0}")]
23    HttpError(
24        #[from]
25        #[source]
26        http_client::Error,
27    ),
28}
29
30crate::provider_response::impl_provider_response_helpers!(VerifyError);
31
32/// A provider client that can verify the configuration.
33/// Clone is required for conversions between client types.
34pub trait VerifyClient {
35    /// Verify the configuration.
36    fn verify(&self) -> impl Future<Output = Result<(), VerifyError>> + WasmCompatSend;
37}
38
39#[cfg(test)]
40mod provider_response_tests {
41    use super::*;
42    use http::StatusCode;
43
44    #[test]
45    fn verify_error_provider_response_helpers_with_preserved_json_body() {
46        let body = r#"{"error":{"message":"rate limited"}}"#;
47        let error = VerifyError::ProviderResponse(
48            provider_response::ProviderResponseError::without_status(body.to_string()),
49        );
50
51        assert_eq!(error.provider_response_body(), Some(body));
52        assert_eq!(error.provider_response_status(), None);
53        assert_eq!(
54            error.provider_response_json().expect("valid JSON"),
55            Some(serde_json::json!({ "error": { "message": "rate limited" } }))
56        );
57    }
58
59    #[test]
60    fn verify_error_provider_response_helpers_with_http_non_success() {
61        let body = r#"{"error":{"message":"bad request"}}"#;
62        let error = VerifyError::HttpError(http_client::Error::InvalidStatusCodeWithMessage(
63            StatusCode::BAD_REQUEST,
64            body.to_string(),
65        ));
66
67        assert_eq!(error.provider_response_body(), Some(body));
68        assert_eq!(
69            error.provider_response_status(),
70            Some(StatusCode::BAD_REQUEST)
71        );
72        assert_eq!(
73            error.provider_response_json().expect("valid JSON"),
74            Some(serde_json::json!({ "error": { "message": "bad request" } }))
75        );
76    }
77
78    #[test]
79    fn verify_error_provider_response_helpers_with_preserved_plain_text_body() {
80        let error = VerifyError::ProviderResponse(
81            provider_response::ProviderResponseError::without_status("not json".to_string()),
82        );
83
84        assert_eq!(error.provider_response_body(), Some("not json"));
85        assert!(error.provider_response_json().is_err());
86    }
87
88    #[test]
89    fn verify_error_provider_error_is_not_a_provider_response() {
90        let error = VerifyError::ProviderError("internal diagnostic".to_string());
91
92        assert_eq!(error.provider_response_body(), None);
93        assert_eq!(error.provider_response_status(), None);
94        assert_eq!(error.provider_response_json().expect("no body"), None);
95    }
96
97    #[test]
98    fn verify_error_provider_response_helpers_with_unrelated_variant() {
99        let error = VerifyError::InvalidAuthentication;
100
101        assert_eq!(error.provider_response_body(), None);
102        assert_eq!(error.provider_response_status(), None);
103        assert_eq!(error.provider_response_json().expect("no body"), None);
104    }
105
106    #[tokio::test]
107    async fn verify_preserves_status_and_body_on_provider_error_response() {
108        use crate::client::VerifyClient;
109        use crate::providers::openai::Client;
110        use crate::test_utils::RecordingHttpClient;
111
112        let body = r#"{"error":{"message":"server exploded","type":"server_error"}}"#;
113        let http_client =
114            RecordingHttpClient::with_error_response(StatusCode::INTERNAL_SERVER_ERROR, body);
115        let client = Client::builder()
116            .api_key("test-key")
117            .http_client(http_client)
118            .build()
119            .expect("build client");
120
121        let error = client
122            .verify()
123            .await
124            .expect_err("verify should fail on a 500 response");
125
126        assert_eq!(
127            error.provider_response_status(),
128            Some(StatusCode::INTERNAL_SERVER_ERROR)
129        );
130        assert_eq!(error.provider_response_body(), Some(body));
131        let json = error
132            .provider_response_json()
133            .expect("raw body should be valid JSON")
134            .expect("parsed JSON should be present");
135        assert_eq!(json["error"]["type"], "server_error");
136    }
137
138    /// rig#2210: `verify` builds its own errors on three separate branches
139    /// (500, Anthropic's 529, and the generic non-success tail). Each must
140    /// preserve the failed response's headers, so a rejected verification can
141    /// still be retried on the server's schedule.
142    #[tokio::test]
143    async fn verify_preserves_response_headers_on_every_failing_branch() {
144        use crate::client::VerifyClient;
145        use crate::providers::openai::Client;
146        use crate::test_utils::RecordingHttpClient;
147
148        let body = r#"{"error":{"message":"slow down"}}"#;
149        for status in [
150            StatusCode::INTERNAL_SERVER_ERROR,
151            StatusCode::from_u16(529).expect("overloaded"),
152            StatusCode::TOO_MANY_REQUESTS,
153        ] {
154            let mut headers = http::HeaderMap::new();
155            headers.insert(http::header::RETRY_AFTER, "20".parse().expect("value"));
156            let http_client =
157                RecordingHttpClient::with_error_response_headers(status, body, headers);
158            let client = Client::builder()
159                .api_key("test-key")
160                .http_client(http_client)
161                .build()
162                .expect("build client");
163
164            let error = client
165                .verify()
166                .await
167                .expect_err("verify should fail on a non-success status");
168
169            assert_eq!(
170                error
171                    .provider_response_headers()
172                    .and_then(|headers| headers.get(http::header::RETRY_AFTER))
173                    .and_then(|value| value.to_str().ok()),
174                Some("20"),
175                "{status}: Retry-After not recoverable from a failed verify",
176            );
177            assert_eq!(error.provider_response_status(), Some(status));
178            assert_eq!(error.provider_response_body(), Some(body));
179        }
180    }
181}