Skip to main content

grit_lib/transport/
stateless_http.rs

1//! Smart-HTTP [`Connection`] over stateless POST rounds (upload-pack) or a single
2//! POST body (receive-pack).
3//!
4//! Lets [`crate::fetch::fetch_remote`] and [`crate::push::push_remote`] drive
5//! smart HTTP without a separate negotiation implementation.
6
7use std::io::{self, Cursor, Read, Write};
8use std::sync::mpsc::{Receiver, RecvTimeoutError, SyncSender, TrySendError};
9use std::sync::Arc;
10use std::time::Duration;
11
12use crate::error::{Error, Result};
13use crate::objects::ObjectId;
14use crate::progress::{Cancel, NeverCancel};
15use crate::transport::http::HttpClient;
16use crate::transport::rpc_channel::RpcChannel;
17use crate::transport::{Connection, Service};
18
19/// Reader that surfaces a stored I/O error on the first read (instead of EOF).
20struct ErrRead {
21    err: io::Error,
22}
23
24impl Read for ErrRead {
25    fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
26        Err(std::mem::replace(
27            &mut self.err,
28            io::Error::other("transport error already reported"),
29        ))
30    }
31}
32
33/// Receive-pack bodies up to this size are sent in one buffered request; larger
34/// ones (a pack, in practice) stream with chunked encoding. Git's default
35/// `http.postBuffer`.
36const LARGE_POST: usize = 1024 * 1024;
37
38/// Bytes handed to the upload thread at a time.
39const STREAM_CHUNK: usize = 256 * 1024;
40
41/// Chunks queued for the upload thread before the writer waits for the network.
42const STREAM_QUEUE: usize = 4;
43
44/// A piece of a streamed request body.
45enum BodyChunk {
46    Data(Vec<u8>),
47    End,
48}
49
50/// The body of a POST still being produced, read by the request as it is sent.
51struct ChunkReader {
52    chunks: Receiver<BodyChunk>,
53    chunk: Vec<u8>,
54    pos: usize,
55}
56
57impl Read for ChunkReader {
58    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
59        while self.pos == self.chunk.len() {
60            match self.chunks.recv() {
61                Ok(BodyChunk::Data(chunk)) => {
62                    self.chunk = chunk;
63                    self.pos = 0;
64                }
65                Ok(BodyChunk::End) => return Ok(0),
66                // The writer went away mid-body: fail the request rather than
67                // let a truncated body look complete.
68                Err(_) => {
69                    return Err(io::Error::new(
70                        io::ErrorKind::UnexpectedEof,
71                        "request body abandoned",
72                    ))
73                }
74            }
75        }
76        let n = buf.len().min(self.chunk.len() - self.pos);
77        buf[..n].copy_from_slice(&self.chunk[self.pos..self.pos + n]);
78        self.pos += n;
79        Ok(n)
80    }
81}
82
83/// How often a wait for the receive-pack request checks for a cancel.
84const CANCEL_POLL: Duration = Duration::from_millis(20);
85
86/// A receive-pack response read to the end, replaying the error that ended it
87/// (a reset after the report, say) once its bytes are consumed.
88struct ReadResponse {
89    bytes: Cursor<Vec<u8>>,
90    error: Option<io::Error>,
91}
92
93impl ReadResponse {
94    fn read_from(mut reader: Box<dyn Read + Send>) -> Self {
95        let mut bytes = Vec::new();
96        let error = reader.read_to_end(&mut bytes).err();
97        Self {
98            bytes: Cursor::new(bytes),
99            error,
100        }
101    }
102}
103
104impl Read for ReadResponse {
105    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
106        match self.bytes.read(buf)? {
107            0 if !buf.is_empty() => self.error.take().map_or(Ok(0), Err),
108            n => Ok(n),
109        }
110    }
111}
112
113/// A receive-pack request running on its own thread, which also reads the
114/// (small) report so waiting for it can be interrupted.
115struct PendingResponse(Receiver<Result<ReadResponse>>);
116
117impl PendingResponse {
118    fn spawn<F>(request: F) -> io::Result<Self>
119    where
120        F: FnOnce() -> Result<Box<dyn Read + Send>> + Send + 'static,
121    {
122        let (tx, rx) = std::sync::mpsc::channel();
123        std::thread::Builder::new()
124            .name("grit-http-post".to_owned())
125            .spawn(move || {
126                let _ = tx.send(request().map(ReadResponse::read_from));
127            })?;
128        Ok(Self(rx))
129    }
130
131    /// The response, or [`Error::Cancelled`] once `cancel` fires first. A cancelled
132    /// request is left to finish on its own thread.
133    fn wait(&self, cancel: &dyn Cancel) -> Result<ReadResponse> {
134        loop {
135            match self.0.recv_timeout(CANCEL_POLL) {
136                Ok(response) => return response,
137                Err(RecvTimeoutError::Timeout) if cancel.is_cancelled() => {
138                    return Err(Error::Cancelled)
139                }
140                Err(RecvTimeoutError::Timeout) => {}
141                Err(RecvTimeoutError::Disconnected) => {
142                    return Err(Error::Message("HTTP upload thread panicked".to_owned()))
143                }
144            }
145        }
146    }
147}
148
149/// A receive-pack POST running on its own thread while the body is written.
150struct StreamingPost {
151    chunks: SyncSender<BodyChunk>,
152    response: PendingResponse,
153}
154
155impl StreamingPost {
156    fn start<C: HttpClient + 'static>(
157        client: Arc<C>,
158        url: String,
159        content_type: String,
160        accept: String,
161    ) -> io::Result<Self> {
162        let (chunks, rx) = std::sync::mpsc::sync_channel(STREAM_QUEUE);
163        let response = PendingResponse::spawn(move || {
164            let mut body = ChunkReader {
165                chunks: rx,
166                chunk: Vec::new(),
167                pos: 0,
168            };
169            client.post_streaming(&url, &content_type, &accept, &mut body, None)
170        })?;
171        Ok(Self { chunks, response })
172    }
173
174    /// Queue `chunk`; when the request already ended, its error.
175    fn send(self, chunk: Vec<u8>) -> std::result::Result<Self, Error> {
176        match self.chunks.send(BodyChunk::Data(chunk)) {
177            Ok(()) => Ok(self),
178            Err(_) => Err(self.ended_early()),
179        }
180    }
181
182    /// Queue `item` while the upload thread makes room, unless `cancel` fires first.
183    fn send_until(&self, mut item: BodyChunk, cancel: &dyn Cancel) -> Result<()> {
184        loop {
185            match self.chunks.try_send(item) {
186                Ok(()) => return Ok(()),
187                Err(TrySendError::Full(back)) => {
188                    if cancel.is_cancelled() {
189                        return Err(Error::Cancelled);
190                    }
191                    item = back;
192                    std::thread::sleep(CANCEL_POLL);
193                }
194                Err(TrySendError::Disconnected(_)) => return Err(self.ended_early()),
195            }
196        }
197    }
198
199    /// Why a request whose body could not be queued ended.
200    fn ended_early(&self) -> Error {
201        match self.response.wait(&NeverCancel) {
202            Err(e) => e,
203            Ok(_) => Error::Message("server answered before the request body was sent".into()),
204        }
205    }
206
207    /// Queue the last `chunk`, end the body and wait for the response. When
208    /// `cancel` fires before the body ended, dropping `self` abandons it.
209    fn finish(self, chunk: Vec<u8>, cancel: &dyn Cancel) -> Result<ReadResponse> {
210        self.send_until(BodyChunk::Data(chunk), cancel)?;
211        self.send_until(BodyChunk::End, cancel)?;
212        self.response.wait(cancel)
213    }
214}
215
216/// A live smart-HTTP connection backed by [`HttpClient`] stateless RPC.
217///
218/// A receive-pack request body larger than Git's default `http.postBuffer`
219/// (1 MiB) is not held in memory: after a small probe request settles
220/// authentication, it streams to the server through
221/// [`HttpClient::post_streaming`] while the caller is still writing it.
222pub struct StatelessHttpConnection<C: HttpClient> {
223    client: Arc<C>,
224    service: Service,
225    repo_url: String,
226    post_url: String,
227    content_type: String,
228    accept: String,
229    git_protocol: Option<String>,
230    adv_refs: Vec<(String, ObjectId)>,
231    caps: Vec<String>,
232    head_symref: Option<String>,
233    protocol_version: u8,
234    object_format: String,
235    /// Bytes already sent on prior upload-pack POSTs (stateless replay prefix).
236    sent_state: Vec<u8>,
237    pending: Vec<u8>,
238    response: Option<Box<dyn Read + Send>>,
239    fail_read: Option<ErrRead>,
240    empty_response: Cursor<Vec<u8>>,
241    recv_body: Vec<u8>,
242    recv_stream: Option<StreamingPost>,
243    recv_posted: bool,
244}
245
246impl<C: HttpClient + 'static> StatelessHttpConnection<C> {
247    /// Build a connection after `info/refs` discovery.
248    #[allow(clippy::too_many_arguments)]
249    pub fn new(
250        client: C,
251        service: Service,
252        repo_url: String,
253        protocol_version: u8,
254        adv_refs: Vec<(String, ObjectId)>,
255        caps: Vec<String>,
256        head_symref: Option<String>,
257        object_format: String,
258        git_protocol: Option<String>,
259    ) -> Self {
260        let wire = service.wire_name();
261        let base = repo_url.trim_end_matches('/');
262        let post_url = format!("{base}/{wire}");
263        let content_type = format!("application/x-{wire}-request");
264        let accept = format!("application/x-{wire}-result");
265        Self {
266            client: Arc::new(client),
267            service,
268            repo_url,
269            post_url,
270            content_type,
271            accept,
272            git_protocol,
273            adv_refs,
274            caps,
275            head_symref,
276            protocol_version,
277            object_format,
278            sent_state: Vec::new(),
279            pending: Vec::new(),
280            response: None,
281            fail_read: None,
282            empty_response: Cursor::new(Vec::new()),
283            recv_body: Vec::new(),
284            recv_stream: None,
285            recv_posted: false,
286        }
287    }
288
289    /// Repository URL used for subsequent POSTs.
290    #[must_use]
291    pub fn repo_url(&self) -> &str {
292        &self.repo_url
293    }
294
295    /// The server's advertised object format (`sha1` or `sha256`).
296    #[must_use]
297    pub fn object_format(&self) -> &str {
298        &self.object_format
299    }
300
301    fn drain_response(&mut self) {
302        if let Some(mut r) = self.response.take() {
303            let _ = crate::pack_receive::copy_reader_to_writer(&mut r, &mut io::sink());
304        }
305    }
306
307    fn set_transport_err(&mut self, err: &Error) {
308        self.fail_read = Some(ErrRead {
309            err: io::Error::other(err.to_string()),
310        });
311        self.response = None;
312    }
313
314    fn upload_pack_post_due(&self) -> bool {
315        if self.pending.is_empty() {
316            return false;
317        }
318        // Stateless RPC posts on section flushes and on the final `done` pkt-line
319        // (there is no trailing `0000` after `done` before reading the ACK/pack).
320        self.pending.ends_with(b"0000") || self.pending.ends_with(b"done\n")
321    }
322
323    fn maybe_post_on_flush(&mut self) -> Result<()> {
324        if self.service != Service::UploadPack {
325            return Ok(());
326        }
327        if self.upload_pack_post_due() {
328            self.post_upload_pack_round()?;
329        }
330        Ok(())
331    }
332
333    fn post_upload_pack_round(&mut self) -> Result<()> {
334        if self.pending.is_empty() {
335            return Ok(());
336        }
337        let body = if self.protocol_version >= 2 {
338            // Protocol v2 stateless HTTP sends one command per POST (ls-refs, fetch, …),
339            // not a cumulative v0/v1 want/have transcript.
340            std::mem::take(&mut self.pending)
341        } else {
342            let mut body = self.sent_state.clone();
343            body.extend_from_slice(&self.pending);
344            self.sent_state.extend_from_slice(&self.pending);
345            self.pending.clear();
346            body
347        };
348        self.response = Some(self.request(&body)?);
349        Ok(())
350    }
351
352    /// Send the receive-pack request (or the rest of its streamed body) and wait
353    /// for the report, giving up once `cancel` fires.
354    fn post_receive_pack(&mut self, cancel: &dyn Cancel) -> Result<()> {
355        if self.recv_posted {
356            return Ok(());
357        }
358        self.recv_posted = true;
359        let body = std::mem::take(&mut self.recv_body);
360        let response = match self.recv_stream.take() {
361            Some(stream) => stream.finish(body, cancel),
362            None => {
363                let client = Arc::clone(&self.client);
364                let (url, content_type, accept) = (
365                    self.post_url.clone(),
366                    self.content_type.clone(),
367                    self.accept.clone(),
368                );
369                PendingResponse::spawn(move || {
370                    client.post_into_reader(&url, &content_type, &accept, &body, None)
371                })
372                .map_err(Error::Io)
373                .and_then(|pending| pending.wait(cancel))
374            }
375        };
376        let response = response.inspect_err(|e| self.set_transport_err(e))?;
377        self.response = Some(Box::new(response));
378        Ok(())
379    }
380
381    /// Buffer receive-pack body bytes, streaming them once the body outgrows
382    /// [`LARGE_POST`].
383    fn write_receive_pack(&mut self, buf: &[u8]) -> Result<()> {
384        if self.recv_posted {
385            return Err(Error::Message(
386                "receive-pack request already sent".to_owned(),
387            ));
388        }
389        self.recv_body.extend_from_slice(buf);
390        if self.recv_stream.is_none() {
391            if self.recv_body.len() <= LARGE_POST {
392                return Ok(());
393            }
394            self.probe_receive_pack()?;
395            self.recv_stream = Some(StreamingPost::start(
396                Arc::clone(&self.client),
397                self.post_url.clone(),
398                self.content_type.clone(),
399                self.accept.clone(),
400            )?);
401        }
402        if self.recv_body.len() < STREAM_CHUNK {
403            return Ok(());
404        }
405        let chunk = std::mem::replace(&mut self.recv_body, Vec::with_capacity(STREAM_CHUNK));
406        if let Some(stream) = self.recv_stream.take() {
407            match stream.send(chunk) {
408                Ok(stream) => self.recv_stream = Some(stream),
409                Err(e) => {
410                    self.recv_posted = true;
411                    self.set_transport_err(&e);
412                    return Err(e);
413                }
414            }
415        }
416        Ok(())
417    }
418
419    /// Post an empty command list before a streamed body, as Git does, so an
420    /// authentication challenge is answered on a request that can be resent.
421    fn probe_receive_pack(&mut self) -> Result<()> {
422        let mut reply = self.client.post_into_reader(
423            &self.post_url,
424            &self.content_type,
425            &self.accept,
426            b"0000",
427            None,
428        )?;
429        io::copy(&mut reply, &mut io::sink()).map_err(Error::Io)?;
430        Ok(())
431    }
432
433    fn ensure_response_for_read(&mut self) -> Result<()> {
434        if self.service == Service::ReceivePack && !self.recv_posted {
435            self.post_receive_pack(&NeverCancel)?;
436        }
437        Ok(())
438    }
439}
440
441impl<C: HttpClient + 'static> RpcChannel for StatelessHttpConnection<C> {
442    fn request(&mut self, body: &[u8]) -> Result<Box<dyn Read + Send>> {
443        self.drain_response();
444        self.client
445            .post_into_reader(
446                &self.post_url,
447                &self.content_type,
448                &self.accept,
449                body,
450                self.git_protocol.as_deref(),
451            )
452            .inspect_err(|e| {
453                self.set_transport_err(e);
454            })
455    }
456}
457
458impl<C: HttpClient + 'static> Write for StatelessHttpConnection<C> {
459    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
460        match self.service {
461            Service::UploadPack => {
462                self.pending.extend_from_slice(buf);
463                self.maybe_post_on_flush()
464                    .map_err(|e| io::Error::other(e.to_string()))?;
465                Ok(buf.len())
466            }
467            Service::ReceivePack => {
468                // A large object arrives in one write; taking a chunk at a time keeps
469                // the buffered body bounded.
470                let taken = &buf[..buf.len().min(STREAM_CHUNK)];
471                self.write_receive_pack(taken)
472                    .map_err(|e| io::Error::other(e.to_string()))?;
473                Ok(taken.len())
474            }
475        }
476    }
477
478    fn flush(&mut self) -> io::Result<()> {
479        if self.service == Service::UploadPack {
480            self.maybe_post_on_flush()
481                .map_err(|e| io::Error::other(e.to_string()))?;
482        }
483        Ok(())
484    }
485}
486
487impl<C: HttpClient + 'static> Connection for StatelessHttpConnection<C> {
488    fn reader(&mut self) -> &mut dyn Read {
489        if self.fail_read.is_none() {
490            if let Err(e) = self.ensure_response_for_read() {
491                self.fail_read = Some(ErrRead {
492                    err: io::Error::other(e.to_string()),
493                });
494            }
495        }
496        if let Some(r) = self.fail_read.as_mut() {
497            return r;
498        }
499        match self.response.as_mut() {
500            Some(r) => r.as_mut(),
501            None => &mut self.empty_response,
502        }
503    }
504
505    fn writer(&mut self) -> &mut dyn Write {
506        self
507    }
508
509    fn advertised_refs(&self) -> &[(String, ObjectId)] {
510        &self.adv_refs
511    }
512
513    fn capabilities(&self) -> &[String] {
514        &self.caps
515    }
516
517    fn head_symref(&self) -> Option<&str> {
518        self.head_symref.as_deref()
519    }
520
521    fn protocol_version(&self) -> u8 {
522        self.protocol_version
523    }
524
525    fn stateless_rpc(&self) -> bool {
526        true
527    }
528
529    fn finish_send(&mut self) {
530        if self.service == Service::ReceivePack && !self.recv_posted {
531            if let Err(e) = self.post_receive_pack(&NeverCancel) {
532                self.set_transport_err(&e);
533            }
534        }
535    }
536
537    fn wait_for_response(&mut self, cancel: &dyn Cancel) -> Result<()> {
538        if self.service == Service::ReceivePack {
539            self.post_receive_pack(cancel)?;
540        }
541        Ok(())
542    }
543}
544
545#[cfg(test)]
546mod tests {
547    use super::*;
548    use std::sync::mpsc;
549    use std::sync::Mutex;
550    use std::time::Duration;
551
552    /// What the fake server saw.
553    #[derive(Default)]
554    struct Seen {
555        /// Bodies of buffered POSTs, in order.
556        buffered: Vec<Vec<u8>>,
557        /// The streamed body, once read to the end.
558        streamed: Option<Vec<u8>>,
559        /// Largest single read of the streamed body.
560        largest_read: usize,
561    }
562
563    /// A receive-pack endpoint that reads streamed bodies like a server would.
564    struct FakeServer {
565        seen: Arc<Mutex<Seen>>,
566        /// Fail a streamed POST before reading its body.
567        refuse_stream: bool,
568        /// Where a streamed POST reports how reading its body ended.
569        body_end: Option<Mutex<mpsc::Sender<io::Result<usize>>>>,
570    }
571
572    impl FakeServer {
573        fn new() -> (Self, Arc<Mutex<Seen>>) {
574            let seen = Arc::new(Mutex::new(Seen::default()));
575            let server = Self {
576                seen: Arc::clone(&seen),
577                refuse_stream: false,
578                body_end: None,
579            };
580            (server, seen)
581        }
582    }
583
584    impl HttpClient for FakeServer {
585        fn get(&self, _: &str, _: Option<&str>) -> Result<Vec<u8>> {
586            Err(Error::Message("no GETs here".to_owned()))
587        }
588
589        fn post(&self, _: &str, _: &str, _: &str, body: &[u8], _: Option<&str>) -> Result<Vec<u8>> {
590            self.seen
591                .lock()
592                .unwrap_or_else(|e| e.into_inner())
593                .buffered
594                .push(body.to_vec());
595            Ok(b"report".to_vec())
596        }
597
598        fn post_streaming(
599            &self,
600            _: &str,
601            _: &str,
602            _: &str,
603            body: &mut dyn Read,
604            _: Option<&str>,
605        ) -> Result<Box<dyn Read + Send>> {
606            if self.refuse_stream {
607                return Err(Error::Message("HTTP 500 from the fake server".to_owned()));
608            }
609            let mut got = Vec::new();
610            let mut largest = 0;
611            let mut buf = vec![0u8; 1 << 20];
612            let end = loop {
613                match body.read(&mut buf) {
614                    Ok(0) => break Ok(got.len()),
615                    Ok(n) => {
616                        largest = largest.max(n);
617                        got.extend_from_slice(&buf[..n]);
618                    }
619                    Err(e) => break Err(e),
620                }
621            };
622            let failed = end.is_err();
623            if let Some(tx) = &self.body_end {
624                let _ = tx.lock().unwrap_or_else(|e| e.into_inner()).send(end);
625            }
626            if failed {
627                return Err(Error::Message("body did not finish".to_owned()));
628            }
629            let mut seen = self.seen.lock().unwrap_or_else(|e| e.into_inner());
630            seen.streamed = Some(got);
631            seen.largest_read = largest;
632            Ok(Box::new(Cursor::new(b"streamed report".to_vec())))
633        }
634    }
635
636    fn receive_pack(server: FakeServer) -> StatelessHttpConnection<FakeServer> {
637        StatelessHttpConnection::new(
638            server,
639            Service::ReceivePack,
640            "http://h/r.git".to_owned(),
641            0,
642            Vec::new(),
643            Vec::new(),
644            None,
645            "sha1".to_owned(),
646            None,
647        )
648    }
649
650    fn body(len: usize) -> Vec<u8> {
651        (0..len).map(|i| (i % 251) as u8).collect()
652    }
653
654    fn read_reply(conn: &mut StatelessHttpConnection<FakeServer>) -> io::Result<Vec<u8>> {
655        let mut reply = Vec::new();
656        conn.reader().read_to_end(&mut reply)?;
657        Ok(reply)
658    }
659
660    #[test]
661    fn small_receive_pack_bodies_are_posted_once_whole() {
662        let (server, seen) = FakeServer::new();
663        let mut conn = receive_pack(server);
664        let data = body(4096);
665        conn.writer().write_all(&data).unwrap();
666        assert_eq!(read_reply(&mut conn).unwrap(), b"report");
667        let seen = seen.lock().unwrap();
668        assert_eq!(seen.buffered, vec![data]);
669        assert!(seen.streamed.is_none());
670    }
671
672    #[test]
673    fn large_receive_pack_bodies_stream_after_a_probe_without_being_held() {
674        let (server, seen) = FakeServer::new();
675        let mut conn = receive_pack(server);
676        let data = body(6 * LARGE_POST + 12345);
677        for piece in data.chunks(10_000) {
678            conn.writer().write_all(piece).unwrap();
679            assert!(
680                conn.recv_body.len() <= LARGE_POST + 10_000,
681                "the connection holds at most one post buffer"
682            );
683        }
684        assert!(conn.recv_stream.is_some());
685        assert_eq!(read_reply(&mut conn).unwrap(), b"streamed report");
686
687        let seen = seen.lock().unwrap();
688        assert_eq!(
689            seen.buffered,
690            vec![b"0000".to_vec()],
691            "only the probe is buffered"
692        );
693        assert_eq!(seen.streamed.as_deref(), Some(&data[..]));
694        assert!(seen.largest_read <= LARGE_POST + 10_000);
695    }
696
697    #[test]
698    fn one_huge_write_is_streamed_a_chunk_at_a_time() {
699        let (server, seen) = FakeServer::new();
700        let mut conn = receive_pack(server);
701        let data = body(8 * LARGE_POST);
702        let first = conn.writer().write(&data).unwrap();
703        assert!(first <= STREAM_CHUNK, "took {first} bytes in one write");
704        conn.writer().write_all(&data[first..]).unwrap();
705        assert!(conn.recv_body.len() <= STREAM_CHUNK);
706        assert_eq!(read_reply(&mut conn).unwrap(), b"streamed report");
707        let seen = seen.lock().unwrap();
708        assert_eq!(seen.streamed.as_deref(), Some(&data[..]));
709        assert!(seen.largest_read <= LARGE_POST + STREAM_CHUNK);
710    }
711
712    #[test]
713    fn a_refused_stream_fails_the_write_with_the_server_error() {
714        let (mut server, _) = FakeServer::new();
715        server.refuse_stream = true;
716        let mut conn = receive_pack(server);
717        let data = body(LARGE_POST);
718        let mut failure = None;
719        for _ in 0..(STREAM_QUEUE + 8) {
720            if let Err(e) = conn.writer().write_all(&data) {
721                failure = Some(e);
722                break;
723            }
724        }
725        let failure = failure.expect("writes fail once the request is refused");
726        assert!(failure.to_string().contains("HTTP 500"), "{failure}");
727        let after = read_reply(&mut conn).unwrap_err();
728        assert!(after.to_string().contains("HTTP 500"), "{after}");
729    }
730
731    #[test]
732    fn dropping_a_connection_mid_body_fails_the_request_instead_of_ending_it() {
733        let (mut server, _) = FakeServer::new();
734        let (tx, rx) = mpsc::channel();
735        server.body_end = Some(Mutex::new(tx));
736        let mut conn = receive_pack(server);
737        conn.writer().write_all(&body(2 * LARGE_POST)).unwrap();
738        drop(conn);
739        let end = rx
740            .recv_timeout(Duration::from_secs(30))
741            .expect("the request thread reports how the body ended");
742        let err = end.expect_err("an abandoned body must not look complete");
743        assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
744    }
745}