Skip to content

Commit 9223ee9

Browse files
merge master into feature/improved-error-handling branch
1 parent dfc3cf8 commit 9223ee9

File tree

3 files changed

+65
-51
lines changed

3 files changed

+65
-51
lines changed

Cargo.lock

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "cfspeedtest"
3-
version = "2.0.1"
3+
version = "2.0.2"
44
edition = "2021"
55
license = "MIT"
66
description = "Unofficial CLI for speed.cloudflare.com"
@@ -14,7 +14,7 @@ exclude = [".github/"]
1414
log = "0.4"
1515
env_logger = "0.11"
1616
regex = "1.12"
17-
reqwest = { version = "0.12.24", default-features = false, features = ["blocking", "rustls-tls"] }
17+
reqwest = { version = "0.12.28", default-features = false, features = ["blocking", "rustls-tls"] }
1818
clap = { version = "4.5", features = ["derive"] }
1919
serde = { version = "1.0", features = ["derive"] }
2020
csv = "1.3.0"

src/speedtest.rs

Lines changed: 54 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ use reqwest::{blocking::Client, StatusCode};
1111
use serde::Serialize;
1212
use std::{
1313
fmt::Display,
14+
sync::atomic::{AtomicBool, Ordering},
1415
time::{Duration, Instant},
1516
};
1617

1718
const BASE_URL: &str = "https://speed.cloudflare.com";
1819
const DOWNLOAD_URL: &str = "__down?bytes=";
1920
const UPLOAD_URL: &str = "__up";
21+
static WARNED_NEGATIVE_LATENCY: AtomicBool = AtomicBool::new(false);
2022

2123
#[derive(Clone, Copy, Debug, Hash, Serialize, Eq, PartialEq)]
2224
pub enum TestType {
@@ -180,63 +182,72 @@ pub fn test_latency(client: &Client) -> f64 {
180182
let req_builder = client.get(url);
181183

182184
let start = Instant::now();
183-
let response = match req_builder.send() {
185+
let mut response = match req_builder.send() {
184186
Ok(res) => res,
185187
Err(e) => {
186188
log::error!("Failed to get response for latency test: {}", e);
187189
return 0.0;
188190
}
189191
};
190192
let _status_code = response.status();
191-
let duration = start.elapsed().as_secs_f64() * 1_000.0;
193+
// Drain body to complete the request; ignore errors.
194+
let _ = std::io::copy(&mut response, &mut std::io::sink());
195+
let total_ms = start.elapsed().as_secs_f64() * 1_000.0;
192196

193197
// Try to extract cfRequestDuration from Server-Timing header
194-
let cf_req_duration = match response.headers().get("Server-Timing") {
198+
let re = match Regex::new(r"cfRequestDuration;dur=([\d.]+)") {
199+
Ok(re) => re,
200+
Err(e) => {
201+
log::error!("Failed to compile regex: {}", e);
202+
return total_ms;
203+
}
204+
};
205+
206+
let server_timing = match response.headers().get("Server-Timing") {
195207
Some(header_value) => match header_value.to_str() {
196-
Ok(header_str) => {
197-
let re = match Regex::new(r"cfRequestDuration;dur=([\d.]+)") {
198-
Ok(re) => re,
199-
Err(e) => {
200-
log::error!("Failed to compile regex: {}", e);
201-
return duration; // Return full duration if we can't parse server timing
202-
}
203-
};
204-
205-
match re.captures(header_str) {
206-
Some(captures) => match captures.get(1) {
207-
Some(dur_match) => match dur_match.as_str().parse::<f64>() {
208-
Ok(parsed) => parsed,
209-
Err(e) => {
210-
log::error!("Failed to parse cfRequestDuration: {}", e);
211-
return duration;
212-
}
213-
},
214-
None => {
215-
log::debug!("No cfRequestDuration found in Server-Timing header");
216-
return duration;
217-
}
218-
},
219-
None => {
220-
log::debug!("Server-Timing header doesn't match expected format");
221-
return duration;
222-
}
223-
}
224-
}
208+
Ok(s) => s,
225209
Err(e) => {
226210
log::error!("Failed to convert Server-Timing header to string: {}", e);
227-
return duration;
211+
return total_ms;
228212
}
229213
},
230214
None => {
231215
log::debug!("No Server-Timing header in response");
232-
return duration;
216+
return total_ms;
217+
}
218+
};
219+
220+
let cf_req_duration: f64 = match re.captures(server_timing) {
221+
Some(captures) => match captures.get(1) {
222+
Some(dur_match) => match dur_match.as_str().parse::<f64>() {
223+
Ok(parsed) => parsed,
224+
Err(e) => {
225+
log::error!("Failed to parse cfRequestDuration: {}", e);
226+
return total_ms;
227+
}
228+
},
229+
None => {
230+
log::debug!("No cfRequestDuration found in Server-Timing header");
231+
return total_ms;
232+
}
233+
},
234+
None => {
235+
log::debug!("Server-Timing header doesn't match expected format");
236+
return total_ms;
233237
}
234238
};
235239

236-
let mut req_latency = duration - cf_req_duration;
240+
let mut req_latency = total_ms - cf_req_duration;
241+
log::debug!(
242+
"latency debug: total_ms={total_ms:.3} cf_req_duration_ms={cf_req_duration:.3} req_latency_total={req_latency:.3} server_timing={server_timing}"
243+
);
237244
if req_latency < 0.0 {
238-
log::warn!("Negative latency calculated: {req_latency}ms, using 0.0ms instead");
239-
req_latency = 0.0;
245+
if !WARNED_NEGATIVE_LATENCY.swap(true, Ordering::Relaxed) {
246+
log::warn!(
247+
"negative latency after server timing subtraction; clamping to 0.0 (total_ms={total_ms:.3} cf_req_duration_ms={cf_req_duration:.3})"
248+
);
249+
}
250+
req_latency = 0.0
240251
}
241252
req_latency
242253
}
@@ -296,7 +307,7 @@ pub fn test_upload(client: &Client, payload_size_bytes: usize, output_format: Ou
296307
let req_builder = client.post(url).body(payload);
297308

298309
let start = Instant::now();
299-
let response = match req_builder.send() {
310+
let mut response = match req_builder.send() {
300311
Ok(res) => res,
301312
Err(e) => {
302313
log::error!("Failed to send upload request: {}", e);
@@ -307,6 +318,8 @@ pub fn test_upload(client: &Client, payload_size_bytes: usize, output_format: Ou
307318
let duration = start.elapsed();
308319
let mbits = (payload_size_bytes as f64 * 8.0 / 1_000_000.0) / duration.as_secs_f64();
309320

321+
// Drain response after timing so we don't skew upload measurement.
322+
let _ = std::io::copy(&mut response, &mut std::io::sink());
310323
if output_format == OutputFormat::StdOut {
311324
print_current_speed(mbits, duration, status_code, payload_size_bytes);
312325
}
@@ -322,15 +335,16 @@ pub fn test_download(
322335
let req_builder = client.get(url);
323336

324337
let start = Instant::now();
325-
let response = match req_builder.send() {
338+
let mut response = match req_builder.send() {
326339
Ok(res) => res,
327340
Err(e) => {
328341
log::error!("Failed to send download request: {}", e);
329342
return 0.0;
330343
}
331344
};
332345
let status_code = response.status();
333-
let _res_bytes = response.bytes();
346+
// Stream the body to avoid buffering the full payload in memory.
347+
let _ = std::io::copy(&mut response, &mut std::io::sink());
334348
let duration = start.elapsed();
335349
let mbits = (payload_size_bytes as f64 * 8.0 / 1_000_000.0) / duration.as_secs_f64();
336350

0 commit comments

Comments
 (0)