zng_task/http/
ctx.rs

1use std::{any::Any, fmt, mem, pin::Pin};
2
3#[cfg(feature = "http_cookie")]
4use http::{HeaderValue, Uri};
5use parking_lot::Mutex;
6
7use crate::{
8    channel::IpcBytes,
9    http::{CacheKey, CachePolicy, Error, Request, Response, curl, file_cache},
10};
11
12type Fut<O> = Pin<Box<dyn Future<Output = O> + Send>>;
13
14/// HTTP cache backend.
15///
16/// Cache implementers must store a [`CachePolicy`] and [`IpcBytes`] body for a given [`CacheKey`].
17pub trait HttpCache: Send + Sync + Any {
18    /// Get the cache-policy for the given `key`.
19    fn policy(&'static self, key: CacheKey) -> Fut<Option<CachePolicy>>;
20
21    /// Replaces the cache-policy for the given `key`.
22    ///
23    /// Returns `false` if the entry does not exist.
24    fn set_policy(&'static self, key: CacheKey, policy: CachePolicy) -> Fut<bool>;
25
26    /// Get the cached body for the given `key`.
27    fn body(&'static self, key: CacheKey) -> Fut<Option<IpcBytes>>;
28
29    /// Caches the `policy` and `body` for the given `key`.
30    fn set(&'static self, key: CacheKey, policy: CachePolicy, body: IpcBytes) -> Fut<()>;
31
32    /// Remove cache policy and body for the given `key`.
33    fn remove(&'static self, key: CacheKey) -> Fut<()>;
34
35    /// Get the Cookie value associated with the `uri`.
36    ///
37    /// The returned value is validated and ready for sending.
38    #[cfg(feature = "http_cookie")]
39    fn cookie(&'static self, uri: Uri) -> Fut<Option<HeaderValue>>;
40
41    /// Store the Set-Cookie value associated with the `uri`.
42    ///
43    /// The uri and cookie must be directly from the response, the cache will parse and property associate the cookie with domain.
44    #[cfg(feature = "http_cookie")]
45    fn set_cookie(&'static self, uri: Uri, cookie: HeaderValue) -> Fut<()>;
46
47    /// Remove the Cookie value associated with the `uri`.
48    #[cfg(feature = "http_cookie")]
49    fn remove_cookie(&'static self, uri: Uri) -> Fut<()>;
50
51    /// Remove all cached entries that are not locked in a `set*` operation.
52    fn purge(&'static self) -> Fut<()>;
53
54    /// Remove cache entries to reduce pressure.
55    ///
56    /// What entries are removed depends on the cache DB implementer.
57    fn prune(&'static self) -> Fut<()>;
58}
59
60/// HTTP client backend.
61///
62/// See [`http_client`] for more details.
63pub trait HttpClient: Send + Sync + Any {
64    /// If the client manages cache and cookie storage.
65    ///
66    /// Is `false` by default. When `false` the [`http_cache`] is used before and after `send`.
67    fn is_cache_manager(&self) -> bool {
68        true
69    }
70
71    /// Send a request and await until response header is received.
72    /// Full response body can continue to be received using the [`Response`] value.
73    fn send(&'static self, request: Request) -> Fut<Result<Response, Error>>;
74}
75
76/// Error returned by [`set_http_client`] and [`set_http_cache`] if the default was already initialized.
77#[derive(Debug, Clone, Copy)]
78#[non_exhaustive]
79pub struct AlreadyInitedError {}
80impl fmt::Display for AlreadyInitedError {
81    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82        write!(f, "default client already initialized, can only set before first use")
83    }
84}
85impl std::error::Error for AlreadyInitedError {}
86
87/// The [`HttpClient`] used by the functions in this module.
88///
89/// You can replace the default client at the start of the process using [`set_http_client`].
90///
91/// # Defaults
92///
93/// The default client is a minimal implementation that uses the system `curl` command line executable.
94/// You can set the `"ZNG_CURL"` environment variable before the first usage to define the path to the curl executable.
95pub fn http_client() -> &'static dyn HttpClient {
96    use once_cell::sync::Lazy;
97
98    static SHARED: Lazy<Box<dyn HttpClient>> = Lazy::new(|| {
99        let ci = mem::replace(&mut *CLIENT_INIT.lock(), ClientInit::Inited);
100        if let ClientInit::Set(init) = ci {
101            init()
102        } else {
103            // browser defaults
104            Box::new(curl::CurlProcessClient::default())
105        }
106    });
107    &**SHARED
108}
109static CLIENT_INIT: Mutex<ClientInit> = Mutex::new(ClientInit::None);
110enum ClientInit {
111    None,
112    Set(Box<dyn FnOnce() -> Box<dyn HttpClient> + Send>),
113    Inited,
114}
115
116/// Set a custom initialization function for the [`http_client`].
117///
118/// The [`http_client`] is used by all functions in this module and is initialized on the first usage,
119/// you can use this function before any HTTP operation to replace backend implementation.
120///
121/// Returns an error if the [`http_client`] was already initialized.
122///
123/// [`isahc`]: https://docs.rs/isahc
124pub fn set_http_client<I>(init: I) -> Result<(), AlreadyInitedError>
125where
126    I: FnOnce() -> Box<dyn HttpClient> + Send + 'static,
127{
128    let mut ci = CLIENT_INIT.lock();
129    if let ClientInit::Inited = &*ci {
130        Err(AlreadyInitedError {})
131    } else {
132        *ci = ClientInit::Set(Box::new(init));
133        Ok(())
134    }
135}
136
137/// The [`HttpCache`] used by the functions in this module.
138///
139/// You can replace the default cache at the start of the process using [`set_http_cache`].
140///
141/// # Defaults
142///
143/// The default cache is a minimal implementation that uses the file system.
144pub fn http_cache() -> &'static dyn HttpCache {
145    use once_cell::sync::Lazy;
146
147    static SHARED: Lazy<Box<dyn HttpCache>> = Lazy::new(|| {
148        let ci = mem::replace(&mut *CACHE_INIT.lock(), CacheInit::Inited);
149        if let CacheInit::Set(init) = ci {
150            init()
151        } else {
152            Box::new(file_cache::FileSystemCache::new(zng_env::cache("zng-task-http-cache")).unwrap())
153        }
154    });
155    &**SHARED
156}
157static CACHE_INIT: Mutex<CacheInit> = Mutex::new(CacheInit::None);
158enum CacheInit {
159    None,
160    Set(Box<dyn FnOnce() -> Box<dyn HttpCache> + Send>),
161    Inited,
162}
163
164/// Set a custom initialization function for the [`http_client`].
165///
166/// The [`http_client`] is used by all functions in this module and is initialized on the first usage,
167/// you can use this function before any HTTP operation to replace backend implementation.
168///
169/// Returns an error if the [`http_client`] was already initialized.
170///
171/// [`isahc`]: https://docs.rs/isahc
172pub fn set_http_cache<I>(init: I) -> Result<(), AlreadyInitedError>
173where
174    I: FnOnce() -> Box<dyn HttpCache> + Send + 'static,
175{
176    let mut ci = CACHE_INIT.lock();
177    if let CacheInit::Inited = &*ci {
178        Err(AlreadyInitedError {})
179    } else {
180        *ci = CacheInit::Set(Box::new(init));
181        Ok(())
182    }
183}
184
185/// Set the default values returned by [`Request::new`].
186///
187/// The method and uri are ignored in this value, the other fields are used as default in all subsequent requests.
188pub fn set_request_default(d: Request) {
189    *REQUEST_DEFAULT.lock() = Some(d);
190}
191pub(super) static REQUEST_DEFAULT: Mutex<Option<Request>> = Mutex::new(None);