Skip to main content

base_http_client/
connection.rs

1// Released under the MIT License.
2// Copyright, 2026, by Samuel Williams.
3
4use crate::{
5    Endpoint, Error, Options, ResponseBody,
6    body::Consumption,
7    driver::{Assigned, Operation, Queue, checked, linked, lock},
8    outgoing::Interim,
9    writable::{
10        COMPLETE, ExchangeCompletion, FINALIZE, Open, OpenFactory, Opening, Return, ReturnTo,
11    },
12};
13use base_endpoint::{
14    AddressEndpoint, ConnectEndpoint,
15    stream::{ReadableStream, WritableStream},
16};
17use base_http::{
18    InterimResponses, Response,
19    body::{BodyError, BodyReader, ReadableBody},
20};
21use base_http1::{RequestWriter, ResponseReader};
22use base_pool::{Admission, MultiplexBuildPermit, MultiplexResource};
23use base_scheduler::{
24    Cancellation, OperationError, OperationResult, SocketClose, SocketConnect, SocketOpen,
25    SocketSetOption, SocketStreamReading, SocketStreamWriting, TcpNoDelay,
26};
27use event_listener::Event;
28use futures_channel::oneshot;
29use futures_util::future::join;
30use std::sync::{
31    Arc, Mutex, Weak,
32    atomic::{AtomicBool, Ordering},
33};
34
35struct Inner<'scope> {
36    assigned: Mutex<Option<Assigned<'scope>>>,
37    direct: Mutex<Option<Arc<dyn DirectConnection<'scope> + 'scope>>>,
38    changed: Event,
39    stop: Cancellation,
40    result: Mutex<Option<Result<(), Error>>>,
41}
42
43/// An established HTTP/1 session retaining its transport and codec buffers.
44/// Direct requests take and return its directions in the calling task;
45/// supervised work handles construction, queued calls and retirement.
46#[derive(Clone)]
47pub struct Connection<'scope> {
48    inner: Arc<Inner<'scope>>,
49}
50impl<'scope> Connection<'scope> {
51    fn new() -> Self {
52        Self {
53            inner: Arc::new(Inner {
54                assigned: Mutex::new(None),
55                direct: Mutex::new(None),
56                changed: Event::new(),
57                stop: Cancellation::new(),
58                result: Mutex::new(None),
59            }),
60        }
61    }
62    pub(crate) fn clone_handle(&self) -> Self {
63        self.clone()
64    }
65    pub(crate) fn submit(&self, assigned: Assigned<'scope>) {
66        let mut slot = lock(&self.inner.assigned);
67        assert!(slot.is_none(), "HTTP/1 admits one exchange at a time");
68        *slot = Some(assigned);
69        drop(slot);
70        self.inner.changed.notify(usize::MAX);
71    }
72    pub(crate) fn open(&self, opening: Open<'scope>) -> Opening<'scope> {
73        // Admission is published only after the session installs its direct state, which it never removes.
74        let direct = lock(&self.inner.direct)
75            .clone()
76            .expect("an admitted connection has direct state");
77        direct.open(opening)
78    }
79    /// Check an admitted idle connection before its exchange begins. A failed
80    /// probe retires the registration; the caller completes its use and
81    /// acquires again, since no request bytes have been sent.
82    pub(crate) fn probe(&self) -> bool {
83        let direct = lock(&self.inner.direct)
84            .clone()
85            .expect("an admitted connection has direct state");
86        direct.probe()
87    }
88    async fn next(&self, lifetime: &Cancellation) -> Option<Assigned<'scope>> {
89        loop {
90            event_listener::listener!(self.inner.changed => listener);
91            if let Some(assigned) = lock(&self.inner.assigned).take() {
92                return Some(assigned);
93            }
94            if checked(
95                checked(
96                    async {
97                        listener.await;
98                        Ok(())
99                    },
100                    &self.inner.stop,
101                ),
102                lifetime,
103            )
104            .await
105            .is_err()
106            {
107                return None;
108            }
109        }
110    }
111    pub(crate) fn stop(&self) {
112        self.inner.stop.cancel();
113    }
114    pub(crate) async fn wait_closed(&self) -> Result<(), Error> {
115        loop {
116            event_listener::listener!(self.inner.changed => listener);
117            if let Some(result) = lock(&self.inner.result).clone() {
118                return result;
119            }
120            listener.await;
121        }
122    }
123    fn finish(&self, result: Result<(), Error>) {
124        let mut current = lock(&self.inner.result);
125        if current.is_none() {
126            *current = Some(result);
127        }
128        drop(current);
129        self.inner.changed.notify(usize::MAX);
130    }
131}
132
133struct Owner<'scope> {
134    connection: Connection<'scope>,
135    registration: Option<MultiplexResource<Connection<'scope>>>,
136}
137impl Drop for Owner<'_> {
138    fn drop(&mut self) {
139        if let Some(registration) = &self.registration {
140            registration.retire();
141        }
142        let abandoned = lock(&self.connection.inner.assigned).take();
143        drop(abandoned);
144        self.connection.finish(Err(Error::Closed));
145    }
146}
147
148#[allow(clippy::too_many_arguments)]
149pub(crate) async fn run<'scope, S>(
150    scheduler: &'scope S,
151    endpoint: Endpoint,
152    options: Options,
153    build: MultiplexBuildPermit<Connection<'scope>>,
154    cancellation: Cancellation,
155    lifetime: Cancellation,
156    ready: oneshot::Sender<Result<(), Error>>,
157    queue: Weak<Queue<'scope>>,
158) where
159    S: SocketOpen
160        + SocketConnect
161        + SocketClose
162        + SocketSetOption<TcpNoDelay>
163        + SocketStreamReading
164        + SocketStreamWriting,
165    S::Socket: 'static,
166{
167    let connection = Connection::new();
168    let mut owner = Owner {
169        connection: connection.clone(),
170        registration: None,
171    };
172    let mut build = Some(build);
173    let mut ready = Some(ready);
174    let result = async {
175        let address = endpoint.address;
176        let endpoint_transport = TcpEndpoint {
177            address: AddressEndpoint::new(address),
178            nodelay: options.tcp_nodelay,
179        };
180        #[cfg(feature = "rustls")]
181        if let Some(tls) = endpoint.tls {
182            use base_endpoint::tls::{TlsClientEndpoint, TlsSession, rustls::RustlsConnector};
183            let mut config = (*tls.config).clone();
184            config.alpn_protocols = options.protocols.alpn_protocols();
185            let endpoint = TlsClientEndpoint::new(
186                endpoint_transport,
187                RustlsConnector::new(Arc::new(config)),
188                tls.server_name,
189            );
190            let transport = endpoint
191                .connect(scheduler, &cancellation)
192                .await
193                .map_err(|error| Error::connect(address, error))?;
194            let selected = transport.negotiated_protocol().map(<[u8]>::to_vec);
195            return transport
196                .with_duplex(scheduler, &lifetime, |reader, writer| async {
197                    let protocol = options.protocols.negotiated(selected.as_deref())?;
198                    let (input, output) = protocol
199                        .client(reader, writer, options.http1.clone())
200                        .expect("Client::new validated the HTTP/1 options");
201                    session(
202                        input,
203                        output,
204                        &mut owner,
205                        &mut build,
206                        &mut ready,
207                        &options,
208                        &cancellation,
209                        &lifetime,
210                        queue.clone(),
211                    )
212                    .await
213                })
214                .await;
215        }
216        let socket = endpoint_transport
217            .connect(scheduler, &cancellation)
218            .await
219            .map_err(|error| Error::connect(address, error))?;
220        let initialized = async {
221            let mut reader = scheduler.socket_stream_reader(&socket)?;
222            let writer = match scheduler.socket_stream_writer(&socket) {
223                Ok(writer) => writer,
224                Err(error) => {
225                    let _ = ReadableStream::close(&mut reader).await;
226                    return Err(Error::from(error));
227                }
228            };
229            Ok::<_, Error>(
230                options
231                    .protocols
232                    .plaintext()
233                    .client(reader, writer, options.http1.clone())
234                    .expect("Client::new validated the HTTP/1 options"),
235            )
236        }
237        .await;
238        let result = match initialized {
239            Ok((input, output)) => {
240                session(
241                    input,
242                    output,
243                    &mut owner,
244                    &mut build,
245                    &mut ready,
246                    &options,
247                    &cancellation,
248                    &lifetime,
249                    queue.clone(),
250                )
251                .await
252            }
253            Err(error) => Err(error),
254        };
255        let closed = scheduler.socket_close(socket).await;
256        result?;
257        closed?;
258        Ok(())
259    }
260    .await;
261    // Failed construction remains charged through socket/lane cleanup.
262    drop(build);
263    if let Some(ready) = ready {
264        let _ = ready.send(Err(result.as_ref().err().cloned().unwrap_or(Error::Closed)));
265    }
266    connection.finish(result);
267}
268
269/// A TCP connection, configured before any plaintext or TLS bytes are sent.
270struct TcpEndpoint {
271    address: AddressEndpoint,
272    nodelay: bool,
273}
274impl<S> ConnectEndpoint<S> for TcpEndpoint
275where
276    S: SocketOpen + SocketConnect + SocketClose + SocketSetOption<TcpNoDelay>,
277{
278    type Connection = S::Socket;
279
280    async fn connect(
281        &self,
282        scheduler: &S,
283        cancellation: &Cancellation,
284    ) -> OperationResult<S::Socket> {
285        let socket = self.address.connect(scheduler, cancellation).await?;
286        if self.nodelay {
287            // A latency hint only: the connection remains usable without it.
288            // Cancellation still ends establishment without further work.
289            let result = scheduler
290                .socket_set_option(&socket, TcpNoDelay, true, cancellation)
291                .await;
292            if let Err(error @ OperationError::Cancelled(_)) = result {
293                let _ = scheduler.socket_close(socket).await;
294                return Err(error);
295            }
296        }
297        Ok(socket)
298    }
299}
300
301// A connection retains its typed HTTP buffers and transport directions. Only
302// an admitted exchange may take them; short locks protect transfer, never I/O.
303trait DirectConnection<'scope>: Send + Sync {
304    fn open(self: Arc<Self>, opening: Open<'scope>) -> Opening<'scope>;
305    fn probe(&self) -> bool;
306}
307struct Codecs<R, W> {
308    input: Option<ResponseReader<R>>,
309    output: Option<RequestWriter<W>>,
310}
311struct Http1Connection<'scope, R, W> {
312    codecs: Mutex<Codecs<R, W>>,
313    changed: Event,
314    closing: AtomicBool,
315    registration: MultiplexResource<Connection<'scope>>,
316    queue: Weak<Queue<'scope>>,
317    open: OpenFactory<'scope, R, W>,
318}
319impl<R: ReadableStream, W: WritableStream> Http1Connection<'_, R, W> {
320    fn take(&self) -> Result<(ResponseReader<R>, RequestWriter<W>), Error> {
321        let mut codecs = lock(&self.codecs);
322        if self.closing.load(Ordering::Acquire) || codecs.input.is_none() || codecs.output.is_none()
323        {
324            return Err(Error::Closed);
325        }
326        Ok((codecs.input.take().unwrap(), codecs.output.take().unwrap()))
327    }
328    fn returned(&self) {
329        if self.closing.load(Ordering::Acquire) {
330            self.changed.notify(usize::MAX);
331        }
332    }
333    async fn close(&self) -> Result<(), Error> {
334        self.closing.store(true, Ordering::Release);
335        let (mut input, mut output) = loop {
336            event_listener::listener!(self.changed => listener);
337            let returned = {
338                let mut codecs = lock(&self.codecs);
339                if codecs.input.is_some() && codecs.output.is_some() {
340                    Some((codecs.input.take().unwrap(), codecs.output.take().unwrap()))
341                } else {
342                    None
343                }
344            };
345            if let Some(codecs) = returned {
346                break codecs;
347            }
348            listener.await;
349        };
350        let a = input.close().await;
351        let b = output.close().await;
352        drop(input);
353        drop(output);
354        a?;
355        b?;
356        Ok(())
357    }
358}
359impl<'scope, R: ReadableStream + 'scope, W: WritableStream + 'scope> DirectConnection<'scope>
360    for Http1Connection<'scope, R, W>
361{
362    fn open(self: Arc<Self>, opening: Open<'scope>) -> Opening<'scope> {
363        let (input, output) = match self.take() {
364            Ok(codecs) => codecs,
365            Err(error) => {
366                self.registration.retire();
367                opening.active.complete();
368                return crate::writable::failed_open(error);
369            }
370        };
371        let completion = ExchangeCompletion::new(
372            opening.active,
373            opening.pending,
374            opening.cancellation.clone(),
375            self.registration.clone(),
376        );
377        // Construct guards before the future: even dropping an unpolled open
378        // returns both directions and acknowledges their retirement obligation.
379        let input = Return::new(input, completion.clone(), self.clone(), false);
380        let output = Return::new(output, completion, self.clone(), true);
381        (self.open)(
382            input,
383            output,
384            opening.metadata,
385            opening.interim,
386            opening.lifetime,
387        )
388    }
389    fn probe(&self) -> bool {
390        // Probe outside the lock: transfer is short, and the probe performs I/O.
391        let viable = match self.take() {
392            Ok((mut input, output)) => {
393                let viable = input.probe_viable();
394                {
395                    let mut codecs = lock(&self.codecs);
396                    codecs.input = Some(input);
397                    codecs.output = Some(output);
398                }
399                self.returned();
400                viable
401            }
402            Err(_) => false,
403        };
404        if !viable {
405            self.registration.retire();
406        }
407        viable
408    }
409}
410impl<'scope, R: ReadableStream + 'scope, W: WritableStream + 'scope>
411    ReturnTo<'scope, ResponseReader<R>> for Http1Connection<'scope, R, W>
412{
413    fn give_back(
414        self: Arc<Self>,
415        input: ResponseReader<R>,
416        mode: u8,
417        completion: Arc<ExchangeCompletion<'scope>>,
418    ) {
419        let reusable = mode == COMPLETE && input.is_reusable();
420        lock(&self.codecs).input = Some(input);
421        self.returned();
422        completion.returned(1, reusable, None);
423    }
424}
425impl<'scope, R: ReadableStream + 'scope, W: WritableStream + 'scope>
426    ReturnTo<'scope, RequestWriter<W>> for Http1Connection<'scope, R, W>
427{
428    fn give_back(
429        self: Arc<Self>,
430        output: RequestWriter<W>,
431        mode: u8,
432        completion: Arc<ExchangeCompletion<'scope>>,
433    ) {
434        if mode == FINALIZE {
435            // The exchange's pending admission retains the queue until both directions return.
436            let queue = self
437                .queue
438                .upgrade()
439                .expect("an unfinished exchange retains its queue");
440            let cancellation = completion.cancellation().clone();
441            let mut owner = Return::new(output, completion, self, true);
442            let work = Box::pin(async move {
443                let result = owner
444                    .value
445                    .as_mut()
446                    .unwrap()
447                    .write_frame(base_http::body::BodyFrame::<Vec<u8>>::End, &cancellation)
448                    .await;
449                match result {
450                    Ok(()) => owner.complete(),
451                    Err(error) => owner.report_error(error.into()),
452                }
453                // Failure/drop returns the poisoned writer through the guard.
454            });
455            // An abandoned driver drops the work, whose guard abandons the output.
456            let _ = queue.push_work(work);
457            return;
458        }
459        let reusable = mode == COMPLETE && output.is_reusable();
460        lock(&self.codecs).output = Some(output);
461        self.returned();
462        completion.returned(
463            2,
464            reusable,
465            Some(if mode == COMPLETE {
466                Ok(())
467            } else {
468                Err(Error::Abandoned)
469            }),
470        );
471    }
472}
473
474#[allow(clippy::too_many_arguments)]
475async fn session<'scope, R, W>(
476    mut input: ResponseReader<R>,
477    mut output: RequestWriter<W>,
478    owner: &mut Owner<'scope>,
479    build: &mut Option<MultiplexBuildPermit<Connection<'scope>>>,
480    ready: &mut Option<oneshot::Sender<Result<(), Error>>>,
481    options: &Options,
482    cancellation: &Cancellation,
483    lifetime: &Cancellation,
484    queue: Weak<Queue<'scope>>,
485) -> Result<(), Error>
486where
487    R: ReadableStream + 'scope,
488    W: WritableStream + 'scope,
489{
490    if cancellation.is_cancelled() {
491        let _ = input.close().await;
492        let _ = output.close().await;
493        return Err(Error::Cancelled);
494    }
495    // Set the direct interface before publication; no admitted caller can see
496    // a connection without its codec storage.
497    let Ok(registration) = build
498        .take()
499        .unwrap()
500        .commit(owner.connection.clone(), Admission::concurrent(0))
501    else {
502        // Commit fails only once shutdown has closed the pool, which queues this connection for retirement. Its waiter sees the closed client, and only a failed close is a driver failure.
503        let ready = ready.take().expect("a session reports readiness once");
504        let _ = ready.send(Err(Error::Closed));
505        let input = input.close().await;
506        let output = output.close().await;
507        input?;
508        output?;
509        return Ok(());
510    };
511    owner.registration = Some(registration.clone());
512    let state = Arc::new(Http1Connection {
513        codecs: Mutex::new(Codecs {
514            input: Some(input),
515            output: Some(output),
516        }),
517        changed: Event::new(),
518        closing: AtomicBool::new(false),
519        registration,
520        queue,
521        open: crate::writable::factory(),
522    });
523    *lock(&owner.connection.inner.direct) = Some(state.clone());
524    // A registration that is already draining publishes no admission; its retirement stops the session below.
525    state.registration.update(Admission::concurrent(1));
526    // Construction runs one session, and reports readiness itself only if the session never does.
527    let ready = ready.take().expect("a session reports readiness once");
528    let _ = ready.send(Ok(()));
529    // Only queued pull-body calls run here. Imperative open/write/read acquires the retained state directly on the calling task.
530    while let Some(Assigned {
531        mut operation,
532        active,
533    }) = owner.connection.next(lifetime).await
534    {
535        // Direct exchanges restore both directions before releasing their use, and closing begins only after this loop.
536        let (mut input, mut output) = state
537            .take()
538            .expect("an admitted exchange finds both codec directions");
539        let local = operation.cancellation.clone();
540        let caller = operation.lifetime.clone();
541        let outcome = linked(
542            exchange(&mut input, &mut output, &mut operation, options),
543            &local,
544            &caller,
545            lifetime,
546        )
547        .await;
548        operation.request.body.close(None);
549        if let Some(reply) = operation.reply.take() {
550            let _ = reply.send(Err(outcome
551                .as_ref()
552                .err()
553                .cloned()
554                .unwrap_or(Error::Closed)));
555        }
556        let reusable = matches!(outcome, Ok(true));
557        if !reusable {
558            state.registration.retire();
559        }
560        {
561            let mut codecs = lock(&state.codecs);
562            codecs.input = Some(input);
563            codecs.output = Some(output);
564        }
565        drop(operation);
566        active.complete();
567        if !reusable {
568            break;
569        }
570    }
571    state.registration.retire();
572    state.close().await
573}
574
575async fn exchange<R: ReadableStream, W: WritableStream>(
576    input: &mut ResponseReader<R>,
577    output: &mut RequestWriter<W>,
578    operation: &mut Operation<'_>,
579    options: &Options,
580) -> Result<bool, Error> {
581    let Operation {
582        request,
583        interim,
584        reply,
585        cancellation,
586        ..
587    } = operation;
588    let metadata = request.metadata.clone();
589    let headers = AtomicBool::new(false);
590    let send = async {
591        let result = checked(
592            async {
593                output
594                    .write_request(request, cancellation)
595                    .await
596                    .map_err(Error::from)
597            },
598            cancellation,
599        )
600        .await;
601        // Preserve a readable final response when the peer refuses further
602        // upload. The connection is retired even if that response succeeds.
603        if result.is_err() && !headers.load(Ordering::Acquire) {
604            cancellation.cancel();
605        }
606        result
607    };
608    let receive = async {
609        let mut writer = None;
610        let result = checked(
611            async {
612                let mut observer = InterimResponses::new(Interim(interim.as_mut()));
613                let mut response = input
614                    .read_response(&metadata, &mut observer, cancellation)
615                    .await?;
616                let consumption = Consumption::new(cancellation.clone());
617                let empty = response.body.is_empty();
618                let (reader, mut producer) = BodyReader::bounded(
619                    options.response_buffer_bytes,
620                    options.response_buffer_frames,
621                );
622                // Publish known-empty input as completed before handing it to
623                // another task. Its consumer may drop it immediately, without
624                // racing a redundant End write or retiring a healthy session.
625                if empty {
626                    consumption.complete();
627                    producer.close_write(None);
628                }
629                let body = ResponseBody {
630                    reader,
631                    consumption: consumption.clone(),
632                    length: response.body.length(),
633                    trailers: response.body.may_have_trailers(),
634                };
635                writer = Some(producer);
636                let returned = Response::from_parts(response.metadata, body);
637                if reply.take().unwrap().send(Ok(returned)).is_err() {
638                    return Err(Error::Abandoned);
639                }
640                headers.store(true, Ordering::Release);
641                if !empty {
642                    loop {
643                        let frame = response.body.read(cancellation).await?;
644                        let end = frame.is_end();
645                        writer
646                            .as_mut()
647                            .unwrap()
648                            .write_frame(frame, cancellation)
649                            .await?;
650                        if end {
651                            break;
652                        }
653                    }
654                }
655                consumption.wait().await
656            },
657            cancellation,
658        )
659        .await;
660        if let Err(error) = &result {
661            if let Some(writer) = &mut writer {
662                writer.close_write(Some(match error {
663                    Error::Body(error) => error.clone(),
664                    error => BodyError::new(error.clone()),
665                }));
666            }
667            cancellation.cancel();
668        }
669        result
670    };
671    let (sent, received) = join(send, receive).await;
672    match (sent, received) {
673        (Ok(persistent), Ok(())) => Ok(persistent && input.is_reusable()),
674        (Err(_), Ok(())) => Ok(false),
675        (Err(error), Err(Error::Cancelled)) if !matches!(error, Error::Cancelled) => Err(error),
676        (_, Err(error)) => Err(error),
677    }
678}
679
680#[cfg(test)]
681mod tests {
682    use super::*;
683    use crate::{Client, driver::Pending, outgoing::tests::ignore};
684    use base_http::{Method, RequestMetadata};
685    use base_scheduler_native::NativeScheduler;
686    use futures_lite::future::{block_on, yield_now, zip};
687    use std::{io::Read, net::TcpListener, thread};
688
689    #[test]
690    fn opening_an_admitted_use_after_the_session_closes_fails_and_releases_it() {
691        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
692        let address = listener.local_addr().unwrap();
693        let peer = thread::spawn(move || {
694            let (mut stream, _) = listener.accept().unwrap();
695            let mut bytes = Vec::new();
696            stream.read_to_end(&mut bytes).unwrap();
697            assert!(bytes.is_empty());
698        });
699        let scheduler = NativeScheduler::new().unwrap();
700        let (client, driver) =
701            Client::new(&scheduler, Endpoint::new(address), Options::default()).unwrap();
702        let lifetime = Cancellation::new();
703        let application = async {
704            let active = client
705                .context
706                .acquire(&Cancellation::never())
707                .await
708                .unwrap();
709            let connection = active.clone_handle();
710            let pending = Pending::new(client.queue.clone()).unwrap();
711            // Another thread can close the session between admission and opening its codecs.
712            lifetime.cancel();
713            while lock(&connection.inner.result).is_none() {
714                yield_now().await;
715            }
716            assert!(!connection.probe());
717            let opening = Open {
718                metadata: RequestMetadata::new(Method::GET),
719                interim: Box::new(ignore),
720                active,
721                pending,
722                cancellation: Cancellation::new(),
723                lifetime: Cancellation::never(),
724            };
725            let error = connection.open(opening).await.err().unwrap();
726            assert_eq!(error.to_string(), Error::Closed.to_string());
727        };
728        let (result, ()) = block_on(zip(driver.run(&lifetime), application));
729        result.unwrap();
730        assert_eq!(client.pool_stats().total(), 0);
731        peer.join().unwrap();
732        block_on(scheduler.shutdown());
733    }
734}