Skip to main content

secretspec_ipc/
client.rs

1use crate::deadline::instant_from_unix_ms;
2use crate::error::{ErrorKind, RpcError};
3use crate::frame::{AsyncFrameReader, write_frame};
4use crate::jsonrpc::{Envelope, Notification, Request, RequestId, Response};
5use crate::protocol::{CancelParams, InitializeParams, InitializeResult, Limits, rpc};
6use crate::{ABSOLUTE_MAX_FRAME_BYTES, Error, Result};
7use serde::Serialize;
8use serde::de::DeserializeOwned;
9use serde_json::{Value, json};
10use std::collections::{HashMap, HashSet};
11use std::sync::atomic::{AtomicU8, AtomicU64, AtomicUsize, Ordering};
12use std::sync::{Arc, Mutex as StdMutex, Weak};
13use std::time::Duration;
14use tokio::io::{AsyncRead, AsyncWrite};
15use tokio::sync::{Mutex, Notify, RwLock, Semaphore, mpsc, oneshot};
16use tokio::task::JoinHandle;
17use tokio::time::Instant;
18use tokio_util::sync::CancellationToken;
19use zeroize::{Zeroize, Zeroizing};
20
21const INITIALIZING: u8 = 0;
22const READY: u8 = 1;
23const CLOSING: u8 = 2;
24const CLOSED: u8 = 3;
25const MAX_ABANDONED_REQUESTS: usize = crate::MAX_IN_FLIGHT * 4;
26
27enum WriterCommand {
28    Frame {
29        payload: Zeroizing<Vec<u8>>,
30        limit: usize,
31    },
32    Close,
33}
34
35/// Answers the server's callbacks on this session (0.21+).
36///
37/// A client installs one only for the methods it advertised in
38/// `client_methods`; a server never sends anything else. The returned
39/// value is serialized as the callback's result, and an `RpcError` becomes its
40/// error response, so declining is a normal outcome rather than a transport
41/// failure.
42#[async_trait::async_trait]
43pub trait CallbackHandler: Send + Sync + 'static {
44    async fn call(&self, method: &str, params: Value) -> std::result::Result<Value, RpcError>;
45}
46
47struct Inner {
48    writer: mpsc::Sender<WriterCommand>,
49    callbacks: Option<Arc<dyn CallbackHandler>>,
50    /// The callbacks this client advertised. Fixed at initialization, so it
51    /// needs no lock and cannot drift from what the server was told.
52    advertised: HashSet<String>,
53    inbound: StdMutex<InboundCallbacks>,
54    pending: StdMutex<HashMap<RequestId, PendingRequest>>,
55    abandoned: StdMutex<HashSet<RequestId>>,
56    // Keep ID allocation and writer enqueue in the same order.
57    request_order: Mutex<()>,
58    next_id: AtomicU64,
59    max_frame_bytes: AtomicUsize,
60    max_in_flight: AtomicUsize,
61    limits: RwLock<Limits>,
62    semaphore: RwLock<Arc<Semaphore>>,
63    capabilities: RwLock<HashSet<String>>,
64    state: AtomicU8,
65    closed: Notify,
66    reader_task: Mutex<Option<JoinHandle<()>>>,
67    writer_task: Mutex<Option<JoinHandle<()>>>,
68}
69
70impl Drop for Inner {
71    fn drop(&mut self) {
72        // Connected sessions have no child whose exit would close the stream.
73        // Do not leave a reader blocked forever when the last client is dropped.
74        // Explicit close remains the graceful, awaited cleanup path.
75        if let Some(task) = self.reader_task.get_mut().take() {
76            task.abort();
77        }
78        if let Some(task) = self.writer_task.get_mut().take() {
79            task.abort();
80        }
81    }
82}
83
84struct PendingRequest {
85    sender: oneshot::Sender<Response>,
86    deadline_unix_ms: u64,
87    cancellation: CancellationToken,
88}
89
90#[derive(Default)]
91struct InboundCallbacks {
92    active: usize,
93    last_seen_id: Option<RequestId>,
94}
95
96/// A multiplexed SecretSpec JSON-RPC session.
97#[derive(Clone)]
98pub struct Client {
99    inner: Arc<Inner>,
100}
101
102impl Client {
103    /// Initialize a session on an already-authenticated private transport.
104    pub async fn connect<R, W, A, B>(
105        reader: R,
106        writer: W,
107        initialize: InitializeParams<A>,
108        startup_deadline_unix_ms: u64,
109    ) -> Result<(Self, InitializeResult<B>)>
110    where
111        R: AsyncRead + Unpin + Send + 'static,
112        W: AsyncWrite + Unpin + Send + 'static,
113        A: Serialize,
114        B: DeserializeOwned,
115    {
116        Self::connect_with_callbacks(reader, writer, initialize, startup_deadline_unix_ms, None)
117            .await
118    }
119
120    /// As [`Self::connect`], installing a handler for the callbacks this
121    /// client advertised in `client_methods` (0.21+).
122    ///
123    /// Passing `None` while advertising a callback would leave the server
124    /// waiting out its deadline on a request nothing answers, so the two are
125    /// checked against each other here rather than at the first callback.
126    pub async fn connect_with_callbacks<R, W, A, B>(
127        reader: R,
128        mut writer: W,
129        initialize: InitializeParams<A>,
130        startup_deadline_unix_ms: u64,
131        callbacks: Option<Arc<dyn CallbackHandler>>,
132    ) -> Result<(Self, InitializeResult<B>)>
133    where
134        R: AsyncRead + Unpin + Send + 'static,
135        W: AsyncWrite + Unpin + Send + 'static,
136        A: Serialize,
137        B: DeserializeOwned,
138    {
139        if callbacks.is_none() && !initialize.client_methods.is_empty() {
140            return Err(Error::Protocol(
141                "client advertised a callback with no handler to answer it",
142            ));
143        }
144        let offered_protocol = initialize.protocol.clone();
145        let offered_versions = initialize.versions.clone();
146        let offered_limits = initialize.limits;
147        let advertised: HashSet<String> = initialize.client_methods.iter().cloned().collect();
148        // `offered_protocol` is a copy of `initialize.protocol`, so the protocol
149        // comparison inside `validate_common` is trivially true here. The call
150        // is kept for the checks that do bite locally: version list shape,
151        // product strings, and limit ranges.
152        initialize.validate_common(&offered_protocol)?;
153        let initialize_id = RequestId::new(1)?;
154        let initialize_params = serde_json::to_value(initialize)
155            .map_err(|_| Error::Protocol("failed to serialize initialization"))?;
156        let initialize_request = Request::new(
157            initialize_id,
158            rpc::INITIALIZE,
159            startup_deadline_unix_ms,
160            initialize_params,
161        )?;
162
163        let (writer_tx, mut writer_rx) =
164            mpsc::channel::<WriterCommand>(offered_limits.max_in_flight.saturating_add(2));
165        let mut reader = AsyncFrameReader::new(reader);
166        let inner = Arc::new(Inner {
167            writer: writer_tx,
168            callbacks,
169            advertised,
170            inbound: StdMutex::new(InboundCallbacks::default()),
171            pending: StdMutex::new(HashMap::new()),
172            abandoned: StdMutex::new(HashSet::new()),
173            request_order: Mutex::new(()),
174            next_id: AtomicU64::new(2),
175            max_frame_bytes: AtomicUsize::new(ABSOLUTE_MAX_FRAME_BYTES),
176            max_in_flight: AtomicUsize::new(1),
177            limits: RwLock::new(Limits::PRE_NEGOTIATION),
178            semaphore: RwLock::new(Arc::new(Semaphore::new(1))),
179            capabilities: RwLock::new(HashSet::new()),
180            state: AtomicU8::new(INITIALIZING),
181            closed: Notify::new(),
182            reader_task: Mutex::new(None),
183            writer_task: Mutex::new(None),
184        });
185
186        let writer_inner = Arc::downgrade(&inner);
187        let writer_task = tokio::spawn(async move {
188            while let Some(command) = writer_rx.recv().await {
189                match command {
190                    WriterCommand::Frame { payload, limit } => {
191                        if write_frame(&mut writer, &payload, limit).await.is_err() {
192                            fail_session_weak(&writer_inner);
193                            break;
194                        }
195                    }
196                    WriterCommand::Close => break,
197                }
198            }
199            use tokio::io::AsyncWriteExt;
200            let _ = writer.shutdown().await;
201        });
202        *inner.writer_task.lock().await = Some(writer_task);
203
204        let reader_inner = Arc::downgrade(&inner);
205        let reader_task = tokio::spawn(async move {
206            loop {
207                let Some(inner) = reader_inner.upgrade() else {
208                    break;
209                };
210                let limit = inner.max_frame_bytes.load(Ordering::Acquire);
211                drop(inner);
212                let frame = match reader.read_frame(limit).await {
213                    Ok(Some(frame)) => frame,
214                    Ok(None) | Err(_) => {
215                        fail_session_weak(&reader_inner);
216                        break;
217                    }
218                };
219                let response = match Envelope::parse(&frame) {
220                    Ok(Envelope::Response(response)) => response,
221                    // A callback the client advertised. It is answered on its
222                    // own task so this loop keeps reading: the same session
223                    // still carries the responses this client is waiting for,
224                    // and blocking here to ask a person would deadlock both
225                    // sides until the deadline.
226                    Ok(Envelope::Request(request)) => {
227                        let Some(inner) = reader_inner.upgrade() else {
228                            break;
229                        };
230                        let Some(handler) = inner.callbacks.clone() else {
231                            fail_session(&inner);
232                            break;
233                        };
234                        if !serve_callback(&inner, handler, request) {
235                            fail_session(&inner);
236                            break;
237                        }
238                        continue;
239                    }
240                    // Notifications have no response channel. Ignore unknown
241                    // methods and malformed method-specific parameters after
242                    // the strict envelope itself has parsed.
243                    Ok(Envelope::Notification(_)) => continue,
244                    _ => {
245                        fail_session_weak(&reader_inner);
246                        break;
247                    }
248                };
249                let Some(id) = response.id() else {
250                    fail_session_weak(&reader_inner);
251                    break;
252                };
253                let Some(inner) = reader_inner.upgrade() else {
254                    break;
255                };
256                let pending = lock_unpoisoned(&inner.pending).remove(&id);
257                if let Some(pending) = pending {
258                    // A deadline can race a response after marking this ID as
259                    // abandoned but before removing it from `pending`.
260                    lock_unpoisoned(&inner.abandoned).remove(&id);
261                    pending.cancellation.cancel();
262                    let _ = pending.sender.send(response);
263                    continue;
264                }
265                if lock_unpoisoned(&inner.abandoned).remove(&id) {
266                    continue;
267                }
268                // Unknown and duplicate terminal IDs are protocol violations.
269                fail_session(&inner);
270                break;
271            }
272        });
273        *inner.reader_task.lock().await = Some(reader_task);
274
275        let client = Self { inner };
276        let response = match client
277            .request_internal(initialize_request, ABSOLUTE_MAX_FRAME_BYTES)
278            .await
279        {
280            Ok(response) => response,
281            Err(error) => {
282                client.abort_transport().await;
283                return Err(error);
284            }
285        };
286        let initialized = response_value(response)
287            .and_then(|value| {
288                serde_json::from_value::<InitializeResult<B>>(value)
289                    .map_err(|error| Error::ProtocolOwned(error.to_string()))
290            })
291            .and_then(|initialized| {
292                initialized.validate_common(
293                    &offered_protocol,
294                    &offered_versions,
295                    offered_limits,
296                )?;
297                Ok(initialized)
298            });
299        let initialized = match initialized {
300            Ok(initialized) => initialized,
301            Err(error) => {
302                client.abort_transport().await;
303                return Err(error);
304            }
305        };
306        client
307            .inner
308            .max_frame_bytes
309            .store(initialized.limits.max_frame_bytes, Ordering::Release);
310        client
311            .inner
312            .max_in_flight
313            .store(initialized.limits.max_in_flight, Ordering::Release);
314        *client.inner.limits.write().await = initialized.limits;
315        *client.inner.semaphore.write().await =
316            Arc::new(Semaphore::new(initialized.limits.max_in_flight));
317        *client.inner.capabilities.write().await = initialized.methods.iter().cloned().collect();
318        client.inner.state.store(READY, Ordering::Release);
319        Ok((client, initialized))
320    }
321
322    pub async fn start<T: Serialize>(
323        &self,
324        method: &str,
325        params: &T,
326        deadline_unix_ms: u64,
327    ) -> Result<Call> {
328        if self.inner.state.load(Ordering::Acquire) != READY {
329            return Err(Error::Closed);
330        }
331        if !self.inner.capabilities.read().await.contains(method) {
332            return Err(Error::Protocol("method was not advertised"));
333        }
334        let params = serde_json::to_value(params)
335            .map_err(|_| Error::Protocol("failed to serialize call params"))?;
336        // Clamp once, then use the same value locally and on the wire so the
337        // peer never enforces a longer deadline than this client waits for.
338        let deadline_unix_ms = crate::deadline::clamp_unix_ms(deadline_unix_ms);
339        let deadline = instant_from_unix_ms(deadline_unix_ms);
340        if deadline <= Instant::now() {
341            return Err(Error::DeadlineExceeded);
342        }
343        let semaphore = self.inner.semaphore.read().await.clone();
344        let permit = semaphore
345            .try_acquire_owned()
346            .map_err(|_| Error::Unavailable)?;
347        let _order = tokio::time::timeout_at(deadline, self.inner.request_order.lock())
348            .await
349            .map_err(|_| Error::DeadlineExceeded)?;
350        if self.inner.state.load(Ordering::Acquire) != READY {
351            return Err(Error::Closed);
352        }
353        let id = self.next_id()?;
354        let request = Request::new(id, method, deadline_unix_ms, params)?;
355        let (sender, receiver) = oneshot::channel();
356        lock_unpoisoned(&self.inner.pending).insert(
357            id,
358            PendingRequest {
359                sender,
360                deadline_unix_ms,
361                cancellation: CancellationToken::new(),
362            },
363        );
364        let registration = PendingRegistration::new(&self.inner, id);
365        self.queue_request(request, deadline).await?;
366        registration.queued();
367        Ok(Call {
368            id,
369            deadline,
370            receiver: Some(receiver),
371            client: Arc::downgrade(&self.inner),
372            _permit: Some(permit),
373            terminal: false,
374        })
375    }
376
377    pub async fn call<P: Serialize, R: DeserializeOwned>(
378        &self,
379        method: &str,
380        params: &P,
381        deadline_unix_ms: u64,
382    ) -> Result<R> {
383        let mut call = self.start(method, params, deadline_unix_ms).await?;
384        let value = call.wait().await?;
385        serde_json::from_value(value).map_err(|error| Error::ProtocolOwned(error.to_string()))
386    }
387
388    pub async fn limits(&self) -> Limits {
389        *self.inner.limits.read().await
390    }
391
392    pub async fn capabilities(&self) -> HashSet<String> {
393        self.inner.capabilities.read().await.clone()
394    }
395
396    pub fn is_closed(&self) -> bool {
397        self.inner.state.load(Ordering::Acquire) == CLOSED
398    }
399
400    /// Close the protocol session. The process launcher separately enforces
401    /// child termination and reaping after this wire shutdown completes.
402    pub async fn close(&self, deadline_unix_ms: u64) -> Result<()> {
403        let _order = tokio::time::timeout_at(
404            instant_from_unix_ms(deadline_unix_ms),
405            self.inner.request_order.lock(),
406        )
407        .await
408        .map_err(|_| Error::DeadlineExceeded)?;
409        let state =
410            self.inner
411                .state
412                .compare_exchange(READY, CLOSING, Ordering::AcqRel, Ordering::Acquire);
413        let outcome = match state {
414            Ok(_) => {
415                let id = self.next_id()?;
416                let request = Request::new(id, rpc::SHUTDOWN, deadline_unix_ms, json!({}))?;
417                self.request_internal(request, self.inner.max_frame_bytes.load(Ordering::Acquire))
418                    .await
419                    .and_then(|response| {
420                        let value = response_value(response)?;
421                        if value == json!({}) {
422                            Ok(())
423                        } else {
424                            Err(Error::Protocol("shutdown result is not empty"))
425                        }
426                    })
427            }
428            Err(CLOSED) => Ok(()),
429            Err(_) => return Err(Error::Closed),
430        };
431
432        let _ = self.inner.writer.try_send(WriterCommand::Close);
433        let deadline = instant_from_unix_ms(deadline_unix_ms);
434        if let Some(mut task) = self.inner.writer_task.lock().await.take()
435            && tokio::time::timeout_at(deadline, &mut task).await.is_err()
436        {
437            task.abort();
438            let _ = task.await;
439        }
440        if let Some(mut task) = self.inner.reader_task.lock().await.take()
441            && tokio::time::timeout_at(deadline, &mut task).await.is_err()
442        {
443            task.abort();
444            let _ = task.await;
445        }
446        fail_session(&self.inner);
447        outcome
448    }
449
450    /// Mark a transport dead from a process watcher without exposing pending
451    /// request data in an error.
452    pub async fn abandon(&self) {
453        fail_session(&self.inner);
454    }
455
456    /// Let the transport reader consume frames already buffered in the pipe
457    /// before a process watcher declares the session dead. A child can write a
458    /// terminal response and exit before the reader task is scheduled; failing
459    /// the session immediately in that window would discard the valid reply.
460    pub(crate) async fn abandon_after_process_exit(&self, grace: Duration) {
461        if self.is_closed() {
462            return;
463        }
464        let closed = self.inner.closed.notified();
465        tokio::pin!(closed);
466        closed.as_mut().enable();
467        if self.is_closed() {
468            return;
469        }
470        if tokio::time::timeout(grace, closed).await.is_err() && !self.is_closed() {
471            fail_session(&self.inner);
472        }
473    }
474
475    async fn abort_transport(&self) {
476        fail_session(&self.inner);
477        let _ = self.inner.writer.try_send(WriterCommand::Close);
478        if let Some(task) = self.inner.writer_task.lock().await.take() {
479            task.abort();
480            let _ = task.await;
481        }
482        if let Some(task) = self.inner.reader_task.lock().await.take() {
483            task.abort();
484            let _ = task.await;
485        }
486    }
487
488    fn next_id(&self) -> Result<RequestId> {
489        let id = self.inner.next_id.fetch_add(1, Ordering::Relaxed);
490        RequestId::new(id)
491    }
492
493    async fn queue_request(&self, request: Request, deadline: Instant) -> Result<()> {
494        let envelope = Envelope::Request(request);
495        let payload = Zeroizing::new(envelope.to_vec()?);
496        let limit = self.inner.max_frame_bytes.load(Ordering::Acquire);
497        if payload.len() > limit {
498            return Err(Error::Protocol(
499                "request exceeds the negotiated frame limit",
500            ));
501        }
502        match tokio::time::timeout_at(
503            deadline,
504            self.inner
505                .writer
506                .send(WriterCommand::Frame { payload, limit }),
507        )
508        .await
509        {
510            Ok(Ok(())) => Ok(()),
511            Ok(Err(_)) => Err(Error::Closed),
512            Err(_) => Err(Error::DeadlineExceeded),
513        }
514    }
515
516    async fn request_internal(&self, request: Request, limit: usize) -> Result<Response> {
517        let id = request.id;
518        let deadline_unix_ms = request.deadline_unix_ms();
519        let deadline = instant_from_unix_ms(deadline_unix_ms);
520        let (sender, receiver) = oneshot::channel();
521        lock_unpoisoned(&self.inner.pending).insert(
522            id,
523            PendingRequest {
524                sender,
525                deadline_unix_ms,
526                cancellation: CancellationToken::new(),
527            },
528        );
529        let registration = PendingRegistration::new(&self.inner, id);
530        let payload = Zeroizing::new(Envelope::Request(request).to_vec()?);
531        if payload.len() > limit {
532            return Err(Error::Protocol("request exceeds the active frame limit"));
533        }
534        match tokio::time::timeout_at(
535            deadline,
536            self.inner
537                .writer
538                .send(WriterCommand::Frame { payload, limit }),
539        )
540        .await
541        {
542            Ok(Ok(())) => registration.queued(),
543            Ok(Err(_)) => return Err(Error::Closed),
544            Err(_) => return Err(Error::DeadlineExceeded),
545        }
546        match tokio::time::timeout_at(deadline, receiver).await {
547            Ok(Ok(response)) => Ok(response),
548            Ok(Err(_)) => Err(Error::Closed),
549            Err(_) => {
550                abandon_request(&self.inner, id);
551                Err(Error::DeadlineExceeded)
552            }
553        }
554    }
555}
556
557/// One in-flight call. Exactly one task may wait on a call; cancellation may
558/// race that waiter through `cancel`.
559pub struct Call {
560    id: RequestId,
561    deadline: Instant,
562    receiver: Option<oneshot::Receiver<Response>>,
563    client: Weak<Inner>,
564    _permit: Option<tokio::sync::OwnedSemaphorePermit>,
565    terminal: bool,
566}
567
568impl Call {
569    pub const fn id(&self) -> RequestId {
570        self.id
571    }
572
573    pub async fn cancel(&self) -> Result<()> {
574        let Some(client) = self.client.upgrade() else {
575            return Err(Error::Closed);
576        };
577        cancel_parent_callbacks(&client, self.id);
578        match tokio::time::timeout_at(self.deadline, queue_cancel(&client, self.id)).await {
579            Ok(result) => result,
580            Err(_) => Err(Error::DeadlineExceeded),
581        }
582    }
583
584    pub async fn wait(&mut self) -> Result<Value> {
585        let receiver = self
586            .receiver
587            .take()
588            .ok_or(Error::Protocol("call has already been waited"))?;
589        let response = match tokio::time::timeout_at(self.deadline, receiver).await {
590            Ok(Ok(response)) => response,
591            Ok(Err(_)) => {
592                self.terminal = true;
593                self._permit.take();
594                return Err(Error::Closed);
595            }
596            Err(_) => {
597                if let Some(client) = self.client.upgrade() {
598                    abandon_request(&client, self.id);
599                    try_queue_cancel(&client, self.id);
600                }
601                self.terminal = true;
602                self._permit.take();
603                return Err(Error::DeadlineExceeded);
604            }
605        };
606        self.terminal = true;
607        self._permit.take();
608        response_value(response)
609    }
610}
611
612impl Drop for Call {
613    fn drop(&mut self) {
614        if self.terminal {
615            return;
616        }
617        if let Some(client) = self.client.upgrade() {
618            abandon_request(&client, self.id);
619            try_queue_cancel(&client, self.id);
620        }
621    }
622}
623
624async fn queue_cancel(inner: &Arc<Inner>, id: RequestId) -> Result<()> {
625    let notification = Notification::new(
626        rpc::CANCEL,
627        serde_json::to_value(CancelParams { id })
628            .map_err(|_| Error::Protocol("failed to serialize cancellation"))?,
629    )?;
630    let payload = Zeroizing::new(Envelope::Notification(notification).to_vec()?);
631    let limit = inner.max_frame_bytes.load(Ordering::Acquire);
632    inner
633        .writer
634        .send(WriterCommand::Frame { payload, limit })
635        .await
636        .map_err(|_| Error::Closed)
637}
638
639fn try_queue_cancel(inner: &Arc<Inner>, id: RequestId) {
640    let notification = serde_json::to_value(CancelParams { id })
641        .ok()
642        .and_then(|params| Notification::new(rpc::CANCEL, params).ok())
643        .and_then(|notification| Envelope::Notification(notification).to_vec().ok())
644        .map(Zeroizing::new);
645    if let Some(payload) = notification {
646        let limit = inner.max_frame_bytes.load(Ordering::Acquire);
647        let _ = inner
648            .writer
649            .try_send(WriterCommand::Frame { payload, limit });
650    }
651}
652
653/// Accept one inbound callback and answer it on its own task.
654///
655/// Returns `false` for a request this session must not accept at all: the
656/// server reusing an inbound ID, naming a method the client never advertised,
657/// or exceeding the negotiated active or bounded session-ID limits. Each means
658/// the peer is not tracking the session state this side is, which is a protocol
659/// violation rather than an application error.
660///
661/// Completed IDs remain in a bounded session set so reuse is still rejected
662/// after a callback finishes. Active callbacks are bounded independently by
663/// the negotiated in-flight limit.
664fn serve_callback(inner: &Arc<Inner>, handler: Arc<dyn CallbackHandler>, request: Request) -> bool {
665    if !inner.advertised.contains(&request.method) {
666        return false;
667    }
668    let Some(parent_id) = request.meta.parent_request_id else {
669        return false;
670    };
671    let parent_cancellation = {
672        // Hold the parent table while reserving the inbound slot. A terminal
673        // response cannot remove the parent between validation and attaching
674        // the callback's cancellation state.
675        let pending = lock_unpoisoned(&inner.pending);
676        let Some(parent) = pending.get(&parent_id) else {
677            return false;
678        };
679        if request.deadline_unix_ms() > parent.deadline_unix_ms {
680            return false;
681        }
682        let mut inbound = lock_unpoisoned(&inner.inbound);
683        let active_limit = inner.max_in_flight.load(Ordering::Acquire);
684        if inbound.active >= active_limit
685            || inbound.last_seen_id.is_some_and(|last| request.id <= last)
686        {
687            return false;
688        }
689        inbound.last_seen_id = Some(request.id);
690        inbound.active += 1;
691        parent.cancellation.clone()
692    };
693    let deadline = instant_from_unix_ms(request.deadline_unix_ms());
694    let task_inner = Arc::downgrade(inner);
695    tokio::spawn(async move {
696        let mut outcome = tokio::select! {
697            biased;
698            _ = parent_cancellation.cancelled() => Err(RpcError::new(ErrorKind::Cancelled)),
699            outcome = tokio::time::timeout_at(
700                deadline,
701                handler.call(&request.method, request.params),
702            ) => outcome.unwrap_or_else(|_| Err(RpcError::new(ErrorKind::DeadlineExceeded))),
703        };
704        // A terminal parent may race a ready handler. Cancellation wins and
705        // any returned secret is scrubbed before it can be serialized.
706        if parent_cancellation.is_cancelled() {
707            if let Ok(value) = &mut outcome {
708                zeroize_json(value);
709            }
710            outcome = Err(RpcError::new(ErrorKind::Cancelled));
711        }
712        let mut response = match outcome {
713            Ok(result) => Response::success(request.id, result),
714            Err(error) => Response::error(Some(request.id), error),
715        };
716        let Some(inner) = task_inner.upgrade() else {
717            zeroize_response(&mut response);
718            return;
719        };
720        {
721            let mut inbound = lock_unpoisoned(&inner.inbound);
722            inbound.active = inbound.active.saturating_sub(1);
723        }
724        let limit = inner.max_frame_bytes.load(Ordering::Acquire);
725        let encoded = serde_json::to_vec(&response).map(Zeroizing::new);
726        zeroize_response(&mut response);
727        let payload = match encoded {
728            Ok(payload) if payload.len() <= limit => payload,
729            // An answer that cannot fit still owes the server one terminal
730            // frame, or its callback would hang until the deadline.
731            _ => {
732                let replacement =
733                    Response::error(Some(request.id), RpcError::new(ErrorKind::MessageTooLarge));
734                match Envelope::Response(replacement).to_vec().map(Zeroizing::new) {
735                    Ok(payload) => payload,
736                    Err(_) => return,
737                }
738            }
739        };
740        let send = inner.writer.send(WriterCommand::Frame { payload, limit });
741        tokio::select! {
742            biased;
743            _ = parent_cancellation.cancelled() => {
744                // Dropping the competing send zeroizes its serialized payload.
745                // Only a value-free cancellation may be queued after the
746                // parent has become terminal.
747                let replacement =
748                    Response::error(Some(request.id), RpcError::new(ErrorKind::Cancelled));
749                if let Ok(payload) = Envelope::Response(replacement).to_vec().map(Zeroizing::new) {
750                    let _ = inner
751                        .writer
752                        .send(WriterCommand::Frame { payload, limit })
753                        .await;
754                }
755            }
756            _ = send => {}
757        }
758    });
759    true
760}
761
762fn response_value(response: Response) -> Result<Value> {
763    match response {
764        Response::Success(response) => Ok(response.result),
765        Response::Error(response) => match response.error.data.kind {
766            ErrorKind::Cancelled => Err(Error::Cancelled),
767            ErrorKind::DeadlineExceeded => Err(Error::DeadlineExceeded),
768            ErrorKind::Unavailable => Err(Error::Unavailable),
769            _ => Err(Error::Remote(response.error)),
770        },
771    }
772}
773
774fn fail_session_weak(inner: &Weak<Inner>) {
775    if let Some(inner) = inner.upgrade() {
776        fail_session(&inner);
777    }
778}
779
780fn fail_session(inner: &Arc<Inner>) {
781    inner.state.store(CLOSED, Ordering::Release);
782    for (_, pending) in lock_unpoisoned(&inner.pending).drain() {
783        pending.cancellation.cancel();
784    }
785    lock_unpoisoned(&inner.abandoned).clear();
786    inner.closed.notify_waiters();
787}
788
789fn abandon_request(inner: &Arc<Inner>, id: RequestId) {
790    // Mark while holding the pending table. The reader takes a response's
791    // pending entry before it looks for a marker, so either it already took
792    // the entry (the request is terminal and needs no marker) or it will find
793    // the marker. Marking a terminal request would leave a stale marker that
794    // counts toward the bound below and eventually fails a healthy session.
795    let overflow = {
796        let mut pending = lock_unpoisoned(&inner.pending);
797        let Some(entry) = pending.remove(&id) else {
798            return;
799        };
800        entry.cancellation.cancel();
801        let mut abandoned = lock_unpoisoned(&inner.abandoned);
802        if abandoned.len() >= MAX_ABANDONED_REQUESTS {
803            true
804        } else {
805            abandoned.insert(id);
806            false
807        }
808    };
809    if overflow {
810        // A peer that never terminates cancelled/timed-out requests cannot
811        // grow client memory without bound. Close and reconnect instead.
812        fail_session(inner);
813    }
814}
815
816fn cancel_parent_callbacks(inner: &Arc<Inner>, id: RequestId) {
817    if let Some(pending) = lock_unpoisoned(&inner.pending).get(&id) {
818        pending.cancellation.cancel();
819    }
820}
821
822/// A pending entry for a request whose frame is not queued yet. A caller that
823/// drops `start` while the writer applies backpressure, or any early return,
824/// removes the entry here: the request never reaches the peer, so no response
825/// would ever clear it.
826struct PendingRegistration<'a> {
827    inner: &'a Arc<Inner>,
828    id: Option<RequestId>,
829}
830
831impl<'a> PendingRegistration<'a> {
832    fn new(inner: &'a Arc<Inner>, id: RequestId) -> Self {
833        Self {
834            inner,
835            id: Some(id),
836        }
837    }
838
839    /// The frame is queued; its response now owns the entry.
840    fn queued(mut self) {
841        self.id = None;
842    }
843}
844
845impl Drop for PendingRegistration<'_> {
846    fn drop(&mut self) {
847        if let Some(id) = self.id {
848            remove_pending(self.inner, id);
849        }
850    }
851}
852
853fn remove_pending(inner: &Arc<Inner>, id: RequestId) {
854    if let Some(pending) = lock_unpoisoned(&inner.pending).remove(&id) {
855        pending.cancellation.cancel();
856    }
857}
858
859fn zeroize_response(response: &mut Response) {
860    if let Response::Success(response) = response {
861        zeroize_json(&mut response.result);
862    }
863}
864
865fn zeroize_json(value: &mut Value) {
866    match value {
867        Value::String(value) => value.zeroize(),
868        Value::Array(values) => values.iter_mut().for_each(zeroize_json),
869        Value::Object(values) => values.values_mut().for_each(zeroize_json),
870        _ => {}
871    }
872}
873
874fn lock_unpoisoned<T>(mutex: &StdMutex<T>) -> std::sync::MutexGuard<'_, T> {
875    mutex
876        .lock()
877        .unwrap_or_else(|poisoned| poisoned.into_inner())
878}
879
880#[cfg(test)]
881mod tests {
882    use super::*;
883    use crate::frame::read_frame;
884    use crate::protocol::Product;
885
886    /// A peer that completes initialization and then never reads again, so
887    /// the writer blocks and the queue fills.
888    async fn stalled_session() -> (Client, JoinHandle<()>) {
889        let (client_io, mut peer_io) = tokio::io::duplex(64);
890        let peer = tokio::spawn(async move {
891            let request = read_frame(&mut peer_io, ABSOLUTE_MAX_FRAME_BYTES)
892                .await
893                .unwrap()
894                .unwrap();
895            let id = serde_json::from_slice::<Value>(&request).unwrap()["id"].clone();
896            let response = serde_json::to_vec(&json!({
897                "jsonrpc": "2.0",
898                "id": id,
899                "result": {
900                    "protocol": "secretspec.resolver",
901                    "version": 1,
902                    "server": {"name": "stalled", "version": "1"},
903                    "methods": ["resolver.get"],
904                    "capabilities": {},
905                    "limits": {"max_frame_bytes": 4096, "max_in_flight": 1},
906                    "application": {}
907                }
908            }))
909            .unwrap();
910            write_frame(&mut peer_io, &response, ABSOLUTE_MAX_FRAME_BYTES)
911                .await
912                .unwrap();
913            std::future::pending::<()>().await;
914        });
915        let (client_read, client_write) = tokio::io::split(client_io);
916        let initialize = InitializeParams {
917            protocol: "secretspec.resolver".into(),
918            versions: vec![1],
919            client: Product {
920                name: "test".into(),
921                version: "1".into(),
922            },
923            limits: Limits {
924                max_frame_bytes: 4096,
925                max_in_flight: 1,
926            },
927            client_methods: Vec::new(),
928            application: json!({}),
929        };
930        let (client, _) = Client::connect::<_, _, _, Value>(
931            client_read,
932            client_write,
933            initialize,
934            crate::deadline_unix_ms_after(Duration::from_secs(300)),
935        )
936        .await
937        .unwrap();
938        (client, peer)
939    }
940
941    #[tokio::test]
942    async fn a_start_dropped_under_backpressure_leaves_no_pending_request() {
943        let (client, peer) = stalled_session().await;
944        let deadline = crate::deadline_unix_ms_after(Duration::from_secs(300));
945        let params = json!({"padding": "x".repeat(3000)});
946        // Two abandoned calls and their cancellations fill the writer queue
947        // behind a frame the stalled peer never reads.
948        for _ in 0..2 {
949            let call = client
950                .start("resolver.get", &params, deadline)
951                .await
952                .unwrap();
953            drop(call);
954        }
955        let blocked = client.start("resolver.get", &params, deadline);
956        tokio::select! {
957            biased;
958            _ = blocked => panic!("the writer queue should be full"),
959            _ = std::future::ready(()) => {}
960        }
961        assert!(lock_unpoisoned(&client.inner.pending).is_empty());
962        peer.abort();
963    }
964
965    #[tokio::test]
966    async fn abandoning_an_answered_request_leaves_no_marker() {
967        let (client, peer) = stalled_session().await;
968        // The reader already delivered this request's response and removed
969        // its entry by the time the local deadline fires.
970        abandon_request(&client.inner, RequestId::new(99).unwrap());
971        assert!(lock_unpoisoned(&client.inner.abandoned).is_empty());
972        peer.abort();
973    }
974}