Skip to main content

karyon_jsonrpc/client/http/
mod.rs

1//! HTTP client backend: HTTP/1.1 (smol, or pooled on tokio), opt-in
2//! HTTP/2 over plain TCP, and HTTP/3 (QUIC).
3
4mod h1;
5mod h2;
6#[cfg(feature = "http3")]
7mod h3;
8
9use std::sync::Arc;
10use std::time::Duration;
11
12#[cfg(feature = "tokio")]
13use bytes::Bytes;
14use http_body_util::BodyExt;
15#[cfg(feature = "tokio")]
16use http_body_util::Full;
17use hyper::StatusCode;
18use log::info;
19use serde::{de::DeserializeOwned, Serialize};
20use serde_json::json;
21
22#[cfg(feature = "tokio")]
23use hyper_util::client::legacy::{connect::HttpConnector, Client as HyperClient};
24
25#[cfg(feature = "tokio")]
26use crate::hyper_exec::HyperExecutor;
27
28use karyon_core::{
29    async_util::{timeout, TaskGroup},
30    util::random_32,
31};
32
33#[cfg(feature = "http3")]
34use karyon_core::async_util::TaskResult;
35
36use karyon_net::Endpoint;
37
38use crate::{
39    client::{Client, ClientBackend, ClientConfig, RequestID, WsCodec},
40    codec::{decode_body, JsonRpcCodec, DEFAULT_CONTENT_TYPE},
41    error::{Error, Result},
42    message::{self, SubscriptionID},
43};
44
45#[cfg(feature = "http3")]
46use std::collections::HashMap;
47
48#[cfg(feature = "http3")]
49use parking_lot::Mutex;
50
51#[cfg(feature = "http3")]
52use crate::client::subscriptions::{Subscription, Subscriptions};
53
54/// HTTP settings for `http://` endpoints, set with
55/// `ClientBuilder::http_config`.
56#[derive(Clone, Debug)]
57pub struct HttpConfig {
58    /// Use HTTP/2 over plain TCP (prior knowledge, no upgrade) instead of
59    /// HTTP/1.1. One connection is opened in `build()` and shared by all
60    /// calls. A QUIC config (HTTP/3) takes priority.
61    pub http2: bool,
62    /// `Host` header for HTTP/1.1 requests. Not sent when `None`.
63    pub host: Option<String>,
64    /// `Content-Type` of the requests. Set it to match a custom codec.
65    /// Defaults to `application/json`.
66    pub content_type: String,
67}
68
69impl Default for HttpConfig {
70    fn default() -> Self {
71        Self {
72            http2: false,
73            host: None,
74            content_type: DEFAULT_CONTENT_TYPE.to_string(),
75        }
76    }
77}
78
79/// HTTP client backend for JSON-RPC.
80pub(crate) struct HttpClientBackend {
81    inner: HttpTransport,
82    config: HttpConfig,
83    /// Shared with the Client so driver tasks (h1 per-request, h3
84    /// connection) get cancelled when the Client stops.
85    task_group: Arc<TaskGroup>,
86}
87
88// smol uses HTTP/1.1 via `hyper::client::conn::http1`, which is the
89// smallest hyper API that compiles against `smol-hyper`. tokio gets
90// HTTP/1.1 with connection pooling through `hyper-util`'s legacy
91// `Client`, boxed because it is much larger than the other variants.
92// HTTP/2 is opt-in and uses one shared connection on both runtimes.
93enum HttpTransport {
94    #[cfg(feature = "smol")]
95    Smol { endpoint: Endpoint },
96    #[cfg(feature = "tokio")]
97    Tokio {
98        endpoint: Endpoint,
99        client: Box<HyperClient<HttpConnector, Full<Bytes>>>,
100    },
101    H2 {
102        endpoint: Endpoint,
103        sender: h2::H2SendRequest,
104    },
105    #[cfg(feature = "http3")]
106    H3 { send_request: h3::H3SendRequest },
107}
108
109impl HttpClientBackend {
110    /// Create an HTTP/1.1 client.
111    pub(crate) fn new(
112        endpoint: &Endpoint,
113        config: &HttpConfig,
114        task_group: Arc<TaskGroup>,
115    ) -> Result<Self> {
116        #[cfg(feature = "smol")]
117        let inner = HttpTransport::Smol {
118            endpoint: endpoint.clone(),
119        };
120
121        #[cfg(feature = "tokio")]
122        let inner = HttpTransport::Tokio {
123            endpoint: endpoint.clone(),
124            client: Box::new(
125                HyperClient::builder(HyperExecutor::new(task_group.clone())).build_http(),
126            ),
127        };
128
129        Ok(Self {
130            inner,
131            config: config.clone(),
132            task_group,
133        })
134    }
135
136    /// Create an HTTP/2 client over plain TCP. Opens the connection now.
137    pub(crate) async fn new_h2(
138        endpoint: &Endpoint,
139        config: &HttpConfig,
140        task_group: Arc<TaskGroup>,
141    ) -> Result<Self> {
142        let sender = h2::connect(endpoint, &task_group).await?;
143        Ok(Self {
144            inner: HttpTransport::H2 {
145                endpoint: endpoint.clone(),
146                sender,
147            },
148            config: config.clone(),
149            task_group,
150        })
151    }
152
153    /// Create an HTTP/3 client (QUIC). The driver task is spawned via
154    /// `task_group` so it gets cancelled when the Client stops.
155    #[cfg(feature = "http3")]
156    pub(crate) async fn new_h3(
157        endpoint: &Endpoint,
158        quic_config: &karyon_net::quic::ClientQuicConfig,
159        config: &HttpConfig,
160        task_group: Arc<TaskGroup>,
161    ) -> Result<Self> {
162        let send_request = h3::connect(endpoint, quic_config, task_group.clone()).await?;
163        Ok(Self {
164            inner: HttpTransport::H3 { send_request },
165            config: config.clone(),
166            task_group,
167        })
168    }
169
170    /// Send a JSON-RPC request and return the response.
171    pub(crate) async fn send_request<C: JsonRpcCodec>(
172        &self,
173        msg: serde_json::Value,
174        codec: &C,
175    ) -> Result<message::Response> {
176        let _ = &self.task_group;
177        let config = &self.config;
178        match &self.inner {
179            #[cfg(feature = "smol")]
180            HttpTransport::Smol { endpoint } => {
181                h1::send(endpoint, config, msg, &self.task_group, codec).await
182            }
183            #[cfg(feature = "tokio")]
184            HttpTransport::Tokio { endpoint, client } => {
185                h1::send(client, endpoint, config, msg, codec).await
186            }
187            HttpTransport::H2 { endpoint, sender } => {
188                h2::send(sender, endpoint, config, msg, codec).await
189            }
190            #[cfg(feature = "http3")]
191            HttpTransport::H3 { send_request } => {
192                h3::send(send_request.clone(), config, msg, codec).await
193            }
194        }
195    }
196
197    /// Subscribe over HTTP/3. Returns the initial response + recv stream.
198    #[cfg(feature = "http3")]
199    pub(crate) async fn subscribe_h3<C: JsonRpcCodec>(
200        &self,
201        msg: serde_json::Value,
202        codec: &C,
203    ) -> Result<(message::Response, h3::H3SendStream, h3::H3Reader<C>)> {
204        let HttpTransport::H3 { send_request } = &self.inner else {
205            return Err(Error::UnsupportedProtocol(
206                "subscribe_h3 requires HTTP/3".into(),
207            ));
208        };
209        h3::subscribe(send_request.clone(), &self.config, msg, codec).await
210    }
211}
212
213/// Shared HTTP response parser (used by h1 and h2).
214pub(super) async fn parse_response<B, C: JsonRpcCodec>(
215    response: hyper::Response<B>,
216    codec: &C,
217) -> Result<message::Response>
218where
219    B: hyper::body::Body,
220    B::Error: std::fmt::Display,
221{
222    if response.status() != StatusCode::OK {
223        return Err(Error::HttpError(format!(
224            "HTTP error: {}",
225            response.status()
226        )));
227    }
228
229    let body = response
230        .collect()
231        .await
232        .map_err(|e| Error::HttpError(e.to_string()))?
233        .to_bytes();
234
235    let rpc_response: message::Response = serde_json::from_value(decode_body(codec, &body)?)?;
236    Ok(rpc_response)
237}
238
239/// Build the HTTP backend: HTTP/3 if a QUIC config is set, else
240/// HTTP/2 if enabled, else HTTP/1.1.
241pub(super) async fn build_backend(
242    config: &ClientConfig,
243    task_group: Arc<TaskGroup>,
244) -> Result<ClientBackend> {
245    #[cfg(feature = "http3")]
246    if let Some(qc) = config.quic_config.as_ref() {
247        let backend =
248            HttpClientBackend::new_h3(&config.endpoint, qc, &config.http_config, task_group)
249                .await?;
250        return Ok(http_client_backend(config, backend));
251    }
252
253    let http_backend = if config.http_config.http2 {
254        HttpClientBackend::new_h2(&config.endpoint, &config.http_config, task_group).await?
255    } else {
256        HttpClientBackend::new(&config.endpoint, &config.http_config, task_group)?
257    };
258    Ok(http_client_backend(config, http_backend))
259}
260
261fn http_client_backend(config: &ClientConfig, http_backend: HttpClientBackend) -> ClientBackend {
262    info!("HTTP client configured for endpoint: {}", config.endpoint);
263
264    ClientBackend::Http {
265        http_backend: Box::new(http_backend),
266        #[cfg(feature = "http3")]
267        subscriptions: Subscriptions::new(config.subscription_buffer_size),
268        #[cfg(feature = "http3")]
269        unsub_chans: Mutex::new(HashMap::new()),
270    }
271}
272
273// -- Client impl (HTTP-specific) --
274
275impl<B, W> Client<B, W>
276where
277    B: JsonRpcCodec,
278    W: WsCodec,
279{
280    pub(super) async fn http_call<T: Serialize + DeserializeOwned>(
281        &self,
282        http_backend: &HttpClientBackend,
283        method: &str,
284        params: T,
285    ) -> Result<message::Response> {
286        let id: RequestID = random_32();
287        let request = message::Request {
288            jsonrpc: message::JSONRPC_VERSION.to_string(),
289            id: json!(id),
290            method: method.to_string(),
291            params: Some(json!(params)),
292        };
293        let msg = serde_json::to_value(request)?;
294
295        let response = match self.config.timeout {
296            Some(t) => {
297                timeout(
298                    Duration::from_millis(t),
299                    http_backend.send_request(msg, &self.codec),
300                )
301                .await?
302            }
303            None => http_backend.send_request(msg, &self.codec).await,
304        }?;
305
306        Ok(response)
307    }
308
309    /// Subscribe over HTTP/3.
310    #[cfg(feature = "http3")]
311    pub(super) async fn h3_subscribe<T: Serialize + DeserializeOwned>(
312        self: &Arc<Self>,
313        http_backend: &HttpClientBackend,
314        method: &str,
315        params: T,
316    ) -> Result<Arc<Subscription>> {
317        let ClientBackend::Http {
318            subscriptions,
319            unsub_chans,
320            ..
321        } = &self.backend
322        else {
323            return Err(Error::InvalidState("not in HTTP mode".into()));
324        };
325
326        let id: RequestID = random_32();
327        let request = message::Request {
328            jsonrpc: message::JSONRPC_VERSION.to_string(),
329            id: json!(id),
330            method: method.to_string(),
331            params: Some(json!(params)),
332        };
333        let msg = serde_json::to_value(request)?;
334
335        let (response, send, reader) = http_backend.subscribe_h3(msg, &self.codec).await?;
336
337        if let Some(error) = response.error {
338            return Err(Error::SubscribeError(error.code, error.message));
339        }
340
341        let sub_id = match response.result {
342            Some(result) => serde_json::from_value::<SubscriptionID>(result)?,
343            None => return Err(Error::InvalidMsg("Invalid subscription id".into())),
344        };
345
346        let (unsub_tx, unsub_rx) = async_channel::bounded::<serde_json::Value>(1);
347        unsub_chans.lock().insert(sub_id, unsub_tx);
348
349        let sub = subscriptions.subscribe(sub_id).await;
350
351        self.task_group.spawn_then(
352            h3::subscription_task(
353                send,
354                reader,
355                unsub_rx,
356                sub.clone(),
357                self.config.notification_decoder,
358            ),
359            |res: TaskResult<Result<()>>| async move {
360                log::debug!("H3 subscription task ended: {res}");
361            },
362        );
363
364        Ok(sub)
365    }
366
367    /// Unsubscribe over HTTP/3. The subscription task sends the request
368    /// on the subscription's own stream.
369    #[cfg(feature = "http3")]
370    pub(super) async fn h3_unsubscribe(&self, method: &str, sub_id: SubscriptionID) -> Result<()> {
371        let ClientBackend::Http {
372            subscriptions,
373            unsub_chans,
374            ..
375        } = &self.backend
376        else {
377            return Err(Error::InvalidState("not in HTTP mode".into()));
378        };
379
380        // Own `let` so the MutexGuard is dropped before the `.await`.
381        let ch = unsub_chans.lock().remove(&sub_id);
382        if let Some(ch) = ch {
383            let request = message::Request {
384                jsonrpc: message::JSONRPC_VERSION.to_string(),
385                id: json!(random_32()),
386                method: method.to_string(),
387                params: Some(json!(sub_id)),
388            };
389            let _ = ch.send(serde_json::to_value(request)?).await;
390        }
391        subscriptions.unsubscribe(&sub_id).await;
392        Ok(())
393    }
394}