Skip to main content

base_http1/
output.rs

1// Released under the MIT License.
2// Copyright, 2026, by Samuel Williams.
3
4use crate::{Error, Exchange, wire};
5use base_endpoint::stream::WritableStream;
6use base_http::{
7    InterimHandler, Response, ResponseMetadata, Status, Version,
8    body::{BodyFrame, ReadableBody},
9};
10use base_scheduler::Cancellation;
11use std::{
12    future::{Future, poll_fn},
13    io,
14    pin::pin,
15    task::Poll,
16};
17
18async fn wait<F: Future>(future: F, cancellation: &Cancellation) -> Result<F::Output, Error> {
19    if cancellation.is_never() {
20        return Ok(future.await);
21    }
22    let mut future = pin!(future);
23    let mut cancelled = pin!(cancellation.cancelled());
24    poll_fn(|cx| {
25        if cancelled.as_mut().poll(cx).is_ready() {
26            return Poll::Ready(Err(Error::Cancelled));
27        }
28        future.as_mut().poll(cx).map(Ok)
29    })
30    .await
31}
32
33// Bound copied output independently of the number or size of body chunks.
34const OUTPUT_BATCH_LIMIT: usize = 16 * 1024;
35
36async fn send_all<W: WritableStream>(
37    writer: &mut W,
38    mut bytes: &[u8],
39    cancellation: &Cancellation,
40) -> Result<(), Error> {
41    while !bytes.is_empty() {
42        let n = writer.write(bytes, cancellation).await?;
43        if n == 0 {
44            return Err(
45                io::Error::new(io::ErrorKind::WriteZero, "HTTP output made no progress").into(),
46            );
47        }
48        bytes = &bytes[n..];
49    }
50    Ok(())
51}
52
53/// The response lane. A partially sent or abandoned response permanently
54/// poisons admission. The owner must still await `close` to retire the backend.
55pub struct ResponseWriter<W> {
56    writer: W,
57    pending: Vec<u8>,
58    usable: bool,
59    continue_sent: bool,
60}
61
62impl<W: WritableStream> ResponseWriter<W> {
63    pub fn new(writer: W) -> Self {
64        Self {
65            writer,
66            pending: Vec::new(),
67            usable: true,
68            continue_sent: false,
69        }
70    }
71    pub fn is_usable(&self) -> bool {
72        self.usable
73    }
74    pub fn interim(&mut self, version: Version) -> InterimWriter<'_, W> {
75        InterimWriter {
76            output: self,
77            version,
78        }
79    }
80
81    /// Best-effort rejection for invalid input, followed by connection closure.
82    pub async fn reject(
83        &mut self,
84        status: Status,
85        cancellation: &Cancellation,
86    ) -> Result<(), Error> {
87        cancellation.check()?;
88        if !self.usable {
89            return Err(Error::Invalid("response output is not reusable"));
90        }
91        self.usable = false;
92        self.write_start_line(status, Version::Http11);
93        self.buffer(b"Content-Length: 0\r\nConnection: close\r\n\r\n");
94        self.flush(cancellation).await
95    }
96
97    pub(crate) async fn write_all(
98        &mut self,
99        bytes: &[u8],
100        cancellation: &Cancellation,
101    ) -> Result<(), Error> {
102        cancellation.check()?;
103        if bytes.len() > OUTPUT_BATCH_LIMIT - self.pending.len() {
104            self.send_pending(cancellation).await?;
105        }
106        if bytes.len() >= OUTPUT_BATCH_LIMIT {
107            // Large chunks retain their existing owner and are not copied.
108            return send_all(&mut self.writer, bytes, cancellation).await;
109        }
110        self.buffer(bytes);
111        Ok(())
112    }
113
114    fn reserve(&mut self, additional: usize) {
115        let required = self.pending.len() + additional;
116        if required > self.pending.capacity() {
117            let capacity = required
118                .max(self.pending.capacity().saturating_mul(2))
119                .clamp(256, OUTPUT_BATCH_LIMIT);
120            self.pending.reserve_exact(capacity - self.pending.len());
121        }
122    }
123
124    async fn send_pending(&mut self, cancellation: &Cancellation) -> Result<(), Error> {
125        // The lane is already poisoned while sending. On error/cancellation the
126        // retained prefix must never be retried by a subsequent message or close.
127        send_all(&mut self.writer, &self.pending, cancellation).await?;
128        self.pending.clear();
129        Ok(())
130    }
131
132    fn buffer(&mut self, bytes: &[u8]) {
133        self.reserve(bytes.len());
134        self.pending.extend_from_slice(bytes);
135    }
136
137    // Every message starts with an empty batch, since the previous message or interim head was flushed, so its start line always fits.
138    fn write_start_line(&mut self, status: Status, version: Version) {
139        if status == Status::OK && version == Version::Http11 {
140            self.buffer(b"HTTP/1.1 200 OK\r\n");
141            return;
142        }
143        let version = match version {
144            Version::Http10 => b"HTTP/1.0 ",
145            Version::Http11 => b"HTTP/1.1 ",
146            _ => unreachable!("HTTP/1 exchanges and interim writers are HTTP/1.0 or HTTP/1.1"),
147        };
148        let status_code = status.as_u16();
149        let code = [
150            b'0' + (status_code / 100) as u8,
151            b'0' + (status_code / 10 % 10) as u8,
152            b'0' + (status_code % 10) as u8,
153            b' ',
154        ];
155        let reason = status.description().unwrap_or("").as_bytes();
156        self.reserve(version.len() + code.len() + reason.len() + 2);
157        self.pending.extend_from_slice(version);
158        self.pending.extend_from_slice(&code);
159        self.pending.extend_from_slice(reason);
160        self.pending.extend_from_slice(b"\r\n");
161    }
162
163    async fn write_fields(
164        &mut self,
165        fields: &[base_http::Field],
166        cancellation: &Cancellation,
167    ) -> Result<(), Error> {
168        for field in fields {
169            self.write_field(field.name(), field.value(), cancellation)
170                .await?;
171        }
172        Ok(())
173    }
174
175    async fn write_field(
176        &mut self,
177        name: &str,
178        value: &[u8],
179        cancellation: &Cancellation,
180    ) -> Result<(), Error> {
181        let size = name.len().saturating_add(value.len()).saturating_add(4);
182        if size <= OUTPUT_BATCH_LIMIT - self.pending.len() {
183            self.reserve(size);
184            self.pending.extend_from_slice(name.as_bytes());
185            self.pending.extend_from_slice(b": ");
186            self.pending.extend_from_slice(value);
187            self.pending.extend_from_slice(b"\r\n");
188            return Ok(());
189        }
190        self.write_all(name.as_bytes(), cancellation).await?;
191        self.write_all(b": ", cancellation).await?;
192        self.write_all(value, cancellation).await?;
193        self.write_all(b"\r\n", cancellation).await
194    }
195
196    async fn write_plan(
197        &mut self,
198        plan: &Plan<'_>,
199        cancellation: &Cancellation,
200    ) -> Result<(), Error> {
201        self.write_start_line(plan.response.status, plan.version);
202        for field in plan.response.fields.pairs() {
203            if (plan.upgrade.is_some() && field.name().eq_ignore_ascii_case("upgrade"))
204                || (matches!(plan.framing, Framing::Chunked(_))
205                    && field.name().eq_ignore_ascii_case("content-length"))
206            {
207                continue;
208            }
209            self.write_field(field.name(), field.value(), cancellation)
210                .await?;
211        }
212        if let Some(protocol) = plan.upgrade {
213            self.write_field("Upgrade", protocol, cancellation).await?;
214        }
215        if plan.connection_upgrade {
216            self.write_field("Connection", b"upgrade", cancellation)
217                .await?;
218        }
219        if let Some(length) = plan.content_length {
220            let mut digits = [0; 20];
221            let bytes = decimal(length, &mut digits);
222            self.write_field("Content-Length", bytes, cancellation)
223                .await?;
224        }
225        if plan.chunked {
226            self.write_field("Transfer-Encoding", b"chunked", cancellation)
227                .await?;
228        }
229        if let Some(option) = plan.connection {
230            self.write_field("Connection", option, cancellation).await?;
231        }
232        self.write_all(b"\r\n", cancellation).await
233    }
234
235    /// Write one final response. `true` means output permits persistence; the
236    /// connection owner must also finish input and join concurrent work before
237    /// admitting another request. Discarded input disables persistence.
238    pub async fn write_response<B: ReadableBody>(
239        &mut self,
240        exchange: &Exchange,
241        response: &mut Response<B>,
242        cancellation: &Cancellation,
243    ) -> Result<bool, Error> {
244        let result = self
245            .write_response_inner(exchange, response, cancellation)
246            .await;
247        response.body.close(None);
248        result
249    }
250
251    async fn write_response_inner<B: ReadableBody>(
252        &mut self,
253        exchange: &Exchange,
254        response: &mut Response<B>,
255        cancellation: &Cancellation,
256    ) -> Result<bool, Error> {
257        cancellation.check()?;
258        if !self.usable {
259            return Err(Error::Invalid("response output is not reusable"));
260        }
261        let (framing, persistent, tunnel) = {
262            let plan = plan(
263                exchange,
264                &response.metadata,
265                response.body.length(),
266                response.body.may_have_trailers(),
267            )?;
268            if plan.tunnel
269                && response.metadata.status == Status::SWITCHING_PROTOCOLS
270                && !self.continue_sent
271                && exchange.expects_continue()
272            {
273                self.interim(Version::Http11)
274                    .on_interim_response(ResponseMetadata::new(Status::CONTINUE), cancellation)
275                    .await?;
276            }
277            self.usable = false;
278            self.write_plan(&plan, cancellation).await?;
279            if plan.tunnel {
280                // Commit the handshake before the first potentially blocking input read.
281                self.flush(cancellation).await?;
282                exchange.switch();
283            }
284            (plan.framing, plan.persistent, plan.tunnel)
285        };
286        self.write_body(
287            &mut response.body,
288            framing,
289            !tunnel && response.metadata.status == Status::RESET_CONTENT,
290            cancellation,
291        )
292        .await?;
293        self.usable = persistent;
294        self.continue_sent = false;
295        Ok(persistent)
296    }
297
298    pub(crate) fn begin(&mut self) -> Result<(), Error> {
299        if !self.usable {
300            return Err(Error::Invalid("output is not reusable"));
301        }
302        self.usable = false;
303        Ok(())
304    }
305
306    pub(crate) fn completed(&mut self, persistent: bool) {
307        self.usable = persistent;
308    }
309
310    pub(crate) async fn flush(&mut self, cancellation: &Cancellation) -> Result<(), Error> {
311        self.send_pending(cancellation).await?;
312        Ok(self.writer.flush(cancellation).await?)
313    }
314
315    pub(crate) async fn write_body<B: ReadableBody>(
316        &mut self,
317        body: &mut B,
318        framing: Framing,
319        forbid_content: bool,
320        cancellation: &Cancellation,
321    ) -> Result<(), Error> {
322        if !matches!(framing, Framing::Absent) {
323            let mut encoding = BodyEncoder::new(framing, forbid_content);
324            loop {
325                if !body.is_ready() {
326                    self.flush(cancellation).await?;
327                }
328                let frame = {
329                    let reading = pin!(body.read(cancellation));
330                    let mut read = pin!(wait(reading, cancellation));
331                    let ready = poll_fn(|cx| Poll::Ready(read.as_mut().poll(cx))).await;
332                    match ready {
333                        Poll::Ready(result) => result,
334                        Poll::Pending => {
335                            self.flush(cancellation).await?;
336                            read.await
337                        }
338                    }
339                }?
340                .map_err(|e| {
341                    if cancellation.is_cancelled() {
342                        Error::Cancelled
343                    } else {
344                        Error::Body(Box::new(e))
345                    }
346                })?;
347                let end = frame.is_end();
348                encoding.write_frame(self, frame, cancellation).await?;
349                if end {
350                    break;
351                }
352            }
353        }
354        self.flush(cancellation).await?;
355        Ok(())
356    }
357
358    pub async fn close(&mut self) -> io::Result<()> {
359        self.usable = false;
360        self.pending.clear();
361        self.writer.close().await
362    }
363}
364
365/// Statically dispatched interim-response hook borrowing the output lane.
366pub struct InterimWriter<'a, W> {
367    output: &'a mut ResponseWriter<W>,
368    version: Version,
369}
370
371impl<W: WritableStream> InterimHandler for InterimWriter<'_, W> {
372    type Error = Error;
373    async fn on_interim_response(
374        &mut self,
375        response: ResponseMetadata,
376        cancellation: &Cancellation,
377    ) -> Result<(), Error> {
378        cancellation.check()?;
379        if !self.output.usable {
380            return Err(Error::Invalid("response output is not reusable"));
381        }
382        if !response.status.is_informational() || response.status == Status::SWITCHING_PROTOCOLS {
383            return Err(Error::Invalid("invalid interim status"));
384        }
385        if self.version != Version::Http11 {
386            return Err(Error::Invalid("HTTP/1.0 interim response"));
387        }
388        if response.fields.contains("content-length")
389            || response.fields.contains("transfer-encoding")
390            || response.content_length.is_some()
391            || response.protocol.is_some()
392        {
393            return Err(Error::Invalid("invalid interim framing"));
394        }
395        wire::output_fields(response.fields.pairs())?;
396        self.output.usable = false;
397        self.output.write_start_line(response.status, self.version);
398        self.output
399            .write_fields(response.fields.pairs(), cancellation)
400            .await?;
401        self.output.write_all(b"\r\n", cancellation).await?;
402        self.output.flush(cancellation).await?;
403        if response.status == Status::CONTINUE {
404            self.output.continue_sent = true;
405        }
406        self.output.usable = true;
407        Ok(())
408    }
409}
410
411#[derive(Clone, Copy)]
412pub(crate) enum Framing {
413    Absent,
414    Fixed(u64),
415    Chunked(Option<u64>),
416    Raw,
417}
418/// Incremental body framing shared by pull and imperative output. The owner
419/// poisons its lane before entering this state and restores reuse only at EOF.
420pub(crate) struct BodyEncoder {
421    framing: Framing,
422    total: u64,
423    trailers: Option<base_http::Fields>,
424    forbid_content: bool,
425}
426impl BodyEncoder {
427    pub(crate) fn end_requires_output(&self) -> bool {
428        matches!(self.framing, Framing::Chunked(_))
429    }
430    pub(crate) fn new(framing: Framing, forbid_content: bool) -> Self {
431        Self {
432            framing,
433            total: 0,
434            trailers: None,
435            forbid_content,
436        }
437    }
438    pub(crate) async fn write_frame<W: WritableStream, D: AsRef<[u8]>>(
439        &mut self,
440        output: &mut ResponseWriter<W>,
441        frame: BodyFrame<D>,
442        cancellation: &Cancellation,
443    ) -> Result<(), Error> {
444        let end = frame.is_end();
445        match frame {
446            BodyFrame::Data { data, .. } => {
447                if self.trailers.is_some() {
448                    return Err(Error::Invalid("data after trailing headers"));
449                }
450                let bytes = data.as_ref();
451                // No transport carries `u64::MAX` bytes, so saturation only guards the arithmetic.
452                self.total = self.total.saturating_add(bytes.len() as u64);
453                if self.forbid_content && self.total != 0 {
454                    return Err(Error::Invalid("content on 205"));
455                }
456                if let Framing::Fixed(length) | Framing::Chunked(Some(length)) = self.framing {
457                    if self.total > length {
458                        return Err(Error::LengthMismatch);
459                    }
460                }
461                if !bytes.is_empty() {
462                    if matches!(self.framing, Framing::Chunked(_)) {
463                        output
464                            .write_all(format!("{:x}\r\n", bytes.len()).as_bytes(), cancellation)
465                            .await?;
466                        output.write_all(bytes, cancellation).await?;
467                        output.write_all(b"\r\n", cancellation).await?;
468                    } else {
469                        output.write_all(bytes, cancellation).await?;
470                    }
471                }
472            }
473            BodyFrame::Headers { fields, .. } => {
474                if self.trailers.is_some() {
475                    return Err(Error::Invalid("repeated trailing headers"));
476                }
477                if !matches!(self.framing, Framing::Chunked(_)) {
478                    return Err(Error::Invalid("trailers require chunked framing"));
479                }
480                validate_trailer_fields(fields.pairs())?;
481                wire::output_fields(fields.pairs())?;
482                self.trailers = Some(fields);
483            }
484            BodyFrame::End => {}
485        }
486        if end {
487            if let Framing::Fixed(length) | Framing::Chunked(Some(length)) = self.framing {
488                if self.total != length {
489                    return Err(Error::LengthMismatch);
490                }
491            }
492            if matches!(self.framing, Framing::Chunked(_)) {
493                output.write_all(b"0\r\n", cancellation).await?;
494                if let Some(trailers) = &self.trailers {
495                    output.write_fields(trailers.pairs(), cancellation).await?;
496                }
497                output.write_all(b"\r\n", cancellation).await?;
498            }
499        }
500        Ok(())
501    }
502}
503
504// Borrow semantic fields and describe only the framing additions. This avoids
505// constructing/cloning a second semantic message just to serialize a wire head.
506struct Plan<'a> {
507    response: &'a ResponseMetadata,
508    version: Version,
509    framing: Framing,
510    persistent: bool,
511    tunnel: bool,
512    content_length: Option<u64>,
513    chunked: bool,
514    upgrade: Option<&'a [u8]>,
515    connection_upgrade: bool,
516    connection: Option<&'static [u8]>,
517}
518
519fn plan<'a>(
520    exchange: &Exchange,
521    response: &'a ResponseMetadata,
522    length: Option<u64>,
523    may_have_trailers: bool,
524) -> Result<Plan<'a>, Error> {
525    if !response.status.is_final() {
526        return Err(Error::Invalid("final response required"));
527    }
528    let fields = &response.fields;
529    wire::output_fields(fields.pairs())?;
530    let version = exchange.version();
531    if response.version.is_some_and(|v| v != version) {
532        return Err(Error::Invalid("response version differs from request"));
533    }
534    // Derive framing and connection facts together from the initial section.
535    let facts = wire::response_framing(fields)?;
536    let declared = wire::declared_length(response.content_length, facts.length)?;
537    if declared.is_some() && facts.chunked {
538        return Err(Error::Invalid("content-length with transfer-encoding"));
539    }
540    let chunked = facts.chunked;
541    let connect = exchange.is_connect() && response.status.is_success();
542    let upgrade = response.status == Status::SWITCHING_PROTOCOLS;
543    let tunnel = connect || upgrade;
544    let status_bodyless =
545        response.status == Status::NO_CONTENT || response.status == Status::NOT_MODIFIED;
546    let absent = !tunnel && (exchange.is_head() || status_bodyless);
547    let mut persistent = exchange.is_persistent()
548        && !exchange.is_abandoned()
549        && response.persistent != Some(false)
550        && !facts.close
551        && !tunnel;
552    let mut content_length = response.content_length.filter(|_| facts.length.is_none());
553    let mut generated_chunked = false;
554    let mut selected_upgrade = None;
555    let mut connection_upgrade = false;
556    let mut connection = None;
557    let framing;
558    if tunnel {
559        if !exchange.is_complete() {
560            return Err(Error::Invalid("switch with unread request body"));
561        }
562        if declared.is_some() || chunked {
563            return Err(Error::Invalid("framing fields on tunnel response"));
564        }
565        if upgrade {
566            if version != Version::Http11 || !exchange.has_upgrade() {
567                return Err(Error::Invalid("unsolicited upgrade"));
568            }
569            if facts.duplicate_upgrade {
570                return Err(Error::Invalid("duplicate singleton field"));
571            }
572            let selected = response
573                .protocol
574                .as_ref()
575                .map(|p| p.as_str().as_bytes())
576                .or(facts.upgrade)
577                .ok_or(Error::Invalid("missing selected protocol"))?;
578            if !wire::token(selected) || !exchange.accepts_upgrade(selected) {
579                return Err(Error::Invalid("upgrade protocol was not offered"));
580            }
581            selected_upgrade = Some(selected);
582            connection_upgrade = !facts.connection_upgrade;
583        } else if response.protocol.is_some() {
584            return Err(Error::Invalid("protocol on traditional CONNECT"));
585        }
586        framing = Framing::Raw;
587    } else {
588        if response.protocol.is_some() {
589            return Err(Error::Invalid("protocol without upgrade status"));
590        }
591        if facts.upgrade.is_some() {
592            connection_upgrade = !facts.connection_upgrade;
593        }
594        if chunked && version != Version::Http11 {
595            return Err(Error::Invalid("HTTP/1.0 transfer-encoding"));
596        }
597        if absent {
598            if chunked {
599                return Err(Error::Invalid(
600                    "trailers or transfer-encoding on bodyless response",
601                ));
602            }
603            if response.status == Status::NO_CONTENT {
604                if declared.is_some() {
605                    return Err(Error::Invalid("content-length on 204"));
606                }
607            } else if declared.is_none() {
608                content_length = length;
609            }
610            framing = Framing::Absent;
611        } else {
612            if let (Some(actual), Some(declared)) = (length, declared) {
613                if actual != declared {
614                    return Err(Error::LengthMismatch);
615                }
616            }
617            if response.status == Status::RESET_CONTENT
618                && (length.is_some_and(|n| n != 0) || declared.is_some_and(|n| n != 0))
619            {
620                return Err(Error::Invalid("content on 205"));
621            }
622            if response.status == Status::RESET_CONTENT
623                && declared.is_none()
624                && !chunked
625                && !may_have_trailers
626                && !facts.trailer
627            {
628                content_length = Some(0);
629                framing = Framing::Fixed(0);
630            } else if chunked || (may_have_trailers && version == Version::Http11) || facts.trailer
631            {
632                if version != Version::Http11 {
633                    return Err(Error::Invalid("trailers require HTTP/1.1 chunked framing"));
634                }
635                content_length = None;
636                generated_chunked = !chunked;
637                framing = Framing::Chunked(declared.or(length));
638            } else if let Some(length) = declared.or(length) {
639                if declared.is_none() {
640                    content_length = Some(length);
641                }
642                framing = Framing::Fixed(length);
643            } else if version == Version::Http11 {
644                generated_chunked = true;
645                framing = Framing::Chunked(declared.or(length));
646            } else {
647                persistent = false;
648                framing = Framing::Raw;
649            }
650        }
651        if !persistent {
652            if !facts.close {
653                connection = Some(&b"close"[..]);
654            }
655        } else if version == Version::Http10 && !facts.keep_alive {
656            connection = Some(&b"keep-alive"[..]);
657        }
658    }
659    Ok(Plan {
660        response,
661        version,
662        framing,
663        persistent,
664        tunnel,
665        content_length,
666        chunked: generated_chunked,
667        upgrade: selected_upgrade,
668        connection_upgrade,
669        connection,
670    })
671}
672
673fn decimal(mut value: u64, digits: &mut [u8; 20]) -> &[u8] {
674    let mut start = digits.len();
675    loop {
676        start -= 1;
677        digits[start] = b'0' + (value % 10) as u8;
678        value /= 10;
679        if value == 0 {
680            return &digits[start..];
681        }
682    }
683}
684
685pub(crate) fn validate_trailer_fields(fields: &[base_http::Field]) -> Result<(), Error> {
686    let policy = base_http::headers::Policy::default();
687    for field in fields {
688        if !policy.get(field.name()).trailer
689            || ["keep-alive", "proxy-connection"]
690                .iter()
691                .any(|name| field.name().eq_ignore_ascii_case(name))
692        {
693            return Err(base_http::FieldError::InvalidTrailer(field.name().to_owned()).into());
694        }
695    }
696    Ok(())
697}
698
699#[cfg(test)]
700mod tests {
701    use super::*;
702    use crate::{Options, RequestReader};
703    use base_endpoint::stream::ReadableStream;
704    use base_http::{Fields, Protocol, body::BufferedBody};
705    use base_scheduler::OperationResult;
706    use futures_lite::future::block_on;
707
708    struct Bytes(&'static [u8]);
709    impl ReadableStream for Bytes {
710        async fn read_into(
711            &mut self,
712            buffer: &mut [u8],
713            _: &Cancellation,
714        ) -> OperationResult<usize> {
715            let n = self.0.len().min(buffer.len());
716            buffer[..n].copy_from_slice(&self.0[..n]);
717            self.0 = &self.0[n..];
718            Ok(n)
719        }
720        async fn close(&mut self) -> io::Result<()> {
721            Ok(())
722        }
723    }
724
725    // Accepts no bytes, so the first transport write fails and reveals where a batch boundary fell.
726    struct Refusing;
727    impl WritableStream for Refusing {
728        async fn write(&mut self, _: &[u8], _: &Cancellation) -> OperationResult<usize> {
729            Err(io::Error::from(io::ErrorKind::BrokenPipe).into())
730        }
731        async fn flush(&mut self, _: &Cancellation) -> OperationResult<()> {
732            Ok(())
733        }
734        async fn close(&mut self) -> io::Result<()> {
735            Ok(())
736        }
737    }
738
739    fn exchange(bytes: &'static [u8]) -> Exchange {
740        let mut input = RequestReader::new(Bytes(bytes), Options::default()).unwrap();
741        let exchange = block_on(input.read_request(&Cancellation::never()))
742            .unwrap()
743            .unwrap()
744            .1;
745        block_on(input.close()).unwrap();
746        exchange
747    }
748
749    // A field occupying exactly `size` bytes of the batch.
750    fn filler(size: usize) -> (&'static str, Vec<u8>) {
751        ("x", vec![b'v'; size - 5])
752    }
753
754    fn is_broken_pipe(result: Result<impl Sized, Error>) -> bool {
755        matches!(result, Err(Error::Io(error)) if error.kind() == io::ErrorKind::BrokenPipe)
756    }
757
758    #[test]
759    fn a_head_field_crossing_the_batch_limit_sends_the_batch_first() {
760        const GET: &[u8] = b"GET / HTTP/1.1\r\nHost: x\r\n\r\n";
761        const UPGRADE: &[u8] =
762            b"GET / HTTP/1.1\r\nHost: x\r\nConnection: upgrade\r\nUpgrade: echo\r\n\r\n";
763        const OK: usize = b"HTTP/1.1 200 OK\r\n".len();
764        const SWITCHING: usize = b"HTTP/1.1 101 Switching Protocols\r\n".len();
765        // Each response leaves one free byte in the batch before the generated field.
766        let full = |start: usize| filler(OUTPUT_BATCH_LIMIT - 1 - start);
767        type Case = (&'static [u8], Response<BufferedBody<Vec<u8>>>);
768        let mut cases: Vec<Case> = Vec::new();
769
770        let mut upgrade = Response::new(Status::SWITCHING_PROTOCOLS, BufferedBody::default());
771        upgrade.protocol = Some(Protocol::new("echo").unwrap());
772        let (name, value) = full(SWITCHING);
773        upgrade.fields.add(name, value).unwrap();
774        cases.push((UPGRADE, upgrade));
775
776        let mut advertised = Response::new(Status::OK, BufferedBody::default());
777        advertised.fields.add("Upgrade", "h2c").unwrap();
778        let (name, value) = full(OK + "Upgrade: h2c\r\n".len());
779        advertised.fields.add(name, value).unwrap();
780        cases.push((GET, advertised));
781
782        let mut length = Response::new(Status::OK, BufferedBody::from(b"hi".to_vec()));
783        let (name, value) = full(OK);
784        length.fields.add(name, value).unwrap();
785        cases.push((GET, length));
786
787        let mut chunked = Response::new(Status::OK, BufferedBody::default());
788        chunked.fields.add("Trailer", "x-digest").unwrap();
789        let (name, value) = full(OK + "Trailer: x-digest\r\n".len());
790        chunked.fields.add(name, value).unwrap();
791        cases.push((GET, chunked));
792
793        let mut close = Response::new(Status::OK, BufferedBody::default());
794        close.persistent = Some(false);
795        close.fields.add("Content-Length", "0").unwrap();
796        let (name, value) = full(OK + "Content-Length: 0\r\n".len());
797        close.fields.add(name, value).unwrap();
798        cases.push((GET, close));
799
800        // A large value, a name filling the batch, and a name leaving no room for its separator.
801        let mut value = Response::new(Status::OK, BufferedBody::default());
802        value
803            .fields
804            .add("x", vec![b'v'; OUTPUT_BATCH_LIMIT + 1])
805            .unwrap();
806        cases.push((GET, value));
807        let mut name = Response::new(Status::OK, BufferedBody::default());
808        name.fields
809            .add("x".repeat(OUTPUT_BATCH_LIMIT), "v")
810            .unwrap();
811        cases.push((GET, name));
812        let mut separator = Response::new(Status::OK, BufferedBody::default());
813        separator
814            .fields
815            .add("x".repeat(OUTPUT_BATCH_LIMIT - 1 - OK), "v")
816            .unwrap();
817        cases.push((GET, separator));
818
819        for (index, (request, mut response)) in cases.into_iter().enumerate() {
820            let mut output = ResponseWriter::new(Refusing);
821            let result = block_on(output.write_response(
822                &exchange(request),
823                &mut response,
824                &Cancellation::never(),
825            ));
826            assert!(is_broken_pipe(result), "case {index}");
827            assert!(!output.is_usable());
828        }
829    }
830
831    #[test]
832    fn interim_heads_crossing_the_batch_limit_send_the_batch_first() {
833        const EARLY_HINTS: usize = b"HTTP/1.1 103 Early Hints\r\n".len();
834        let large = ("link", vec![b'v'; OUTPUT_BATCH_LIMIT + 1]);
835        let full = filler(OUTPUT_BATCH_LIMIT - 1 - EARLY_HINTS);
836        for (name, value) in [large, full] {
837            let mut output = ResponseWriter::new(Refusing);
838            let mut response = ResponseMetadata::new(Status::new(103).unwrap());
839            response.fields.add(name, value).unwrap();
840            let result = block_on(
841                output
842                    .interim(Version::Http11)
843                    .on_interim_response(response, &Cancellation::never()),
844            );
845            assert!(is_broken_pipe(result));
846            assert!(!output.is_usable());
847        }
848    }
849
850    #[test]
851    fn body_frames_crossing_the_batch_limit_send_the_batch_first() {
852        let data = |bytes: &[u8]| BodyFrame::Data {
853            data: bytes.to_vec(),
854            end: false,
855        };
856        let mut trailers = Fields::new();
857        trailers
858            .add("x-digest", vec![b'v'; OUTPUT_BATCH_LIMIT + 1])
859            .unwrap();
860        let large = vec![b'v'; OUTPUT_BATCH_LIMIT];
861        // Each case batches `pending` bytes from earlier frames before writing its frame.
862        let cases = [
863            (Framing::Chunked(None), OUTPUT_BATCH_LIMIT - 2, data(b"abc")),
864            (Framing::Chunked(None), 1, data(&large)),
865            (Framing::Chunked(None), OUTPUT_BATCH_LIMIT - 7, data(b"abc")),
866            (Framing::Fixed(OUTPUT_BATCH_LIMIT as u64), 1, data(&large)),
867            (
868                Framing::Chunked(None),
869                OUTPUT_BATCH_LIMIT - 2,
870                BodyFrame::End,
871            ),
872            (
873                Framing::Chunked(None),
874                OUTPUT_BATCH_LIMIT - 4,
875                BodyFrame::End,
876            ),
877            (
878                Framing::Chunked(None),
879                1,
880                BodyFrame::Headers {
881                    fields: trailers,
882                    end: true,
883                },
884            ),
885        ];
886        for (index, (framing, pending, frame)) in cases.into_iter().enumerate() {
887            let never = Cancellation::never();
888            let mut output = ResponseWriter::new(Refusing);
889            block_on(output.write_all(&vec![b'.'; pending], &never)).unwrap();
890            let mut encoder = BodyEncoder::new(framing, false);
891            let result = block_on(encoder.write_frame(&mut output, frame, &never));
892            assert!(is_broken_pipe(result), "case {index}");
893        }
894    }
895
896    #[test]
897    fn empty_fixed_data_needs_no_transport_write() {
898        let never = Cancellation::never();
899        let mut output = ResponseWriter::new(Refusing);
900        let mut encoder = BodyEncoder::new(Framing::Fixed(0), false);
901        let frame = BodyFrame::Data {
902            data: Vec::new(),
903            end: true,
904        };
905        block_on(encoder.write_frame(&mut output, frame, &never)).unwrap();
906        block_on(output.flush(&never)).unwrap();
907        block_on(output.close()).unwrap();
908    }
909}