Skip to main content

ergon_io/
http.rs

1//! HTTP/1.1 client: one keep-alive connection.
2//!
3//! - [`HttpDial`] sets the connection up without blocking.
4//! - [`HttpConn::request`] writes a request straight into one TLS record: one
5//!   `send`, one TCP segment for a small request.
6//! - Responses are parsed in place: a `Content-Length` body is a slice of the
7//!   receive buffer. Responses match requests in order.
8//! - One request in flight by default ([`HttpOptions::max_in_flight`] 1). A
9//!   second [`HttpConn::request`] before the response returns
10//!   [`Error::Busy`]: no hidden queue.
11//! - A request past [`HttpOptions::request_timeout_ms`] closes its connection
12//!   ([`Handler::on_closed`] with [`Error::Timeout`]): HTTP/1.1 cannot drop
13//!   one request on a live connection.
14//! - A connection that closes with requests in flight reports
15//!   [`Error::Unanswered`]: the cause, and the first request whose TLS
16//!   record never wholly reached the kernel (it, and every later one,
17//!   cannot have run). Cost: one add per `send`, one store per request.
18//! - Raising `max_in_flight` turns on pipelining. Off by default: not every
19//!   server or CDN handles it, and one slow response delays every request
20//!   behind it.
21//! - Several requests at once: keep a small pool of warm connections, each
22//!   with nothing in flight ([`HttpConn::in_flight`] 0), or use
23//!   `HttpClient`, which does this. A WebSocket API (many requests on one
24//!   connection, matched by id) is the other option.
25
26use std::collections::VecDeque;
27use std::net::SocketAddr;
28use std::sync::Arc;
29
30use rustls::ClientConfig;
31
32use crate::RxMeta;
33use crate::clock::monotonic_ns;
34use crate::endpoint::sealed::{Deliver, DialIo, Io};
35use crate::endpoint::{ConnId, ConnMut, Handler};
36use crate::error::{Error, Result};
37use crate::net::{Target, TlsDial};
38use crate::stream::TlsStream;
39use crate::tls::MAX_RECORD;
40
41/// Largest request (line, headers and body) [`HttpConn::request`] accepts: one
42/// TLS record of plaintext.
43pub const MAX_REQUEST: usize = 1 << 14;
44const MAX_HEADERS: usize = 64;
45/// Upper bound for [`HttpOptions::max_in_flight`].
46pub const MAX_IN_FLIGHT: usize = 1024;
47
48/// Header names the client manages itself and callers may not set.
49const RESERVED: [&str; 8] = [
50    "content-length",
51    "transfer-encoding",
52    "host",
53    "connection",
54    "te",
55    "upgrade",
56    "trailer",
57    "expect",
58];
59
60/// RFC 9110 token characters.
61pub(crate) const fn is_tchar(b: u8) -> bool {
62    b.is_ascii_alphanumeric()
63        || matches!(
64            b,
65            b'!' | b'#'
66                | b'$'
67                | b'%'
68                | b'&'
69                | b'\''
70                | b'*'
71                | b'+'
72                | b'-'
73                | b'.'
74                | b'^'
75                | b'_'
76                | b'`'
77                | b'|'
78                | b'~'
79        )
80}
81
82fn validate_header(name: &str, value: &str) -> Result<()> {
83    if name.is_empty() || !name.bytes().all(is_tchar) {
84        return Err(Error::Http(format!("invalid header name {name:?}")));
85    }
86    if RESERVED.iter().any(|r| name.eq_ignore_ascii_case(r)) {
87        return Err(Error::Http(format!("header {name} is set by the client")));
88    }
89    // Field values: visible ASCII, space, tab, or obs-text; never CR, LF or NUL.
90    if value.bytes().any(|b| b == b'\r' || b == b'\n' || b == 0) {
91        return Err(Error::Http(format!("invalid value for header {name}")));
92    }
93    Ok(())
94}
95
96pub(crate) fn validate_request(method: &str, path: &str, headers: &[(&str, &str)]) -> Result<()> {
97    if method.is_empty() || !method.bytes().all(is_tchar) {
98        return Err(Error::Http(format!("invalid method {method:?}")));
99    }
100    if path.is_empty() || path.bytes().any(|b| b <= b' ' || b == 0x7F) {
101        return Err(Error::Http("invalid request target".into()));
102    }
103    for (k, v) in headers {
104        validate_header(k, v)?;
105    }
106    Ok(())
107}
108
109/// Whether a response ends the connection: a `close` token in any
110/// `Connection` header, or HTTP/1.0 without `keep-alive`.
111fn closes(version: Option<u8>, headers: &[httparse::Header<'_>]) -> bool {
112    let token = |t: &[u8]| {
113        headers
114            .iter()
115            .filter(|h| h.name.eq_ignore_ascii_case("connection"))
116            .flat_map(|h| h.value.split(|&b| b == b','))
117            .any(|v| v.trim_ascii().eq_ignore_ascii_case(t))
118    };
119    token(b"close") || (version == Some(0) && !token(b"keep-alive"))
120}
121
122/// A response, borrowed from the receive buffer for the duration of
123/// [`HttpHandler::on_response`].
124#[derive(Debug)]
125pub struct Response<'a> {
126    /// Status code.
127    pub status: u16,
128    /// Headers in arrival order.
129    pub headers: &'a [httparse::Header<'a>],
130    /// The body (de-chunked if the server used chunked encoding).
131    pub body: &'a [u8],
132}
133
134impl Response<'_> {
135    /// The first header named `name` (ASCII case-insensitive).
136    #[must_use]
137    pub fn header(&self, name: &str) -> Option<&[u8]> {
138        self.headers
139            .iter()
140            .find(|h| h.name.eq_ignore_ascii_case(name))
141            .map(|h| h.value)
142    }
143}
144
145/// Receives HTTP responses.
146pub trait HttpHandler: Handler {
147    /// The response to `request` (the id [`HttpConn::request`] returned).
148    fn on_response(&mut self, conn: ConnId, request: u64, response: &Response<'_>, meta: &RxMeta);
149}
150
151/// Per-connection settings.
152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153pub struct HttpOptions {
154    /// Time from sending a request to its complete response, in milliseconds.
155    pub request_timeout_ms: u32,
156    /// Requests allowed in flight on the connection (1: no pipelining).
157    pub max_in_flight: usize,
158    /// Receive buffer size; it must hold the largest response.
159    pub buf_size: usize,
160}
161
162impl Default for HttpOptions {
163    fn default() -> Self {
164        Self {
165            request_timeout_ms: 5_000,
166            max_in_flight: 1,
167            buf_size: 1 << 20,
168        }
169    }
170}
171
172/// Counters.
173#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
174pub struct HttpStats {
175    /// Requests queued.
176    pub requests: u64,
177    /// Responses delivered.
178    pub responses: u64,
179    /// Bytes received from the kernel.
180    pub bytes_in: u64,
181    /// TLS records opened.
182    pub records: u64,
183    /// Plaintext bytes moved because a response spanned records.
184    pub span_copy_bytes: u64,
185    /// Bytes moved by buffer compaction.
186    pub compact_bytes: u64,
187    /// Bytes copied to de-chunk bodies.
188    pub chunk_copy_bytes: u64,
189}
190
191struct InFlight {
192    id: u64,
193    deadline_ns: i64,
194    head: bool,
195    /// Bytes the connection must hand to the kernel before this request's
196    /// record is wholly handed (a count since the connection opened).
197    end: u64,
198}
199
200/// A connected HTTP/1.1 client connection. Hand it to a `Driver`; the socket
201/// closes when it is dropped.
202pub struct HttpConn {
203    stream: TlsStream,
204    /// `Host` and the target's headers, serialised once.
205    fixed: Vec<u8>,
206    in_flight: VecDeque<InFlight>,
207    next_id: u64,
208    opts: HttpOptions,
209    body: Vec<u8>,
210    /// Window length needed before the front response can complete.
211    need: usize,
212    requests: u64,
213    responses: u64,
214    chunk_copy_bytes: u64,
215    /// Bytes handed to the kernel since the connection opened.
216    handed: u64,
217}
218
219impl HttpConn {
220    /// Queues a request for the driver's next `poll` or `flush` and returns its
221    /// id. `headers` come after the connection's fixed headers;
222    /// `Content-Length` is added when there is a body or the method is `POST`,
223    /// `PUT` or `PATCH`.
224    ///
225    /// # Errors
226    /// [`Error::Busy`] when `max_in_flight` requests are outstanding, or the
227    /// request is larger than [`MAX_REQUEST`] or contains CR/LF in a field.
228    pub fn request(
229        &mut self,
230        method: &str,
231        path: &str,
232        headers: &[(&str, &str)],
233        body: &[u8],
234    ) -> Result<u64> {
235        self.queue(method, path, headers, body, |_| {})
236    }
237
238    /// Builds one request into one TLS record; `query` appends to the target
239    /// right after `path`.
240    fn queue(
241        &mut self,
242        method: &str,
243        path: &str,
244        headers: &[(&str, &str)],
245        body: &[u8],
246        query: impl FnOnce(&mut Vec<u8>),
247    ) -> Result<u64> {
248        if self.in_flight.len() >= self.opts.max_in_flight {
249            return Err(Error::Busy("max_in_flight requests outstanding"));
250        }
251        validate_request(method, path, headers)?;
252        let with_length = !body.is_empty() || matches!(method, "POST" | "PUT" | "PATCH");
253        let fixed = &self.fixed;
254        self.stream.seal_build(|out| {
255            let start = out.len();
256            out.extend_from_slice(method.as_bytes());
257            out.push(b' ');
258            out.extend_from_slice(path.as_bytes());
259            query(out);
260            out.extend_from_slice(b" HTTP/1.1\r\n");
261            out.extend_from_slice(fixed);
262            for (k, v) in headers {
263                out.extend_from_slice(k.as_bytes());
264                out.extend_from_slice(b": ");
265                out.extend_from_slice(v.as_bytes());
266                out.extend_from_slice(b"\r\n");
267            }
268            if with_length {
269                out.extend_from_slice(b"Content-Length: ");
270                push_decimal(out, body.len());
271                out.extend_from_slice(b"\r\n");
272            }
273            out.extend_from_slice(b"\r\n");
274            out.extend_from_slice(body);
275            if out.len() - start > MAX_REQUEST {
276                return Err(Error::Http("request larger than one TLS record".into()));
277            }
278            Ok(())
279        })?;
280        let id = self.next_id;
281        self.next_id += 1;
282        let queued = self.stream.tx_queue().len() as u64;
283        self.in_flight.push_back(InFlight {
284            id,
285            deadline_ns: monotonic_ns() + i64::from(self.opts.request_timeout_ms) * 1_000_000,
286            head: method == "HEAD",
287            end: self.handed + queued,
288        });
289        self.requests += 1;
290        Ok(id)
291    }
292
293    /// Queues a signed request: `params` (already percent-encoded), then
294    /// `timestamp=<now, ms>` and a `signature` over exactly those bytes, in
295    /// the query string. The signature is computed into the TLS record being
296    /// built, without allocating. Set the API key header once with
297    /// `Target::with_header`.
298    ///
299    /// # Errors
300    /// As [`HttpConn::request`], or `params` needs percent-encoding.
301    pub fn signed_request(
302        &mut self,
303        method: &str,
304        path: &str,
305        params: &str,
306        headers: &[(&str, &str)],
307        signer: &crate::auth::Signer,
308    ) -> Result<u64> {
309        let now_ms = u64::try_from(crate::clock::realtime_ns() / 1_000_000).unwrap_or(0);
310        self.signed_request_at(method, path, params, headers, signer, now_ms)
311    }
312
313    /// [`HttpConn::signed_request`] with an explicit timestamp (tests, replay).
314    ///
315    /// # Errors
316    /// As [`HttpConn::signed_request`].
317    pub fn signed_request_at(
318        &mut self,
319        method: &str,
320        path: &str,
321        params: &str,
322        headers: &[(&str, &str)],
323        signer: &crate::auth::Signer,
324        timestamp_ms: u64,
325    ) -> Result<u64> {
326        crate::auth::validate_params(params)?;
327        if path.contains('?') {
328            return Err(Error::Http("put query parameters in `params`".into()));
329        }
330        self.queue(method, path, headers, &[], |out| {
331            out.push(b'?');
332            crate::auth::append_signed_params(out, params, timestamp_ms, signer);
333        })
334    }
335
336    /// Requests sent and not yet answered.
337    #[must_use]
338    pub fn in_flight(&self) -> usize {
339        self.in_flight.len()
340    }
341
342    /// Counters.
343    #[must_use]
344    pub const fn stats(&self) -> HttpStats {
345        let s = self.stream.stats();
346        HttpStats {
347            requests: self.requests,
348            responses: self.responses,
349            bytes_in: s.bytes_in,
350            records: s.records,
351            span_copy_bytes: s.span_copy_bytes,
352            compact_bytes: s.compact_bytes,
353            chunk_copy_bytes: self.chunk_copy_bytes,
354        }
355    }
356
357    /// The socket's descriptor, for socket options and diagnostics. Do not read
358    /// from or write to it: the driver owns the byte stream.
359    #[must_use]
360    pub fn raw_fd(&self) -> std::os::fd::RawFd {
361        self.stream.fd()
362    }
363
364    /// What the quick-ACK route check found for this connection
365    /// ([`crate::route::RouteCheck::Pending`] while ergon-io's warning
366    /// thread is still looking it up).
367    #[must_use]
368    pub fn route(&self) -> &crate::route::RouteCheck {
369        self.stream.route()
370    }
371
372    #[inline]
373    fn deliver<H: HttpHandler>(
374        &mut self,
375        id: ConnId,
376        n: usize,
377        meta: &RxMeta,
378        handler: &mut H,
379    ) -> Result<usize> {
380        self.stream.received(n);
381        let mut delivered = 0;
382        if n == 0 {
383            delivered += self.parse(id, meta, handler)?;
384        }
385        while self.stream.open_next()? {
386            delivered += self.parse(id, meta, handler)?;
387        }
388        self.stream.compact();
389        Ok(delivered)
390    }
391
392    /// Delivers every complete response in the window.
393    fn parse<H: HttpHandler>(
394        &mut self,
395        id: ConnId,
396        meta: &RxMeta,
397        handler: &mut H,
398    ) -> Result<usize> {
399        let mut delivered = 0;
400        loop {
401            let window = self.stream.window();
402            if window.is_empty() || window.len() < self.need {
403                return Ok(delivered);
404            }
405            self.need = 0;
406            let Some(req) = self.in_flight.front() else {
407                return Err(Error::Http("response without a request".into()));
408            };
409            let mut hdrs = [httparse::EMPTY_HEADER; MAX_HEADERS];
410            let mut resp = httparse::Response::new(&mut hdrs);
411            let hl = match resp.parse(window).map_err(|e| Error::Http(e.to_string()))? {
412                httparse::Status::Partial => return Ok(delivered),
413                httparse::Status::Complete(n) => n,
414            };
415            let status = resp.code.unwrap_or(0);
416            if (100..200).contains(&status) {
417                // Interim response (100 Continue): skip it.
418                self.stream.consume(hl);
419                continue;
420            }
421            let framing = framing(resp.headers, status, req.head)?;
422            let close = closes(resp.version, resp.headers);
423            let limit = self.opts.buf_size - 2 * MAX_RECORD;
424            let (body, total) = match framing {
425                Framing::Length(len) => {
426                    let total = hl
427                        .checked_add(len)
428                        .filter(|&t| t <= limit)
429                        .ok_or_else(|| Error::Http("response larger than the buffer".into()))?;
430                    if window.len() < total {
431                        // Skip re-parsing until the whole response is here.
432                        self.need = total;
433                        return Ok(delivered);
434                    }
435                    (&window[hl..total], total)
436                }
437                Framing::Chunked => {
438                    let Some(end) = dechunk(&window[hl..], &mut self.body, limit)? else {
439                        if window.len() > limit {
440                            return Err(Error::Http("response larger than the buffer".into()));
441                        }
442                        return Ok(delivered);
443                    };
444                    self.chunk_copy_bytes += self.body.len() as u64;
445                    (self.body.as_slice(), hl + end)
446                }
447            };
448            let request = req.id;
449            handler.on_response(
450                id,
451                request,
452                &Response {
453                    status,
454                    headers: resp.headers,
455                    body,
456                },
457                meta,
458            );
459            self.in_flight.pop_front();
460            self.stream.consume(total);
461            self.responses += 1;
462            delivered += 1;
463            if close {
464                return Err(Error::Closed);
465            }
466        }
467    }
468}
469
470/// A driver-owned [`HttpConn`]: the same request methods.
471impl ConnMut<'_, HttpConn> {
472    /// [`HttpConn::request`].
473    ///
474    /// # Errors
475    /// As [`HttpConn::request`].
476    pub fn request(
477        &mut self,
478        method: &str,
479        path: &str,
480        headers: &[(&str, &str)],
481        body: &[u8],
482    ) -> Result<u64> {
483        self.inner().request(method, path, headers, body)
484    }
485
486    /// [`HttpConn::signed_request`].
487    ///
488    /// # Errors
489    /// As [`HttpConn::signed_request`].
490    pub fn signed_request(
491        &mut self,
492        method: &str,
493        path: &str,
494        params: &str,
495        headers: &[(&str, &str)],
496        signer: &crate::auth::Signer,
497    ) -> Result<u64> {
498        self.inner()
499            .signed_request(method, path, params, headers, signer)
500    }
501
502    /// [`HttpConn::signed_request_at`].
503    ///
504    /// # Errors
505    /// As [`HttpConn::signed_request_at`].
506    pub fn signed_request_at(
507        &mut self,
508        method: &str,
509        path: &str,
510        params: &str,
511        headers: &[(&str, &str)],
512        signer: &crate::auth::Signer,
513        timestamp_ms: u64,
514    ) -> Result<u64> {
515        self.inner()
516            .signed_request_at(method, path, params, headers, signer, timestamp_ms)
517    }
518}
519
520enum Framing {
521    Length(usize),
522    Chunked,
523}
524
525fn framing(headers: &[httparse::Header<'_>], status: u16, head: bool) -> Result<Framing> {
526    if head || status == 204 || status == 304 {
527        return Ok(Framing::Length(0));
528    }
529    let mut length = None;
530    for h in headers {
531        if h.name.eq_ignore_ascii_case("transfer-encoding") {
532            if h.value.eq_ignore_ascii_case(b"chunked") {
533                return Ok(Framing::Chunked);
534            }
535            return Err(Error::Http("unsupported transfer encoding".into()));
536        }
537        if h.name.eq_ignore_ascii_case("content-length") {
538            let v = std::str::from_utf8(h.value)
539                .ok()
540                .and_then(|s| s.trim().parse::<usize>().ok())
541                .ok_or_else(|| Error::Http("bad Content-Length".into()))?;
542            if length.is_some_and(|l| l != v) {
543                return Err(Error::Http("conflicting Content-Length".into()));
544            }
545            length = Some(v);
546        }
547    }
548    length
549        .map(Framing::Length)
550        .ok_or_else(|| Error::Http("response without Content-Length".into()))
551}
552
553/// De-chunks `data` into `out`. `Ok(None)` if incomplete; otherwise the bytes
554/// consumed (including the last chunk and trailers).
555fn dechunk(data: &[u8], out: &mut Vec<u8>, limit: usize) -> Result<Option<usize>> {
556    out.clear();
557    let mut pos = 0;
558    loop {
559        let (hdr, size) = match httparse::parse_chunk_size(&data[pos..])
560            .map_err(|_| Error::Http("bad chunk size".into()))?
561        {
562            httparse::Status::Partial => return Ok(None),
563            httparse::Status::Complete(v) => v,
564        };
565        let size = usize::try_from(size)
566            .ok()
567            .filter(|&s| s <= limit)
568            .ok_or_else(|| Error::Http("chunk too large".into()))?;
569        pos += hdr;
570        if size == 0 {
571            // Trailers end with an empty line.
572            let rest = &data[pos..];
573            let Some(end) = rest.windows(2).position(|w| w == b"\r\n") else {
574                return Ok(None);
575            };
576            if end == 0 {
577                return Ok(Some(pos + 2));
578            }
579            // Skip trailer fields up to the blank line.
580            let Some(blank) = rest.windows(4).position(|w| w == b"\r\n\r\n") else {
581                return Ok(None);
582            };
583            return Ok(Some(pos + blank + 4));
584        }
585        let end = pos
586            .checked_add(size)
587            .and_then(|e| e.checked_add(2))
588            .ok_or_else(|| Error::Http("chunk too large".into()))?;
589        if data.len() < end {
590            return Ok(None);
591        }
592        out.extend_from_slice(&data[pos..end - 2]);
593        if &data[end - 2..end] != b"\r\n" {
594            return Err(Error::Http("chunk without CRLF".into()));
595        }
596        pos = end;
597    }
598}
599
600fn push_decimal(out: &mut Vec<u8>, mut v: usize) {
601    let mut digits = [0u8; 20];
602    let mut i = digits.len();
603    loop {
604        i -= 1;
605        #[allow(clippy::cast_possible_truncation)]
606        {
607            digits[i] = b'0' + (v % 10) as u8;
608        }
609        v /= 10;
610        if v == 0 {
611            break;
612        }
613    }
614    out.extend_from_slice(&digits[i..]);
615}
616
617/// An HTTP connection being set up without blocking (TCP connect, TLS). Hand it
618/// to `Driver::connect`.
619pub struct HttpDial {
620    target: Target,
621    opts: HttpOptions,
622    tls: TlsDial,
623}
624
625impl HttpDial {
626    /// A dial to `target`; its headers are sent with every request.
627    ///
628    /// # Errors
629    /// The buffer in `opts` is too small, or a header contains CR/LF.
630    pub fn new(target: Target, cfg: Arc<ClientConfig>, opts: HttpOptions) -> Result<Self> {
631        if opts.buf_size < 8 * MAX_RECORD {
632            return Err(Error::Config(
633                "receive buffer must hold at least 8 records".into(),
634            ));
635        }
636        if opts.max_in_flight == 0 {
637            return Err(Error::Config("max_in_flight must be at least 1".into()));
638        }
639        if opts.max_in_flight > MAX_IN_FLIGHT {
640            return Err(Error::Config(format!(
641                "max_in_flight above {MAX_IN_FLIGHT}"
642            )));
643        }
644        for (k, v) in &target.headers {
645            validate_header(k, v)?;
646        }
647        let sni = target.host.clone();
648        Ok(Self {
649            target,
650            opts,
651            tls: TlsDial::new(cfg, sni)?,
652        })
653    }
654
655    fn fixed_headers(&self) -> Vec<u8> {
656        let mut f = Vec::new();
657        f.extend_from_slice(b"Host: ");
658        f.extend_from_slice(self.target.host_header().as_bytes());
659        f.extend_from_slice(b"\r\n");
660        for (k, v) in &self.target.headers {
661            f.extend_from_slice(k.as_bytes());
662            f.extend_from_slice(b": ");
663            f.extend_from_slice(v.as_bytes());
664            f.extend_from_slice(b"\r\n");
665        }
666        f
667    }
668}
669
670impl DialIo for HttpDial {
671    type Conn = HttpConn;
672
673    fn host(&self) -> &str {
674        &self.target.host
675    }
676
677    fn port(&self) -> u16 {
678        self.target.port
679    }
680
681    fn addr(&self) -> Option<SocketAddr> {
682        self.target.addr
683    }
684
685    fn start(&mut self, addr: SocketAddr) -> Result<()> {
686        self.tls.start(addr)
687    }
688
689    fn step(&mut self) -> Result<Option<HttpConn>> {
690        let Some((sock, tls, rest)) = self.tls.step()? else {
691            return Ok(None);
692        };
693        // Non-zero fill writes every page now (zeroed memory is mapped lazily),
694        // so the first receives do not take page faults.
695        let mut buf = vec![1u8; self.opts.buf_size].into_boxed_slice();
696        if rest.len() > buf.len() {
697            return Err(Error::Record("handshake leftover larger than the buffer"));
698        }
699        buf[..rest.len()].copy_from_slice(&rest);
700        let mut stream = TlsStream::new(sock, buf, 0, rest.len(), tls, Vec::with_capacity(1 << 16));
701        if let Some(check) = self.tls.take_route() {
702            stream.set_route(crate::endpoint::sealed::RouteSlot::Ready(check));
703        }
704        Ok(Some(HttpConn {
705            stream,
706            fixed: self.fixed_headers(),
707            in_flight: VecDeque::with_capacity(self.opts.max_in_flight),
708            next_id: 0,
709            opts: self.opts,
710            body: Vec::with_capacity(1 << 16),
711            need: 0,
712            requests: 0,
713            responses: 0,
714            chunk_copy_bytes: 0,
715            handed: 0,
716        }))
717    }
718
719    fn gate_route(&mut self) {
720        self.tls.gate_route();
721    }
722
723    fn awaiting_route(&self) -> Option<std::os::fd::RawFd> {
724        self.tls.awaiting_route()
725    }
726
727    fn route_checked(&mut self, check: crate::route::RouteCheck) {
728        self.tls.route_checked(check);
729    }
730}
731
732impl HttpDial {
733    /// Connects and runs the TLS handshake, waiting until done: a convenience
734    /// for tools and tests. In a busy-spin loop use `Driver::connect`. Without
735    /// [`Target::addr`] the host is resolved with the system resolver (blocking).
736    ///
737    /// # Errors
738    /// Resolution, connect or TLS failure, or no progress for 30 s.
739    pub fn connect_blocking(mut self) -> Result<HttpConn> {
740        let addr = match self.target.addr {
741            Some(a) => a,
742            None => std::net::ToSocketAddrs::to_socket_addrs(&(
743                self.target.host.as_str(),
744                self.target.port,
745            ))?
746            .find(SocketAddr::is_ipv4)
747            .ok_or_else(|| Error::Dns(format!("{}: no IPv4 address", self.target.host)))?,
748        };
749        self.start(addr)?;
750        let start = std::time::Instant::now();
751        loop {
752            if let Some(c) = self.step()? {
753                return Ok(c);
754            }
755            if start.elapsed() > std::time::Duration::from_secs(30) {
756                return Err(Error::Timeout("http connect"));
757            }
758            std::thread::yield_now();
759        }
760    }
761}
762
763impl Io for HttpConn {
764    const DEADLINES: bool = true;
765
766    #[inline]
767    fn fd(&self) -> std::os::fd::RawFd {
768        self.stream.fd()
769    }
770    #[inline]
771    fn rx_space(&mut self) -> &mut [u8] {
772        self.stream.rx_space()
773    }
774    #[inline]
775    fn tx_queue(&mut self) -> &mut Vec<u8> {
776        self.stream.tx_queue()
777    }
778    fn has_buffered_input(&self) -> bool {
779        self.stream.has_buffered_input()
780    }
781    fn set_route(&mut self, route: crate::endpoint::sealed::RouteSlot) {
782        self.stream.set_route(route);
783    }
784    fn route_state(&self) -> &crate::endpoint::sealed::RouteSlot {
785        self.stream.route_state()
786    }
787    fn expire(&mut self, now_ns: i64) -> Result<()> {
788        match self.in_flight.front() {
789            Some(r) if r.deadline_ns <= now_ns => Err(Error::Timeout("http request")),
790            _ => Ok(()),
791        }
792    }
793    #[inline]
794    fn on_handed(&mut self, n: usize) {
795        self.handed += n as u64;
796    }
797    /// With requests in flight: [`Error::Unanswered`], naming the first whose
798    /// record the kernel never wholly took.
799    #[cold]
800    fn close_reason(&mut self, error: Error) -> Error {
801        if self.in_flight.is_empty() {
802            return error;
803        }
804        let first_unsent = self
805            .in_flight
806            .iter()
807            .find(|r| r.end > self.handed)
808            .map(|r| r.id);
809        Error::Unanswered(Box::new(crate::error::Unanswered {
810            cause: error,
811            first_unsent,
812        }))
813    }
814}
815
816impl<H: HttpHandler> Deliver<H> for HttpConn {
817    #[inline]
818    fn on_received(
819        &mut self,
820        id: ConnId,
821        n: usize,
822        meta: &RxMeta,
823        handler: &mut H,
824    ) -> Result<usize> {
825        self.deliver(id, n, meta, handler)
826    }
827}