1#![cfg_attr(docsrs, feature(doc_auto_cfg, doc_cfg))]
2#![doc = include_str!("../README.md")]
3#![allow(renamed_and_removed_lints)] #![allow(unknown_lints)] #![warn(missing_docs)]
7#![warn(noop_method_call)]
8#![warn(unreachable_pub)]
9#![warn(clippy::all)]
10#![deny(clippy::await_holding_lock)]
11#![deny(clippy::cargo_common_metadata)]
12#![deny(clippy::cast_lossless)]
13#![deny(clippy::checked_conversions)]
14#![warn(clippy::cognitive_complexity)]
15#![deny(clippy::debug_assert_with_mut_call)]
16#![deny(clippy::exhaustive_enums)]
17#![deny(clippy::exhaustive_structs)]
18#![deny(clippy::expl_impl_clone_on_copy)]
19#![deny(clippy::fallible_impl_from)]
20#![deny(clippy::implicit_clone)]
21#![deny(clippy::large_stack_arrays)]
22#![warn(clippy::manual_ok_or)]
23#![deny(clippy::missing_docs_in_private_items)]
24#![warn(clippy::needless_borrow)]
25#![warn(clippy::needless_pass_by_value)]
26#![warn(clippy::option_option)]
27#![deny(clippy::print_stderr)]
28#![deny(clippy::print_stdout)]
29#![warn(clippy::rc_buffer)]
30#![deny(clippy::ref_option_ref)]
31#![warn(clippy::semicolon_if_nothing_returned)]
32#![warn(clippy::trait_duplication_in_bounds)]
33#![deny(clippy::unchecked_duration_subtraction)]
34#![deny(clippy::unnecessary_wraps)]
35#![warn(clippy::unseparated_literal_suffix)]
36#![deny(clippy::unwrap_used)]
37#![deny(clippy::mod_module_files)]
38#![allow(clippy::let_unit_value)] #![allow(clippy::uninlined_format_args)]
40#![allow(clippy::significant_drop_in_scrutinee)] #![allow(clippy::result_large_err)] #![allow(clippy::needless_raw_string_hashes)] #![allow(clippy::needless_lifetimes)] #![cfg_attr(
48 not(all(feature = "full", feature = "experimental")),
49 allow(unused_imports)
50)]
51
52mod err;
53pub mod request;
54mod response;
55mod util;
56
57use tor_circmgr::{CircMgr, DirInfo};
58use tor_error::bad_api_usage;
59use tor_rtcompat::{Runtime, SleepProvider, SleepProviderExt};
60
61#[cfg(feature = "xz")]
63use async_compression::futures::bufread::XzDecoder;
64use async_compression::futures::bufread::ZlibDecoder;
65#[cfg(feature = "zstd")]
66use async_compression::futures::bufread::ZstdDecoder;
67
68use futures::io::{
69 AsyncBufRead, AsyncBufReadExt, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, BufReader,
70};
71use futures::FutureExt;
72use memchr::memchr;
73use std::sync::Arc;
74use std::time::Duration;
75use tracing::info;
76
77pub use err::{Error, RequestError, RequestFailedError};
78pub use response::{DirResponse, SourceInfo};
79
80pub type Result<T> = std::result::Result<T, Error>;
82
83pub type RequestResult<T> = std::result::Result<T, RequestError>;
85
86#[derive(Copy, Clone, Debug, Eq, PartialEq)]
92#[non_exhaustive]
93pub enum AnonymizedRequest {
94 Anonymized,
96 Direct,
98}
99
100pub async fn get_resource<CR, R, SP>(
113 req: &CR,
114 dirinfo: DirInfo<'_>,
115 runtime: &SP,
116 circ_mgr: Arc<CircMgr<R>>,
117) -> Result<DirResponse>
118where
119 CR: request::Requestable + ?Sized,
120 R: Runtime,
121 SP: SleepProvider,
122{
123 let circuit = circ_mgr.get_or_launch_dir(dirinfo).await?;
124
125 if req.anonymized() == AnonymizedRequest::Anonymized {
126 return Err(bad_api_usage!("Tried to use get_resource for an anonymized request").into());
127 }
128
129 let begin_timeout = Duration::from_secs(5);
131 let source = SourceInfo::from_circuit(&circuit);
132
133 let wrap_err = |error| {
134 Error::RequestFailed(RequestFailedError {
135 source: Some(source.clone()),
136 error,
137 })
138 };
139
140 req.check_circuit(&circuit).await.map_err(wrap_err)?;
141
142 let mut stream = runtime
144 .timeout(begin_timeout, circuit.begin_dir_stream())
145 .await
146 .map_err(RequestError::from)
147 .map_err(wrap_err)?
148 .map_err(RequestError::from)
149 .map_err(wrap_err)?; let r = send_request(runtime, req, &mut stream, Some(source.clone())).await;
154
155 if should_retire_circ(&r) {
156 retire_circ(&circ_mgr, &source, "Partial response");
157 }
158
159 r
160}
161
162fn should_retire_circ(result: &Result<DirResponse>) -> bool {
165 match result {
166 Err(e) => e.should_retire_circ(),
167 Ok(dr) => dr.error().map(RequestError::should_retire_circ) == Some(true),
168 }
169}
170
171#[deprecated(since = "0.8.1", note = "Use send_request instead.")]
173pub async fn download<R, S, SP>(
174 runtime: &SP,
175 req: &R,
176 stream: &mut S,
177 source: Option<SourceInfo>,
178) -> Result<DirResponse>
179where
180 R: request::Requestable + ?Sized,
181 S: AsyncRead + AsyncWrite + Send + Unpin,
182 SP: SleepProvider,
183{
184 send_request(runtime, req, stream, source).await
185}
186
187pub async fn send_request<R, S, SP>(
206 runtime: &SP,
207 req: &R,
208 stream: &mut S,
209 source: Option<SourceInfo>,
210) -> Result<DirResponse>
211where
212 R: request::Requestable + ?Sized,
213 S: AsyncRead + AsyncWrite + Send + Unpin,
214 SP: SleepProvider,
215{
216 let wrap_err = |error| {
217 Error::RequestFailed(RequestFailedError {
218 source: source.clone(),
219 error,
220 })
221 };
222
223 let partial_ok = req.partial_response_body_ok();
224 let maxlen = req.max_response_len();
225 let anonymized = req.anonymized();
226 let req = req.make_request().map_err(wrap_err)?;
227 let encoded = util::encode_request(&req);
228
229 stream
231 .write_all(encoded.as_bytes())
232 .await
233 .map_err(RequestError::from)
234 .map_err(wrap_err)?;
235 stream
236 .flush()
237 .await
238 .map_err(RequestError::from)
239 .map_err(wrap_err)?;
240
241 let mut buffered = BufReader::new(stream);
242
243 let header = read_headers(&mut buffered).await.map_err(wrap_err)?;
246 if header.status != Some(200) {
247 return Ok(DirResponse::new(
248 header.status.unwrap_or(0),
249 header.status_message,
250 None,
251 vec![],
252 source,
253 ));
254 }
255
256 let mut decoder =
257 get_decoder(buffered, header.encoding.as_deref(), anonymized).map_err(wrap_err)?;
258
259 let mut result = Vec::new();
260 let ok = read_and_decompress(runtime, &mut decoder, maxlen, &mut result).await;
261
262 let ok = match (partial_ok, ok, result.len()) {
263 (true, Err(e), n) if n > 0 => {
264 Err(e)
266 }
267 (_, Err(e), _) => {
268 return Err(wrap_err(e));
269 }
270 (_, Ok(()), _) => Ok(()),
271 };
272
273 Ok(DirResponse::new(200, None, ok.err(), result, source))
274}
275
276async fn read_headers<S>(stream: &mut S) -> RequestResult<HeaderStatus>
278where
279 S: AsyncBufRead + Unpin,
280{
281 let mut buf = Vec::with_capacity(1024);
282
283 loop {
284 let n = read_until_limited(stream, b'\n', 2048, &mut buf).await?;
288
289 let mut headers = [httparse::EMPTY_HEADER; 32];
291 let mut response = httparse::Response::new(&mut headers);
292
293 match response.parse(&buf[..])? {
294 httparse::Status::Partial => {
295 if n == 0 {
298 return Err(RequestError::TruncatedHeaders);
300 }
301
302 if buf.len() >= 16384 {
304 return Err(httparse::Error::TooManyHeaders.into());
305 }
306 }
307 httparse::Status::Complete(n_parsed) => {
308 if response.code != Some(200) {
309 return Ok(HeaderStatus {
310 status: response.code,
311 status_message: response.reason.map(str::to_owned),
312 encoding: None,
313 });
314 }
315 let encoding = if let Some(enc) = response
316 .headers
317 .iter()
318 .find(|h| h.name == "Content-Encoding")
319 {
320 Some(String::from_utf8(enc.value.to_vec())?)
321 } else {
322 None
323 };
324 assert!(n_parsed == buf.len());
331 return Ok(HeaderStatus {
332 status: Some(200),
333 status_message: None,
334 encoding,
335 });
336 }
337 }
338 if n == 0 {
339 return Err(RequestError::TruncatedHeaders);
340 }
341 }
342}
343
344#[derive(Debug, Clone)]
346struct HeaderStatus {
347 status: Option<u16>,
349 status_message: Option<String>,
351 encoding: Option<String>,
353}
354
355async fn read_and_decompress<S, SP>(
364 runtime: &SP,
365 mut stream: S,
366 maxlen: usize,
367 result: &mut Vec<u8>,
368) -> RequestResult<()>
369where
370 S: AsyncRead + Unpin,
371 SP: SleepProvider,
372{
373 let buffer_window_size = 1024;
374 let mut written_total: usize = 0;
375 let read_timeout = Duration::from_secs(10);
378 let timer = runtime.sleep(read_timeout).fuse();
379 futures::pin_mut!(timer);
380
381 loop {
382 result.resize(written_total + buffer_window_size, 0);
384 let buf: &mut [u8] = &mut result[written_total..written_total + buffer_window_size];
385
386 let status = futures::select! {
387 status = stream.read(buf).fuse() => status,
388 _ = timer => {
389 result.resize(written_total, 0); return Err(RequestError::DirTimeout);
391 }
392 };
393 let written_in_this_loop = match status {
394 Ok(n) => n,
395 Err(other) => {
396 result.resize(written_total, 0); return Err(other.into());
398 }
399 };
400
401 written_total += written_in_this_loop;
402
403 if written_in_this_loop == 0 {
406 if written_total < result.len() {
412 result.resize(written_total, 0);
413 }
414 return Ok(());
415 }
416
417 if written_total > maxlen {
423 result.resize(maxlen, 0);
424 return Err(RequestError::ResponseTooLong(written_total));
425 }
426 }
427}
428
429fn retire_circ<R>(circ_mgr: &Arc<CircMgr<R>>, source_info: &SourceInfo, error: &str)
431where
432 R: Runtime,
433{
434 let id = source_info.unique_circ_id();
435 info!(
436 "{}: Retiring circuit because of directory failure: {}",
437 &id, &error
438 );
439 circ_mgr.retire_circ(id);
440}
441
442async fn read_until_limited<S>(
449 stream: &mut S,
450 byte: u8,
451 max: usize,
452 buf: &mut Vec<u8>,
453) -> std::io::Result<usize>
454where
455 S: AsyncBufRead + Unpin,
456{
457 let mut n_added = 0;
458 loop {
459 let data = stream.fill_buf().await?;
460 if data.is_empty() {
461 return Ok(n_added);
463 }
464 debug_assert!(n_added < max);
465 let remaining_space = max - n_added;
466 let (available, found_byte) = match memchr(byte, data) {
467 Some(idx) => (idx + 1, true),
468 None => (data.len(), false),
469 };
470 debug_assert!(available >= 1);
471 let n_to_copy = std::cmp::min(remaining_space, available);
472 buf.extend(&data[..n_to_copy]);
473 stream.consume_unpin(n_to_copy);
474 n_added += n_to_copy;
475 if found_byte || n_added == max {
476 return Ok(n_added);
477 }
478 }
479}
480
481macro_rules! decoder {
483 ($dec:ident, $s:expr) => {{
484 let mut decoder = $dec::new($s);
485 decoder.multiple_members(true);
486 Ok(Box::new(decoder))
487 }};
488}
489
490fn get_decoder<'a, S: AsyncBufRead + Unpin + Send + 'a>(
493 stream: S,
494 encoding: Option<&str>,
495 anonymized: AnonymizedRequest,
496) -> RequestResult<Box<dyn AsyncRead + Unpin + Send + 'a>> {
497 use AnonymizedRequest::Direct;
498 match (encoding, anonymized) {
499 (None | Some("identity"), _) => Ok(Box::new(stream)),
500 (Some("deflate"), _) => decoder!(ZlibDecoder, stream),
501 #[cfg(feature = "xz")]
505 (Some("x-tor-lzma"), Direct) => decoder!(XzDecoder, stream),
506 #[cfg(feature = "zstd")]
507 (Some("x-zstd"), Direct) => decoder!(ZstdDecoder, stream),
508 (Some(other), _) => Err(RequestError::ContentEncoding(other.into())),
509 }
510}
511
512#[cfg(test)]
513mod test {
514 #![allow(clippy::bool_assert_comparison)]
516 #![allow(clippy::clone_on_copy)]
517 #![allow(clippy::dbg_macro)]
518 #![allow(clippy::mixed_attributes_style)]
519 #![allow(clippy::print_stderr)]
520 #![allow(clippy::print_stdout)]
521 #![allow(clippy::single_char_pattern)]
522 #![allow(clippy::unwrap_used)]
523 #![allow(clippy::unchecked_duration_subtraction)]
524 #![allow(clippy::useless_vec)]
525 #![allow(clippy::needless_pass_by_value)]
526 use super::*;
528 use tor_rtmock::io::stream_pair;
529
530 #[allow(deprecated)] use tor_rtmock::time::MockSleepProvider;
532
533 use futures_await_test::async_test;
534
535 #[async_test]
536 async fn test_read_until_limited() -> RequestResult<()> {
537 let mut out = Vec::new();
538 let bytes = b"This line eventually ends\nthen comes another\n";
539
540 let mut s = &bytes[..];
542 let res = read_until_limited(&mut s, b'\n', 100, &mut out).await;
543 assert_eq!(res?, 26);
544 assert_eq!(&out[..], b"This line eventually ends\n");
545
546 let mut s = &bytes[..];
548 out.clear();
549 let res = read_until_limited(&mut s, b'\n', 10, &mut out).await;
550 assert_eq!(res?, 10);
551 assert_eq!(&out[..], b"This line ");
552
553 let mut s = &bytes[..];
555 out.clear();
556 let res = read_until_limited(&mut s, b'Z', 100, &mut out).await;
557 assert_eq!(res?, 45);
558 assert_eq!(&out[..], &bytes[..]);
559
560 Ok(())
561 }
562
563 async fn decomp_basic(
565 encoding: Option<&str>,
566 data: &[u8],
567 maxlen: usize,
568 ) -> (RequestResult<()>, Vec<u8>) {
569 #[allow(deprecated)] let mock_time = MockSleepProvider::new(std::time::SystemTime::now());
573
574 let mut output = Vec::new();
575 let mut stream = match get_decoder(data, encoding, AnonymizedRequest::Direct) {
576 Ok(s) => s,
577 Err(e) => return (Err(e), output),
578 };
579
580 let r = read_and_decompress(&mock_time, &mut stream, maxlen, &mut output).await;
581
582 (r, output)
583 }
584
585 #[async_test]
586 async fn decompress_identity() -> RequestResult<()> {
587 let mut text = Vec::new();
588 for _ in 0..1000 {
589 text.extend(b"This is a string with a nontrivial length that we'll use to make sure that the loop is executed more than once.");
590 }
591
592 let limit = 10 << 20;
593 let (s, r) = decomp_basic(None, &text[..], limit).await;
594 s?;
595 assert_eq!(r, text);
596
597 let (s, r) = decomp_basic(Some("identity"), &text[..], limit).await;
598 s?;
599 assert_eq!(r, text);
600
601 let limit = 100;
603 let (s, r) = decomp_basic(Some("identity"), &text[..], limit).await;
604 assert!(s.is_err());
605 assert_eq!(r, &text[..100]);
606
607 Ok(())
608 }
609
610 #[async_test]
611 async fn decomp_zlib() -> RequestResult<()> {
612 let compressed =
613 hex::decode("789cf3cf4b5548cb2cce500829cf8730825253200ca79c52881c00e5970c88").unwrap();
614
615 let limit = 10 << 20;
616 let (s, r) = decomp_basic(Some("deflate"), &compressed, limit).await;
617 s?;
618 assert_eq!(r, b"One fish Two fish Red fish Blue fish");
619
620 Ok(())
621 }
622
623 #[cfg(feature = "zstd")]
624 #[async_test]
625 async fn decomp_zstd() -> RequestResult<()> {
626 let compressed = hex::decode("28b52ffd24250d0100c84f6e6520666973682054776f526564426c756520666973680a0200600c0e2509478352cb").unwrap();
627 let limit = 10 << 20;
628 let (s, r) = decomp_basic(Some("x-zstd"), &compressed, limit).await;
629 s?;
630 assert_eq!(r, b"One fish Two fish Red fish Blue fish\n");
631
632 Ok(())
633 }
634
635 #[cfg(feature = "xz")]
636 #[async_test]
637 async fn decomp_xz2() -> RequestResult<()> {
638 let compressed = hex::decode("fd377a585a000004e6d6b446020021011c00000010cf58cce00024001d5d00279b88a202ca8612cfb3c19c87c34248a570451e4851d3323d34ab8000000000000901af64854c91f600013925d6ec06651fb6f37d010000000004595a").unwrap();
640 let limit = 10 << 20;
641 let (s, r) = decomp_basic(Some("x-tor-lzma"), &compressed, limit).await;
642 s?;
643 assert_eq!(r, b"One fish Two fish Red fish Blue fish\n");
644
645 Ok(())
646 }
647
648 #[async_test]
649 async fn decomp_unknown() {
650 let compressed = hex::decode("28b52ffd24250d0100c84f6e6520666973682054776f526564426c756520666973680a0200600c0e2509478352cb").unwrap();
651 let limit = 10 << 20;
652 let (s, _r) = decomp_basic(Some("x-proprietary-rle"), &compressed, limit).await;
653
654 assert!(matches!(s, Err(RequestError::ContentEncoding(_))));
655 }
656
657 #[async_test]
658 async fn decomp_bad_data() {
659 let compressed = b"This is not good zlib data";
660 let limit = 10 << 20;
661 let (s, _r) = decomp_basic(Some("deflate"), compressed, limit).await;
662
663 assert!(matches!(s, Err(RequestError::IoError(_))));
665 }
666
667 #[async_test]
668 async fn headers_ok() -> RequestResult<()> {
669 let text = b"HTTP/1.0 200 OK\r\nDate: ignored\r\nContent-Encoding: Waffles\r\n\r\n";
670
671 let mut s = &text[..];
672 let h = read_headers(&mut s).await?;
673
674 assert_eq!(h.status, Some(200));
675 assert_eq!(h.encoding.as_deref(), Some("Waffles"));
676
677 let mut s = &text[..15];
679 let h = read_headers(&mut s).await;
680 assert!(matches!(h, Err(RequestError::TruncatedHeaders)));
681
682 let text = b"HTTP/1.0 404 Not found\r\n\r\n";
684 let mut s = &text[..];
685 let h = read_headers(&mut s).await?;
686
687 assert_eq!(h.status, Some(404));
688 assert!(h.encoding.is_none());
689
690 Ok(())
691 }
692
693 #[async_test]
694 async fn headers_bogus() -> Result<()> {
695 let text = b"HTTP/999.0 WHAT EVEN\r\n\r\n";
696 let mut s = &text[..];
697 let h = read_headers(&mut s).await;
698
699 assert!(h.is_err());
700 assert!(matches!(h, Err(RequestError::HttparseError(_))));
701 Ok(())
702 }
703
704 fn run_download_test<Req: request::Requestable>(
710 req: Req,
711 response: &[u8],
712 ) -> (Result<DirResponse>, RequestResult<Vec<u8>>) {
713 let (mut s1, s2) = stream_pair();
714 let (mut s2_r, mut s2_w) = s2.split();
715
716 tor_rtcompat::test_with_one_runtime!(|rt| async move {
717 let rt2 = rt.clone();
718 let (v1, v2, v3): (
719 Result<DirResponse>,
720 RequestResult<Vec<u8>>,
721 RequestResult<()>,
722 ) = futures::join!(
723 async {
724 let r = send_request(&rt, &req, &mut s1, None).await;
726 s1.close().await.map_err(|error| {
727 Error::RequestFailed(RequestFailedError {
728 source: None,
729 error: error.into(),
730 })
731 })?;
732 r
733 },
734 async {
735 let mut v = Vec::new();
737 s2_r.read_to_end(&mut v).await?;
738 Ok(v)
739 },
740 async {
741 s2_w.write_all(response).await?;
743 rt2.sleep(Duration::from_millis(50)).await;
756 s2_w.close().await?;
757 Ok(())
758 }
759 );
760
761 assert!(v3.is_ok());
762
763 (v1, v2)
764 })
765 }
766
767 #[test]
768 fn test_send_request() -> RequestResult<()> {
769 let req: request::MicrodescRequest = vec![[9; 32]].into_iter().collect();
770
771 let (response, request) = run_download_test(
772 req,
773 b"HTTP/1.0 200 OK\r\n\r\nThis is where the descs would go.",
774 );
775
776 let request = request?;
777 assert!(request[..].starts_with(
778 b"GET /tor/micro/d/CQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQk.z HTTP/1.0\r\n"
779 ));
780
781 let response = response.unwrap();
782 assert_eq!(response.status_code(), 200);
783 assert!(!response.is_partial());
784 assert!(response.error().is_none());
785 assert!(response.source().is_none());
786 let out_ref = response.output_unchecked();
787 assert_eq!(out_ref, b"This is where the descs would go.");
788 let out = response.into_output_unchecked();
789 assert_eq!(&out, b"This is where the descs would go.");
790
791 Ok(())
792 }
793
794 #[test]
795 fn test_download_truncated() {
796 let req: request::MicrodescRequest = vec![[9; 32]].into_iter().collect();
798 let mut response_text: Vec<u8> =
799 (*b"HTTP/1.0 200 OK\r\nContent-Encoding: deflate\r\n\r\n").into();
800 response_text.extend(
802 hex::decode("789cf3cf4b5548cb2cce500829cf8730825253200ca79c52881c00e5970c88").unwrap(),
803 );
804 response_text.extend(
805 hex::decode("789cf3cf4b5548cb2cce500829cf8730825253200ca79c52881c00e5").unwrap(),
806 );
807 let (response, request) = run_download_test(req, &response_text);
808 assert!(request.is_ok());
809 assert!(response.is_err()); let req: request::MicrodescRequest = vec![[9; 32]; 2].into_iter().collect();
813
814 let (response, request) = run_download_test(req, &response_text);
815 assert!(request.is_ok());
816
817 let response = response.unwrap();
818 assert_eq!(response.status_code(), 200);
819 assert!(response.error().is_some());
820 assert!(response.is_partial());
821 assert!(response.output_unchecked().len() < 37 * 2);
822 assert!(response.output_unchecked().starts_with(b"One fish"));
823 }
824
825 #[test]
826 fn test_404() {
827 let req: request::MicrodescRequest = vec![[9; 32]].into_iter().collect();
828 let response_text = b"HTTP/1.0 418 I'm a teapot\r\n\r\n";
829 let (response, _request) = run_download_test(req, response_text);
830
831 assert_eq!(response.unwrap().status_code(), 418);
832 }
833
834 #[test]
835 fn test_headers_truncated() {
836 let req: request::MicrodescRequest = vec![[9; 32]].into_iter().collect();
837 let response_text = b"HTTP/1.0 404 truncation happens here\r\n";
838 let (response, _request) = run_download_test(req, response_text);
839
840 assert!(matches!(
841 response,
842 Err(Error::RequestFailed(RequestFailedError {
843 error: RequestError::TruncatedHeaders,
844 ..
845 }))
846 ));
847
848 let req: request::MicrodescRequest = vec![[9; 32]].into_iter().collect();
850 let response_text = b"";
851 let (response, _request) = run_download_test(req, response_text);
852
853 assert!(matches!(
854 response,
855 Err(Error::RequestFailed(RequestFailedError {
856 error: RequestError::TruncatedHeaders,
857 ..
858 }))
859 ));
860 }
861
862 #[test]
863 fn test_headers_too_long() {
864 let req: request::MicrodescRequest = vec![[9; 32]].into_iter().collect();
865 let mut response_text: Vec<u8> = (*b"HTTP/1.0 418 I'm a teapot\r\nX-Too-Many-As: ").into();
866 response_text.resize(16384, b'A');
867 let (response, _request) = run_download_test(req, &response_text);
868
869 assert!(response.as_ref().unwrap_err().should_retire_circ());
870 assert!(matches!(
871 response,
872 Err(Error::RequestFailed(RequestFailedError {
873 error: RequestError::HttparseError(_),
874 ..
875 }))
876 ));
877 }
878
879 }