Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,16 @@ deliberately different:
HTTP status, and no share link is printed. Go does not check the status: it
takes a share ID from any body with exactly one space in it, so a 500 reply
can still yield a share link.
- **A run whose servers are all down fails.** When no server picked with
`--server` answers its probe (the connection is refused, the network is
unreachable, the request times out or the backend answers wrongly), the run
ends with exit status 1 and `Terminated due to error:` naming each server
with its cause, and prints no report. The Go client prints an empty report
instead (`null` for `--json`, a `result` event carrying `null` for
`--json-stream`, an empty line for `--csv`) and exits 0; with `--json`,
`--json-stream`, `--csv` and `--simple` it prints nothing on stderr either,
so a run on a link that cannot reach the server looks like a success. When
some of the servers answer, both clients report those and exit 0.
- **HTTP/1.1 by default, HTTP/2 behind `--http2`.** HTTP/2 carries every stream
over one TCP connection, so `--concurrent` would stop meaning concurrent
connections — and multiple connections is the standard way a speed test
Expand Down
34 changes: 29 additions & 5 deletions src/defs/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ impl Server {
Ok(u) => url_join_path(&u, &self.ping_url),
Err(e) => {
write_debug!(out, "Failed when creating HTTP request: {e}\n");
return ServerStatus::default();
return ServerStatus::down(format!("{e:#}"));
}
};

Expand Down Expand Up @@ -164,16 +164,27 @@ impl Server {
"Failed when parsing get IP result: {}\n",
GoQuote(&body)
);
return ServerStatus::default();
}
// Status first: a web server's 404 or 500 comes with an error
// page, and the status names that fault better than the body.
let down_reason = if status != StatusCode::OK {
Some(format!("the ping request returned HTTP {status}"))
} else if !body.is_empty() {
Some("the ping request returned a non-empty body".to_string())
} else {
None
};
ServerStatus {
up: status == StatusCode::OK,
tls: facts.tls,
up: down_reason.is_none(),
down_reason,
// As before, a probe answered with a body reports nothing
// about its connection.
tls: facts.tls.filter(|_| body.is_empty()),
}
}
Err(e) => {
write_debug!(out, "Error checking for server status: {e:#}\n");
ServerStatus::default()
ServerStatus::down(format!("{e:#}"))
}
}
}
Expand Down Expand Up @@ -622,9 +633,22 @@ const PROBE_BODY_PEEK: usize = 8 * 1024;
#[derive(Debug, Default)]
pub struct ServerStatus {
pub up: bool,
/// Why the backend is down, when it is: the refused connection, the
/// unreachable network, the wrong status or a body where none belongs.
pub down_reason: Option<String>,
pub tls: Option<crate::http::TlsFacts>,
}

impl ServerStatus {
fn down(reason: String) -> Self {
ServerStatus {
up: false,
down_reason: Some(reason),
tls: None,
}
}
}

/// A running `--json-stream` progress ticker.
struct ProgressTicker {
stop: Arc<tokio::sync::Notify>,
Expand Down
41 changes: 38 additions & 3 deletions src/helper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ pub async fn do_speed_test(
let delimiter = cli.csv_delimiter_byte()?;
let mut reps_json: Vec<JSONReport> = Vec::new();
let mut reps_csv: Vec<CSVReport> = Vec::new();
let mut measured = false;
// Each server that did not answer its probe, and why.
let mut down: Vec<(String, String)> = Vec::new();

for current_server in servers {
let tlog = TelemetryLog::new();
Expand Down Expand Up @@ -106,6 +109,16 @@ pub async fn do_speed_test(
if servers.len() > 1 && !out.quiet {
output::write_ui_blank();
}
down.push((
format!(
"{} ({})",
output::sanitize(&current_server.name),
output::sanitize(&hostname)
),
status
.down_reason
.unwrap_or_else(|| "its probe failed".to_string()),
));
continue;
}

Expand Down Expand Up @@ -339,12 +352,20 @@ pub async fn do_speed_test(
});
}

measured = true;

// Add a blank line after each test when testing multiple servers.
if servers.len() > 1 && !out.quiet {
output::write_ui_blank();
}
}

// No server answered: fail the run. Go prints an empty report and exits 0,
// which a script takes for success: quiet mode hides the line saying why.
if !measured && !down.is_empty() {
return Err(none_responding(&down));
}

if cli.csv {
match report::csv_rows(&reps_csv, delimiter) {
Ok(s) => write_out!("{s}"),
Expand Down Expand Up @@ -377,11 +398,25 @@ fn report_failure(what: &str, e: anyhow::Error) -> anyhow::Error {
e
}

/// The error for a run whose servers were all down: each one and its cause.
fn none_responding(down: &[(String, String)]) -> anyhow::Error {
match down {
[(server, why)] => anyhow::anyhow!("Selected server {server} is not responding: {why}"),
_ => anyhow::anyhow!(
"None of the {} selected servers is responding: {}",
down.len(),
down.iter()
.map(|(server, why)| format!("{server}: {why}"))
.collect::<Vec<_>>()
.join("; ")
),
}
}

/// The reports as JSON sees them: `null` when nothing was measured.
///
/// Go declares a nil slice, which marshals to `null`, and both clients exit 0
/// even when every server failed -- so the document is the only signal that
/// nothing happened, and a consumer testing for null must keep seeing it.
/// Go declares a nil slice, which marshals to `null`. Down servers fail the run
/// earlier, leaving a run with no server to test (`--server -1`, empty list).
fn json_reports(reports: &[JSONReport]) -> Option<&[JSONReport]> {
(!reports.is_empty()).then_some(reports)
}
Expand Down
Loading
Loading