mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-02 02:13:49 +00:00
Add lithos-llm as a pinned git dependency and replace the in-repo `test/twin/openai` crate with the published `twins` revision that lithos-llm verifies its codecs against. Move the Fabro policy overlay (`fabro-policy.toml`) into fabro-llm so Fabro owns its own catalog policy layer. Drop the twin-openai nextest overrides and CI package filter now that the crate is no longer a workspace member. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
254 lines
7.5 KiB
Rust
254 lines
7.5 KiB
Rust
#![allow(
|
|
clippy::absolute_paths,
|
|
clippy::disallowed_methods,
|
|
clippy::disallowed_types,
|
|
reason = "This crate intentionally re-exports reqwest types behind the approved facade."
|
|
)]
|
|
|
|
use std::path::Path;
|
|
use std::time::Duration;
|
|
|
|
use fabro_static::EnvVars;
|
|
pub use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
|
|
pub use reqwest::{
|
|
Body, Method, RequestBuilder, Response, StatusCode, Url, header, multipart, tls,
|
|
};
|
|
|
|
pub type HttpClient = reqwest::Client;
|
|
pub type HttpError = reqwest::Error;
|
|
pub type BlockingHttpClient = reqwest::blocking::Client;
|
|
pub type BlockingRequestBuilder = reqwest::blocking::RequestBuilder;
|
|
pub type BlockingResponse = reqwest::blocking::Response;
|
|
pub type Proxy = reqwest::Proxy;
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum ProxyPolicy {
|
|
System,
|
|
Disabled,
|
|
}
|
|
|
|
impl ProxyPolicy {
|
|
fn parse(value: &str) -> Result<Self, HttpClientBuildError> {
|
|
match value.to_ascii_lowercase().as_str() {
|
|
"system" => Ok(Self::System),
|
|
"disabled" => Ok(Self::Disabled),
|
|
_ => Err(HttpClientBuildError::InvalidProxyPolicy(value.to_string())),
|
|
}
|
|
}
|
|
|
|
fn resolve_with_env_value(
|
|
explicit: Option<Self>,
|
|
env_value: Option<&str>,
|
|
) -> Result<Self, HttpClientBuildError> {
|
|
if let Some(policy) = explicit {
|
|
return Ok(policy);
|
|
}
|
|
|
|
match env_value {
|
|
Some(value) => Self::parse(value),
|
|
None => Ok(Self::System),
|
|
}
|
|
}
|
|
|
|
fn resolve(explicit: Option<Self>) -> Result<Self, HttpClientBuildError> {
|
|
match std::env::var(EnvVars::FABRO_HTTP_PROXY_POLICY) {
|
|
Ok(value) => Self::resolve_with_env_value(explicit, Some(&value)),
|
|
Err(std::env::VarError::NotPresent) => Self::resolve_with_env_value(explicit, None),
|
|
Err(std::env::VarError::NotUnicode(value)) => Err(
|
|
HttpClientBuildError::InvalidProxyPolicy(value.to_string_lossy().into_owned()),
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum HttpClientBuildError {
|
|
#[error("invalid {env} value `{0}`; expected `system` or `disabled`", env = EnvVars::FABRO_HTTP_PROXY_POLICY)]
|
|
InvalidProxyPolicy(String),
|
|
|
|
#[error(transparent)]
|
|
Reqwest(#[from] reqwest::Error),
|
|
}
|
|
|
|
/// Generates a proxy-policy-aware builder that wraps a reqwest client builder.
|
|
macro_rules! define_builder {
|
|
($builder_name:ident, $inner_builder:ty, $inner_new:expr, $client_type:ty) => {
|
|
#[derive(Default)]
|
|
pub struct $builder_name {
|
|
inner: $inner_builder,
|
|
proxy_policy: Option<ProxyPolicy>,
|
|
}
|
|
|
|
impl $builder_name {
|
|
#[must_use]
|
|
pub fn new() -> Self {
|
|
Self {
|
|
inner: $inner_new,
|
|
proxy_policy: None,
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn proxy_policy(mut self, proxy_policy: ProxyPolicy) -> Self {
|
|
self.proxy_policy = Some(proxy_policy);
|
|
self
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn no_proxy(mut self) -> Self {
|
|
self.inner = self.inner.no_proxy();
|
|
self
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn proxy(mut self, proxy: Proxy) -> Self {
|
|
self.inner = self.inner.proxy(proxy);
|
|
self
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn user_agent(mut self, value: impl Into<String>) -> Self {
|
|
self.inner = self.inner.user_agent(value.into());
|
|
self
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn redirect(mut self, policy: reqwest::redirect::Policy) -> Self {
|
|
self.inner = self.inner.redirect(policy);
|
|
self
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn cookie_store(mut self, enabled: bool) -> Self {
|
|
self.inner = self.inner.cookie_store(enabled);
|
|
self
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn default_headers(mut self, headers: HeaderMap) -> Self {
|
|
self.inner = self.inner.default_headers(headers);
|
|
self
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn connect_timeout(mut self, timeout: Duration) -> Self {
|
|
self.inner = self.inner.connect_timeout(timeout);
|
|
self
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn timeout(mut self, timeout: Duration) -> Self {
|
|
self.inner = self.inner.timeout(timeout);
|
|
self
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn danger_accept_invalid_certs(mut self, accept_invalid_certs: bool) -> Self {
|
|
self.inner = self.inner.danger_accept_invalid_certs(accept_invalid_certs);
|
|
self
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[must_use]
|
|
pub fn unix_socket<P>(mut self, path: P) -> Self
|
|
where
|
|
P: AsRef<Path>,
|
|
{
|
|
self.inner = self.inner.unix_socket(path.as_ref());
|
|
self
|
|
}
|
|
|
|
pub fn build(self) -> Result<$client_type, HttpClientBuildError> {
|
|
let proxy_policy = ProxyPolicy::resolve(self.proxy_policy)?;
|
|
let inner = match proxy_policy {
|
|
ProxyPolicy::System => self.inner,
|
|
ProxyPolicy::Disabled => self.inner.no_proxy(),
|
|
};
|
|
inner.build().map_err(Into::into)
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
define_builder!(
|
|
HttpClientBuilder,
|
|
reqwest::ClientBuilder,
|
|
reqwest::Client::builder(),
|
|
HttpClient
|
|
);
|
|
|
|
// `read_timeout` is only available on the async builder.
|
|
impl HttpClientBuilder {
|
|
#[must_use]
|
|
pub fn read_timeout(mut self, timeout: Duration) -> Self {
|
|
self.inner = self.inner.read_timeout(timeout);
|
|
self
|
|
}
|
|
}
|
|
|
|
define_builder!(
|
|
BlockingHttpClientBuilder,
|
|
reqwest::blocking::ClientBuilder,
|
|
reqwest::blocking::Client::builder(),
|
|
BlockingHttpClient
|
|
);
|
|
|
|
pub fn http_client() -> Result<HttpClient, HttpClientBuildError> {
|
|
HttpClientBuilder::new().build()
|
|
}
|
|
|
|
pub fn test_http_client() -> Result<HttpClient, HttpClientBuildError> {
|
|
HttpClientBuilder::new()
|
|
.proxy_policy(ProxyPolicy::Disabled)
|
|
.build()
|
|
}
|
|
|
|
pub fn blocking_http_client() -> Result<BlockingHttpClient, HttpClientBuildError> {
|
|
BlockingHttpClientBuilder::new().build()
|
|
}
|
|
|
|
pub fn blocking_test_http_client() -> Result<BlockingHttpClient, HttpClientBuildError> {
|
|
BlockingHttpClientBuilder::new()
|
|
.proxy_policy(ProxyPolicy::Disabled)
|
|
.build()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn proxy_policy_defaults_to_system() {
|
|
assert_eq!(
|
|
ProxyPolicy::resolve_with_env_value(None, None).unwrap(),
|
|
ProxyPolicy::System
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn proxy_policy_reads_disabled_from_env() {
|
|
assert_eq!(
|
|
ProxyPolicy::resolve_with_env_value(None, Some("disabled")).unwrap(),
|
|
ProxyPolicy::Disabled
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn proxy_policy_rejects_invalid_env_values() {
|
|
let error = ProxyPolicy::resolve_with_env_value(None, Some("bogus")).unwrap_err();
|
|
assert!(
|
|
error
|
|
.to_string()
|
|
.contains("expected `system` or `disabled`")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn explicit_proxy_policy_overrides_env() {
|
|
assert_eq!(
|
|
ProxyPolicy::resolve_with_env_value(Some(ProxyPolicy::Disabled), Some("system"))
|
|
.unwrap(),
|
|
ProxyPolicy::Disabled
|
|
);
|
|
}
|
|
}
|