Skip to main content

base_http1/
client.rs

1// Released under the MIT License.
2// Copyright, 2026, by Samuel Williams.
3
4use crate::{
5    Error, Exchange, IncomingBody, Options, RequestReader, ResponseWriter, input, output, wire,
6};
7use base_endpoint::stream::{ReadableStream, WritableStream};
8use base_http::{
9    InterimHandler, InterimResponses, Method, Protocol, Request, RequestMetadata, Response,
10    ResponseMetadata, Status, Version,
11    body::{BodyFrame, ReadableBody},
12};
13use base_scheduler::Cancellation;
14use std::io;
15
16/// Sequential client output. Drive this lane concurrently with `ResponseReader`
17/// so an upstream response can arrive before request production completes.
18/// No dialing, pooling, detached task or Expect/Continue wait policy is hidden.
19pub struct RequestWriter<W> {
20    output: ResponseWriter<W>,
21    body: Option<output::BodyEncoder>,
22    persistent: bool,
23    buffer: Vec<u8>,
24}
25
26impl<W: WritableStream> RequestWriter<W> {
27    pub fn new(writer: W) -> Self {
28        Self {
29            output: ResponseWriter::new(writer),
30            body: None,
31            persistent: false,
32            buffer: Vec::new(),
33        }
34    }
35    /// Whether this output lane permits another request. This does not check
36    /// completion of the independent response lane.
37    pub fn is_reusable(&self) -> bool {
38        self.output.is_usable()
39    }
40
41    /// Send one request. `true` permits another output message; the owner must
42    /// also finish the matching response before reusing the HTTP/1 connection.
43    /// The body is closed on return, including error and cancellation.
44    ///
45    /// A body of known length is sent with `Content-Length`, and any trailers it produces are validated and dropped. On HTTP/1.1 a body of unknown length is chunked and keeps its trailers; HTTP/1.0 has no chunked framing, so it refuses such a body.
46    pub async fn write_request<B: ReadableBody>(
47        &mut self,
48        request: &mut Request<B>,
49        cancellation: &Cancellation,
50    ) -> Result<bool, Error> {
51        let result = self.write_inner(request, cancellation).await;
52        request.body.close(None);
53        result
54    }
55
56    async fn write_inner<B: ReadableBody>(
57        &mut self,
58        request: &mut Request<B>,
59        cancellation: &Cancellation,
60    ) -> Result<bool, Error> {
61        cancellation.check()?;
62        let (framing, persistent) =
63            request_plan(&mut self.buffer, &request.metadata, request.body.length())?;
64        self.output.begin()?;
65        self.output.write_all(&self.buffer, cancellation).await?;
66        self.output.flush(cancellation).await?;
67        let encoding = request_encoder(framing);
68        self.output
69            .write_body(&mut request.body, encoding, cancellation)
70            .await?;
71        self.output.completed(persistent);
72        Ok(persistent)
73    }
74
75    /// Begin imperative output. A missing content length uses chunked HTTP/1.1
76    /// framing; declared fixed lengths are checked at every write and at EOF,
77    /// and trailers written under them are validated and dropped.
78    /// After failure or cancellation this lane must be retired.
79    pub async fn begin_request(
80        &mut self,
81        metadata: &RequestMetadata,
82        cancellation: &Cancellation,
83    ) -> Result<(), Error> {
84        cancellation.check()?;
85        let (framing, persistent) =
86            request_plan(&mut self.buffer, metadata, metadata.content_length)?;
87        self.output.begin()?;
88        self.output.write_all(&self.buffer, cancellation).await?;
89        self.output.flush(cancellation).await?;
90        // Activate the body only once the whole head is sent, so frames cannot follow a failed or dropped head.
91        self.persistent = persistent;
92        self.body = Some(request_encoder(framing));
93        Ok(())
94    }
95
96    /// Directly encode and flush one frame. Taking the encoder before awaiting
97    /// makes a dropped, partially completed write permanently non-resumable.
98    pub async fn write_frame<D: AsRef<[u8]>>(
99        &mut self,
100        frame: BodyFrame<D>,
101        cancellation: &Cancellation,
102    ) -> Result<(), Error> {
103        cancellation.check()?;
104        let mut body = self
105            .body
106            .take()
107            .ok_or(Error::Invalid("request output is not active"))?;
108        let end = frame.is_end();
109        let flush = !matches!(&frame, BodyFrame::End) || body.end_requires_output();
110        body.write_frame(&mut self.output, frame, cancellation)
111            .await?;
112        if flush {
113            self.output.flush(cancellation).await?;
114        }
115        if end {
116            self.output.completed(self.persistent);
117        } else {
118            self.body = Some(body);
119        }
120        Ok(())
121    }
122
123    /// Flush pending transport output. A failure retires the lane; accepted
124    /// bytes must not be replayed after an interrupted flush.
125    pub async fn flush(&mut self, cancellation: &Cancellation) -> Result<(), Error> {
126        let result = self.output.flush(cancellation).await;
127        if result.is_err() {
128            self.body = None;
129            self.output.completed(false);
130        }
131        result
132    }
133
134    pub async fn close(&mut self) -> io::Result<()> {
135        self.output.close().await
136    }
137}
138
139// A request of known length is sent with Content-Length, so trailers it produces are dropped rather than refused.
140fn request_encoder(framing: output::Framing) -> output::BodyEncoder {
141    let chunked = matches!(framing, output::Framing::Chunked(_));
142    output::BodyEncoder::new(framing, false).discard_trailers(!chunked)
143}
144
145fn request_plan(
146    bytes: &mut Vec<u8>,
147    head: &RequestMetadata,
148    length: Option<u64>,
149) -> Result<(output::Framing, bool), Error> {
150    let version = head.version.unwrap_or(Version::Http11);
151    let label = match version {
152        Version::Http10 => "HTTP/1.0",
153        Version::Http11 => "HTTP/1.1",
154        _ => return Err(Error::Invalid("unsupported request version")),
155    };
156    let fields = &head.fields;
157    let host = wire::singleton(fields, "host")?;
158    if let Some(host) = host {
159        wire::authority(
160            std::str::from_utf8(host).map_err(|_| Error::Invalid("invalid host"))?,
161            false,
162        )?;
163    }
164    let add_host = if let Some(authority) = &head.authority {
165        wire::authority(authority, false)?;
166        if let Some(host) = host {
167            if !host.eq_ignore_ascii_case(authority.as_bytes()) {
168                return Err(Error::Invalid("authority differs from Host"));
169            }
170            None
171        } else {
172            Some(authority.as_str())
173        }
174    } else {
175        None
176    };
177    if version == Version::Http11 && host.is_none() && add_host.is_none() {
178        return Err(Error::Invalid("missing host"));
179    }
180    let upgrade = if let Some(protocol) = &head.protocol {
181        if version != Version::Http11 {
182            return Err(Error::Invalid("upgrade requires HTTP/1.1"));
183        }
184        if let Some(offer) = wire::singleton(fields, "upgrade")? {
185            if !offer.eq_ignore_ascii_case(protocol.as_str().as_bytes()) {
186                return Err(Error::Invalid("conflicting upgrade protocol"));
187            }
188            None
189        } else {
190            Some(protocol.as_str())
191        }
192    } else {
193        None
194    };
195    wire::connection_options(fields)?;
196    let add_upgrade = head.protocol.is_some() && !wire::has_token(fields, "connection", b"upgrade");
197    if version == Version::Http11
198        && (add_upgrade || wire::has_token(fields, "connection", b"upgrade"))
199        && !fields.contains("upgrade")
200        && upgrade.is_none()
201    {
202        return Err(Error::Invalid("missing upgrade protocol"));
203    }
204    let connection = head
205        .persistent
206        .map(|p| if p { "keep-alive" } else { "close" })
207        .filter(|value| !wire::has_token(fields, "connection", value.as_bytes()));
208    let target = if head.method == Method::CONNECT {
209        if head.path.is_some() {
210            return Err(Error::Invalid("CONNECT cannot have a path"));
211        }
212        head.authority
213            .as_deref()
214            .ok_or(Error::Invalid("CONNECT requires authority"))?
215    } else {
216        head.path.as_deref().unwrap_or("/")
217    };
218    wire::request_target(&head.method, target)?;
219    let (raw_length, raw_chunked) = wire::framing(fields)?;
220    // Raw controls are written deliberately, so they stay strict: no declared length may accompany a raw Transfer-Encoding, and the body must agree with a raw Content-Length.
221    if raw_chunked && (head.content_length.is_some() || raw_length.is_some()) {
222        return Err(Error::Invalid("content-length with transfer-encoding"));
223    }
224    // A body of known length describes the data a request sends, so a typed declaration it contradicts is ignored.
225    let declared = wire::declared_length(head.declared_length(length), raw_length)?;
226    if raw_chunked && version != Version::Http11 {
227        return Err(Error::Invalid("HTTP/1.0 transfer-encoding"));
228    }
229    if let (Some(length), Some(declared)) = (length, declared) {
230        if length != declared {
231            return Err(Error::LengthMismatch);
232        }
233    }
234    // Request trailers are rare and a client cannot assume the server accepts chunked uploads (RFC 9112 ยง6.1), so a known length always selects Content-Length and any trailers are dropped. A Trailer field is only a hint and never affects framing.
235    let framing = if raw_chunked {
236        output::Framing::Chunked(length)
237    } else if let Some(length) = declared.or(length) {
238        output::Framing::Fixed(length)
239    } else if version == Version::Http11 {
240        output::Framing::Chunked(None)
241    } else {
242        return Err(Error::Invalid("HTTP/1.0 request requires a body length"));
243    };
244    let chunked = matches!(framing, output::Framing::Chunked(_));
245    let persistent = head.persistent != Some(false)
246        && !wire::has_token(fields, "connection", b"close")
247        && (version == Version::Http11
248            || head.persistent == Some(true)
249            || wire::has_token(fields, "connection", b"keep-alive"));
250    wire::output_fields(fields.pairs())?;
251    // Validate first, then encode directly into the connection's reusable buffer.
252    bytes.clear();
253    use std::io::Write;
254    // Writing into a `Vec` cannot fail.
255    write!(bytes, "{} {target} {label}\r\n", head.method.as_str()).expect("writing to a Vec");
256    // Chunked framing comes only from a raw Transfer-Encoding, which refuses Content-Length, or from an unknown length, so every field is written as given.
257    for field in fields.pairs() {
258        bytes.extend_from_slice(field.name().as_bytes());
259        bytes.extend_from_slice(b": ");
260        bytes.extend_from_slice(field.value());
261        bytes.extend_from_slice(b"\r\n");
262    }
263    for (name, value) in [
264        ("Host", add_host),
265        ("Upgrade", upgrade),
266        ("Connection", add_upgrade.then_some("upgrade")),
267        ("Connection", connection),
268        (
269            "Transfer-Encoding",
270            (chunked && !raw_chunked).then_some("chunked"),
271        ),
272    ] {
273        if let Some(value) = value {
274            write!(bytes, "{name}: {value}\r\n").expect("writing to a Vec");
275        }
276    }
277    if let output::Framing::Fixed(length) = framing {
278        if raw_length.is_none()
279            && (declared.is_some()
280                || length != 0
281                || matches!(head.method.as_str(), "POST" | "PUT" | "PATCH"))
282        {
283            write!(bytes, "Content-Length: {length}\r\n").expect("writing to a Vec");
284        }
285    }
286    bytes.extend_from_slice(b"\r\n");
287    Ok((framing, persistent))
288}
289
290/// Sequential client input sharing the native request body's chunk decoder.
291/// The matching request head supplies HEAD/CONNECT semantics. This reader does
292/// not queue outstanding requests; the owner pairs each response in wire order.
293/// Switch responses expose raw input only; coordinating the outgoing switch
294/// and managed bidirectional client bodies is not implemented here.
295pub struct ResponseReader<R> {
296    input: RequestReader<R>,
297    persistent: bool,
298}
299
300impl<R: ReadableStream> ResponseReader<R> {
301    pub fn new(reader: R, options: Options) -> Result<Self, Error> {
302        Ok(Self {
303            input: RequestReader::new(reader, options)?,
304            persistent: true,
305        })
306    }
307    pub fn is_reusable(&self) -> bool {
308        self.persistent && self.input.is_reusable()
309    }
310
311    /// Check an idle connection before reusing it, by attempting one
312    /// immediate read with `ReadableStream::try_read_into`. Unread input, EOF
313    /// or a read error (including a TLS close notification) ends reuse
314    /// permanently. A reader that has not yet
315    /// read a response is not probed and reports `true`. A TLS stream
316    /// consumes records with no plaintext, such as key updates or session
317    /// tickets, during the read. This narrows the window for reusing a
318    /// connection the server closed, but cannot remove it: a close the
319    /// transport has not yet observed, or one arriving later, still fails
320    /// the next exchange.
321    pub fn probe_viable(&mut self) -> bool {
322        self.persistent && self.input.probe_idle()
323    }
324
325    /// Deliver interim heads in order, then return the final response. `101`
326    /// and successful CONNECT yield raw input and permanently end HTTP reuse.
327    /// The connection owner supervises the independent request-writing lane.
328    pub async fn read_response<I: InterimHandler>(
329        &mut self,
330        request: &RequestMetadata,
331        interim: &mut InterimResponses<I>,
332        cancellation: &Cancellation,
333    ) -> Result<Response<IncomingBody<'_, R>>, Error> {
334        cancellation.check()?;
335        if !self.is_reusable() {
336            return Err(Error::Invalid("response input is not reusable"));
337        }
338        let max_fields = self.input.options.max_fields;
339        let head = loop {
340            self.input.poisoned = true;
341            let mut head = self
342                .input
343                .read_section(cancellation, "message head", |bytes| {
344                    wire::response(bytes, max_fields)
345                })
346                .await?
347                .ok_or(Error::Truncated)?;
348            if head.status.is_final() {
349                interim.finish();
350                break head;
351            }
352            if head.fields.contains("content-length")
353                || head.fields.contains("transfer-encoding")
354                || head.version != Some(Version::Http11)
355            {
356                return Err(Error::Invalid("invalid interim framing"));
357            }
358            head.fields = wire::application_fields(head.fields)?.0;
359            interim
360                .send_interim_response(head, cancellation)
361                .await
362                .map_err(|error| {
363                    if cancellation.is_cancelled() {
364                        Error::Cancelled
365                    } else {
366                        Error::Body(Box::new(error))
367                    }
368                })?;
369        };
370        self.response_body(request, head)
371    }
372
373    fn response_body(
374        &mut self,
375        request: &RequestMetadata,
376        mut head: ResponseMetadata,
377    ) -> Result<Response<IncomingBody<'_, R>>, Error> {
378        let connect = request.method == Method::CONNECT && head.status.is_success();
379        let upgrade = head.status == Status::SWITCHING_PROTOCOLS;
380        let tunnel = connect || upgrade;
381        // CONNECT framing fields are ignored by recipients (RFC 9112 6.3).
382        let (length, chunked) = if connect {
383            (None, false)
384        } else {
385            wire::framing(&head.fields)?
386        };
387        if chunked && head.version != Some(Version::Http11) {
388            return Err(Error::Invalid("HTTP/1.0 transfer-encoding"));
389        }
390        if upgrade {
391            if length.is_some()
392                || chunked
393                || !wire::has_token(&head.fields, "connection", b"upgrade")
394            {
395                return Err(Error::Invalid("invalid upgrade response"));
396            }
397            let selected = wire::singleton(&head.fields, "upgrade")?
398                .ok_or(Error::Invalid("missing selected protocol"))?;
399            let selected = std::str::from_utf8(selected)
400                .map_err(|_| Error::Invalid("invalid selected protocol"))?;
401            let offered = request
402                .protocol
403                .as_ref()
404                .is_some_and(|p| p.as_str().eq_ignore_ascii_case(selected))
405                || (wire::has_token(&request.fields, "connection", b"upgrade")
406                    && wire::has_token(&request.fields, "upgrade", selected.as_bytes()));
407            if !offered {
408                return Err(Error::Invalid("upgrade protocol was not offered"));
409            }
410            head.protocol = Some(
411                Protocol::new(selected)
412                    .map_err(|_| Error::Invalid("unsupported selected protocol"))?,
413            );
414        }
415        let absent = !tunnel
416            && (request.method == Method::HEAD
417                || head.status == Status::NO_CONTENT
418                || head.status == Status::NOT_MODIFIED);
419        if head.status == Status::NO_CONTENT && !connect && (length.is_some() || chunked) {
420            return Err(Error::Invalid("framing fields on 204"));
421        }
422        let raw = tunnel || (!absent && !chunked && length.is_none());
423        let framing = if absent || raw {
424            input::Framing::Fixed(0)
425        } else if chunked {
426            input::Framing::CHUNKED
427        } else {
428            input::Framing::Fixed(length.unwrap())
429        };
430        let body_length = if raw {
431            None
432        } else if absent && head.status == Status::NO_CONTENT {
433            Some(0)
434        } else {
435            length
436        };
437        let response_persistent = !wire::has_token(&head.fields, "connection", b"close")
438            && (head.version == Some(Version::Http11)
439                || wire::has_token(&head.fields, "connection", b"keep-alive"));
440        head.content_length = length;
441        let (fields, trailer_options) = wire::application_fields(head.fields)?;
442        head.fields = fields;
443        let exchange = Exchange::new(request, trailer_options);
444        let request_persistent = exchange.is_persistent();
445        self.persistent = request_persistent && response_persistent && !raw;
446        if tunnel {
447            exchange.switch();
448        }
449        self.input.previous = Some(exchange.clone());
450        self.input.poisoned = false;
451        let forbid_content = head.status == Status::RESET_CONTENT && !tunnel;
452        let body = IncomingBody::new(
453            &mut self.input,
454            exchange,
455            framing,
456            body_length,
457            raw,
458            forbid_content,
459        );
460        Ok(Response::from_parts(head, body))
461    }
462
463    pub async fn close(&mut self) -> io::Result<()> {
464        self.input.close().await
465    }
466}