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);