Skip to main content

io_http/
client.rs

1//! Standard, blocking HTTP/1.X client wrapping a boxed
2//! `Read + Write + Send` stream. Each [`HttpClientStd::send`] /
3//! [`HttpClientStd::send_http10`] is self-contained (HTTP has no
4//! session context). With a TLS feature enabled,
5//! [`HttpClientStd::connect`] opens `http://` / `https://` URLs
6//! end-to-end via [`pimalaya_stream::std::stream::StreamStd`].
7
8use core::mem;
9
10#[cfg(any(
11    feature = "rustls-aws",
12    feature = "rustls-ring",
13    feature = "native-tls"
14))]
15use alloc::string::ToString;
16use alloc::{boxed::Box, string::String, vec, vec::Vec};
17
18use std::io::{self, Read, Write};
19
20#[cfg(any(
21    feature = "rustls-aws",
22    feature = "rustls-ring",
23    feature = "native-tls"
24))]
25use pimalaya_stream::{std::stream::StreamStd, tls::Tls};
26use thiserror::Error;
27use url::Url;
28
29use crate::{
30    coroutine::*,
31    rfc1945::send::*,
32    rfc9110::{
33        headers::HTTP_TRANSFER_ENCODING,
34        request::HttpRequest,
35        response::HttpResponse,
36        send::{HttpSendOutput, HttpSendYield},
37    },
38    rfc9112::{chunk_stream::*, read_headers::*, send::*},
39    sse::frame::*,
40};
41
42const READ_BUFFER_SIZE: usize = 16 * 1024;
43
44/// Errors returned by [`HttpClientStd`].
45#[derive(Debug, Error)]
46pub enum HttpClientStdError {
47    /// The HTTP/1.0 send coroutine failed.
48    #[error(transparent)]
49    Http10Send(#[from] Http10SendError),
50    /// The HTTP/1.1 send coroutine failed.
51    #[error(transparent)]
52    Http11Send(#[from] Http11SendError),
53    /// The underlying stream failed to read or write.
54    #[error(transparent)]
55    Io(#[from] io::Error),
56    /// The TCP connection or the TLS negotiation failed.
57    #[cfg(any(
58        feature = "rustls-aws",
59        feature = "rustls-ring",
60        feature = "native-tls"
61    ))]
62    #[error(transparent)]
63    Tls(#[from] anyhow::Error),
64    /// The URL to connect to carries no host.
65    #[cfg(any(
66        feature = "rustls-aws",
67        feature = "rustls-ring",
68        feature = "native-tls"
69    ))]
70    #[error("HTTP URL `{0}` has no host")]
71    UrlMissingHost(String),
72    /// The URL to connect to carries a scheme the client cannot open.
73    #[cfg(any(
74        feature = "rustls-aws",
75        feature = "rustls-ring",
76        feature = "native-tls"
77    ))]
78    #[error("HTTP URL `{0}` has unsupported scheme `{1}` (expected `http` or `https`)")]
79    UrlUnsupportedScheme(String, String),
80    /// The server answered with a redirect the client never follows.
81    #[error("HTTP server redirected to `{url}` (status `{code}`)")]
82    UnexpectedRedirect {
83        /// The resolved redirect target.
84        url: Url,
85        /// The 3xx status code of the response.
86        code: u16,
87    },
88    /// The streaming response did not use chunked transfer coding.
89    #[error("HTTP streaming requires `Transfer-Encoding: chunked` (got status `{0}`)")]
90    StreamingNotChunked(u16),
91    /// The streaming chunked-body decoder failed.
92    #[error(transparent)]
93    ChunkStream(#[from] Http11ChunksReadStreamError),
94}
95
96/// Std-blocking HTTP client wrapping a boxed `Read + Write + Send` stream.
97pub struct HttpClientStd {
98    stream: Box<dyn HttpStream>,
99}
100
101impl HttpClientStd {
102    /// Wraps a pre-connected stream; caller handles TCP and TLS.
103    pub fn new<S: Read + Write + Send + 'static>(stream: S) -> Self {
104        Self {
105            stream: Box::new(stream),
106        }
107    }
108
109    /// Default ALPN identifier for HTTPS connections: `http/1.1`
110    /// ([RFC 7301] + IANA registry).
111    ///
112    /// [RFC 7301]: https://www.rfc-editor.org/rfc/rfc7301
113    pub fn default_alpn() -> Vec<String> {
114        vec![String::from("http/1.1")]
115    }
116
117    /// Connects to `url` (TLS handshake on `https`), reading ALPN from
118    /// `tls.rustls.alpn` (see [`Self::default_alpn`]).
119    #[cfg(any(
120        feature = "rustls-aws",
121        feature = "rustls-ring",
122        feature = "native-tls"
123    ))]
124    pub fn connect(url: &Url, tls: &Tls) -> Result<Self, HttpClientStdError> {
125        let host = url
126            .host_str()
127            .ok_or_else(|| HttpClientStdError::UrlMissingHost(url.to_string()))?;
128
129        let stream = match url.scheme() {
130            "http" => StreamStd::connect_tcp(host, url.port_or_known_default().unwrap_or(80))?,
131            "https" => {
132                StreamStd::connect_tls(host, url.port_or_known_default().unwrap_or(443), tls)?
133            }
134            scheme => {
135                return Err(HttpClientStdError::UrlUnsupportedScheme(
136                    url.to_string(),
137                    scheme.to_string(),
138                ));
139            }
140        };
141
142        Ok(Self {
143            stream: Box::new(stream),
144        })
145    }
146
147    /// Replaces the underlying stream (e.g. after `Connection: close` or
148    /// a cross-authority redirect).
149    pub fn set_stream<S: Read + Write + Send + 'static>(&mut self, stream: S) {
150        self.stream = Box::new(stream);
151    }
152
153    /// Drives any standard-shape coroutine against the wrapped stream
154    /// until it completes. Coroutines with richer Yield variants
155    /// (`Http*Send`, `Http11ChunksReadStream`, `SseFrameParser`) use
156    /// their own per-method loops below.
157    pub fn run<C, T, E>(&mut self, mut coroutine: C) -> Result<T, HttpClientStdError>
158    where
159        C: HttpCoroutine<Yield = HttpYield, Return = Result<T, E>>,
160        HttpClientStdError: From<E>,
161    {
162        let mut buf = [0u8; READ_BUFFER_SIZE];
163        let mut arg: Option<&[u8]> = None;
164
165        loop {
166            match coroutine.resume(arg.take()) {
167                HttpCoroutineState::Complete(Ok(out)) => return Ok(out),
168                HttpCoroutineState::Complete(Err(err)) => return Err(err.into()),
169                HttpCoroutineState::Yielded(HttpYield::WantsRead) => {
170                    let n = self.stream.read(&mut buf)?;
171                    arg = Some(&buf[..n]);
172                }
173                HttpCoroutineState::Yielded(HttpYield::WantsWrite(bytes)) => {
174                    self.stream.write_all(&bytes)?;
175                    arg = None;
176                }
177            }
178        }
179    }
180
181    /// Runs [`Http11Send`]; surfaces 3xx as
182    /// [`HttpClientStdError::UnexpectedRedirect`].
183    pub fn send(&mut self, request: HttpRequest) -> Result<HttpSendOutput, HttpClientStdError> {
184        let mut coroutine = Http11Send::new(request);
185        let mut buf = [0u8; READ_BUFFER_SIZE];
186        let mut arg: Option<&[u8]> = None;
187
188        loop {
189            match coroutine.resume(arg.take()) {
190                HttpCoroutineState::Complete(Ok(out)) => return Ok(out),
191                HttpCoroutineState::Complete(Err(err)) => return Err(err.into()),
192                HttpCoroutineState::Yielded(HttpSendYield::WantsRead) => {
193                    let n = self.stream.read(&mut buf)?;
194                    arg = Some(&buf[..n]);
195                }
196                HttpCoroutineState::Yielded(HttpSendYield::WantsWrite(bytes)) => {
197                    self.stream.write_all(&bytes)?;
198                    arg = None;
199                }
200                HttpCoroutineState::Yielded(HttpSendYield::WantsRedirect {
201                    url, response, ..
202                }) => {
203                    return Err(HttpClientStdError::UnexpectedRedirect {
204                        url,
205                        code: *response.status,
206                    });
207                }
208            }
209        }
210    }
211
212    /// HTTP/1.0 counterpart of [`Self::send`].
213    pub fn send_http10(
214        &mut self,
215        request: HttpRequest,
216    ) -> Result<HttpSendOutput, HttpClientStdError> {
217        let mut coroutine = Http10Send::new(request);
218        let mut buf = [0u8; READ_BUFFER_SIZE];
219        let mut arg: Option<&[u8]> = None;
220
221        loop {
222            match coroutine.resume(arg.take()) {
223                HttpCoroutineState::Complete(Ok(out)) => return Ok(out),
224                HttpCoroutineState::Complete(Err(err)) => return Err(err.into()),
225                HttpCoroutineState::Yielded(HttpSendYield::WantsRead) => {
226                    let n = self.stream.read(&mut buf)?;
227                    arg = Some(&buf[..n]);
228                }
229                HttpCoroutineState::Yielded(HttpSendYield::WantsWrite(bytes)) => {
230                    self.stream.write_all(&bytes)?;
231                    arg = None;
232                }
233                HttpCoroutineState::Yielded(HttpSendYield::WantsRedirect {
234                    url, response, ..
235                }) => {
236                    return Err(HttpClientStdError::UnexpectedRedirect {
237                        url,
238                        code: *response.status,
239                    });
240                }
241            }
242        }
243    }
244}
245
246impl HttpClientStd {
247    /// Opens an HTTP/1.1 SSE stream; requires `Transfer-Encoding: chunked`.
248    /// Consumes `self` because the connection is dedicated to the stream.
249    pub fn send_streaming(self, request: HttpRequest) -> Result<SseStream, HttpClientStdError> {
250        let HttpClientStd { mut stream } = self;
251
252        let req_bytes = request.to_http_11_vec();
253        stream.write_all(&req_bytes)?;
254
255        let mut read_headers = Http11HeadersRead::default();
256        let mut buf = [0u8; READ_BUFFER_SIZE];
257        let mut arg: Option<&[u8]> = None;
258
259        let out = loop {
260            match read_headers.resume(arg.take()) {
261                HttpCoroutineState::Complete(Ok(out)) => break out,
262                HttpCoroutineState::Complete(Err(err)) => {
263                    return Err(Http11SendError::from(err).into());
264                }
265                HttpCoroutineState::Yielded(HttpYield::WantsRead) => {
266                    let n = stream.read(&mut buf)?;
267                    if n == 0 {
268                        return Err(Http11SendError::Eof.into());
269                    }
270                    arg = Some(&buf[..n]);
271                }
272                HttpCoroutineState::Yielded(HttpYield::WantsWrite(_)) => {
273                    unreachable!("Http11HeadersRead never writes");
274                }
275            }
276        };
277
278        let chunked = out
279            .response
280            .header(HTTP_TRANSFER_ENCODING)
281            .is_some_and(|enc| enc.eq_ignore_ascii_case("chunked"));
282
283        if !chunked {
284            return Err(HttpClientStdError::StreamingNotChunked(
285                *out.response.status,
286            ));
287        }
288
289        Ok(SseStream {
290            stream,
291            chunk_stream: Http11ChunksReadStream::default(),
292            sse_parser: SseFrameParser::default(),
293            pending: None,
294            preread: out.remaining,
295            response: out.response,
296            keep_alive: out.keep_alive,
297            done: false,
298        })
299    }
300}
301
302/// Long-lived HTTP/1.1 Server-Sent Events stream; each
303/// [`SseStream::next_frame`] / [`Iterator::next`] blocks until the next
304/// event arrives or the connection closes.
305pub struct SseStream {
306    stream: Box<dyn HttpStream>,
307    chunk_stream: Http11ChunksReadStream,
308    sse_parser: SseFrameParser,
309    pending: Option<Vec<u8>>,
310    preread: Vec<u8>,
311    response: HttpResponse,
312    keep_alive: bool,
313    done: bool,
314}
315
316impl SseStream {
317    /// Parsed response headers (body is the streaming channel itself).
318    pub fn response(&self) -> &HttpResponse {
319        &self.response
320    }
321
322    /// Whether the server signalled the connection can be reused.
323    pub fn keep_alive(&self) -> bool {
324        self.keep_alive
325    }
326
327    /// Last-event-id seen so far; supply via `Last-Event-ID` on reconnect.
328    pub fn last_event_id(&self) -> Option<&str> {
329        self.sse_parser.last_event_id()
330    }
331
332    /// Drives chunked + SSE decoding until the next event; [`None`] on
333    /// connection close or zero-length chunk terminator.
334    pub fn next_frame(&mut self) -> Result<Option<SseFrame>, HttpClientStdError> {
335        if self.done {
336            return Ok(None);
337        }
338
339        loop {
340            let arg = self.pending.take();
341            match self.sse_parser.resume(arg.as_deref()) {
342                HttpCoroutineState::Yielded(SseFrameParserYield::Frame(frame)) => {
343                    return Ok(Some(frame));
344                }
345                HttpCoroutineState::Yielded(SseFrameParserYield::WantsBytes) => {
346                    match self.pull_chunk()? {
347                        Some(body) => self.pending = Some(body),
348                        None => {
349                            self.done = true;
350                            return Ok(None);
351                        }
352                    }
353                }
354                HttpCoroutineState::Complete(never) => match never {},
355            }
356        }
357    }
358
359    /// Closes the underlying connection (equivalent to dropping `self`).
360    pub fn close(self) {
361        drop(self);
362    }
363
364    fn pull_chunk(&mut self) -> Result<Option<Vec<u8>>, HttpClientStdError> {
365        let mut tmp = [0u8; READ_BUFFER_SIZE];
366        let preread = mem::take(&mut self.preread);
367        let mut arg: Option<&[u8]> = if preread.is_empty() {
368            None
369        } else {
370            Some(&preread)
371        };
372
373        loop {
374            match self.chunk_stream.resume(arg.take()) {
375                HttpCoroutineState::Yielded(Http11ChunksReadStreamYield::Frame { body }) => {
376                    return Ok(Some(body));
377                }
378                HttpCoroutineState::Complete(Ok(_remaining)) => return Ok(None),
379                HttpCoroutineState::Yielded(Http11ChunksReadStreamYield::WantsRead) => {
380                    let n = self.stream.read(&mut tmp)?;
381                    if n == 0 {
382                        return Ok(None);
383                    }
384                    arg = Some(&tmp[..n]);
385                }
386                HttpCoroutineState::Complete(Err(err)) => return Err(err.into()),
387            }
388        }
389    }
390}
391
392impl Iterator for SseStream {
393    type Item = Result<SseFrame, HttpClientStdError>;
394
395    fn next(&mut self) -> Option<Self::Item> {
396        match self.next_frame() {
397            Ok(Some(frame)) => Some(Ok(frame)),
398            Ok(None) => None,
399            Err(err) => Some(Err(err)),
400        }
401    }
402}
403
404/// Marker for everything the client can run against; the `Send`
405/// supertrait propagates through the `Box<dyn HttpStream>` erasure so
406/// [`HttpClientStd`] stays `Send`.
407trait HttpStream: Read + Write + Send {}
408impl<T: Read + Write + Send + ?Sized> HttpStream for T {}