rig_core/client/
verify.rs1use crate::{http_client, provider_response, wasm_compat::WasmCompatSend};
2use thiserror::Error;
3
4#[derive(Debug, Error)]
14pub enum VerifyError {
15 #[error("invalid authentication")]
16 InvalidAuthentication,
17 #[error("provider error: {0}")]
18 ProviderError(String),
19 #[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
32pub trait VerifyClient {
35 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 #[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}