From 08643d487ce8fd6286ae17447a107c4b2b5a8d43 Mon Sep 17 00:00:00 2001 From: mikkelam Date: Thu, 19 Jun 2025 19:37:21 +0200 Subject: [PATCH] better handling of internet issues on initial requests also improve json output --- src/cli/root.zig | 56 ++++++++++++++++++++++++++++++------------------ src/lib/fast.zig | 31 +++++++++++++++++++++++++-- 2 files changed, 64 insertions(+), 23 deletions(-) diff --git a/src/cli/root.zig b/src/cli/root.zig index 1df7a3c..96c5819 100644 --- a/src/cli/root.zig +++ b/src/cli/root.zig @@ -13,16 +13,6 @@ const progress = @import("../lib/progress.zig"); const HttpLatencyTester = @import("../lib/http_latency_tester.zig").HttpLatencyTester; const log = std.log.scoped(.cli); -/// Update spinner text with current speed measurement -fn updateSpinnerText(spinner: anytype, measurement: SpeedMeasurement) void { - spinner.updateText("⬇️ {d:.1} {s}", .{ measurement.value, measurement.unit.toString() }) catch {}; -} - -/// Update spinner text with current upload speed measurement -fn updateUploadSpinnerText(spinner: anytype, measurement: SpeedMeasurement) void { - spinner.updateText("⬆️ {d:.1} {s}", .{ measurement.value, measurement.unit.toString() }) catch {}; -} - const https_flag = zli.Flag{ .name = "https", .description = "Use https when connecting to fast.com", @@ -86,7 +76,11 @@ fn run(ctx: zli.CommandContext) !void { if (!json_output) { try ctx.spinner.fail("Failed to get URLs: {}", .{err}); } else { - std.debug.print("{{\"error\": \"{}\"}}\n", .{err}); + const error_msg = switch (err) { + error.ConnectionTimeout => "Failed to contact fast.com servers", + else => "Failed to get URLs", + }; + try outputJson(null, null, null, error_msg); } return; }; @@ -133,7 +127,7 @@ fn run(ctx: zli.CommandContext) !void { // JSON mode: clean output only break :blk speed_tester.measure_download_speed_stability(urls, criteria) catch |err| { log.err("Download test failed: {}", .{err}); - std.debug.print("{{\"error\": \"{}\"}}\n", .{err}); + try outputJson(null, null, null, "Download test failed"); return; }; } else blk: { @@ -155,7 +149,7 @@ fn run(ctx: zli.CommandContext) !void { // JSON mode: clean output only break :blk speed_tester.measure_upload_speed_stability(urls, criteria) catch |err| { log.err("Upload test failed: {}", .{err}); - std.debug.print("{{\"error\": \"{}\"}}\n", .{err}); + try outputJson(download_result.speed.value, latency_ms, null, "Upload test failed"); return; }; } else blk: { @@ -184,13 +178,33 @@ fn run(ctx: zli.CommandContext) !void { } } } else { - std.debug.print("{{\"download_mbps\": {d:.1}", .{download_result.speed.value}); - if (latency_ms) |ping| { - std.debug.print(", \"ping_ms\": {d:.1}", .{ping}); - } - if (upload_result) |up| { - std.debug.print(", \"upload_mbps\": {d:.1}", .{up.speed.value}); - } - std.debug.print("}}\n", .{}); + const upload_speed = if (upload_result) |up| up.speed.value else null; + try outputJson(download_result.speed.value, latency_ms, upload_speed, null); } } + +/// Update spinner text with current speed measurement +fn updateSpinnerText(spinner: anytype, measurement: SpeedMeasurement) void { + spinner.updateText("⬇️ {d:.1} {s}", .{ measurement.value, measurement.unit.toString() }) catch {}; +} + +/// Update spinner text with current upload speed measurement +fn updateUploadSpinnerText(spinner: anytype, measurement: SpeedMeasurement) void { + spinner.updateText("⬆️ {d:.1} {s}", .{ measurement.value, measurement.unit.toString() }) catch {}; +} + +fn outputJson(download_mbps: ?f64, ping_ms: ?f64, upload_mbps: ?f64, error_message: ?[]const u8) !void { + const stdout = std.io.getStdOut().writer(); + + var download_buf: [32]u8 = undefined; + var ping_buf: [32]u8 = undefined; + var upload_buf: [32]u8 = undefined; + var error_buf: [256]u8 = undefined; + + const download_str = if (download_mbps) |d| try std.fmt.bufPrint(&download_buf, "{d:.1}", .{d}) else "null"; + const ping_str = if (ping_ms) |p| try std.fmt.bufPrint(&ping_buf, "{d:.1}", .{p}) else "null"; + const upload_str = if (upload_mbps) |u| try std.fmt.bufPrint(&upload_buf, "{d:.1}", .{u}) else "null"; + const error_str = if (error_message) |e| try std.fmt.bufPrint(&error_buf, "\"{s}\"", .{e}) else "null"; + + try stdout.print("{{\"download_mbps\": {s}, \"ping_ms\": {s}, \"upload_mbps\": {s}, \"error\": {s}}}\n", .{ download_str, ping_str, upload_str, error_str }); +} diff --git a/src/lib/fast.zig b/src/lib/fast.zig index c8b9fbb..d2bf12e 100644 --- a/src/lib/fast.zig +++ b/src/lib/fast.zig @@ -6,11 +6,13 @@ const testing = std.testing; const log = std.log.scoped(.fast_api); const mvzr = @import("mvzr"); + const FastError = error{ HttpRequestFailed, ScriptNotFound, TokenNotFound, JsonParseError, + ConnectionTimeout, }; const Location = struct { @@ -159,11 +161,36 @@ pub const Fast = struct { _ = allocator; var response_body = std.ArrayList(u8).init(self.arena.allocator()); - const response: http.Client.FetchResult = try self.client.fetch(.{ + const response: http.Client.FetchResult = self.client.fetch(.{ .method = .GET, .location = .{ .url = url }, .response_storage = .{ .dynamic = &response_body }, - }); + }) catch |err| switch (err) { + error.NetworkUnreachable, error.ConnectionRefused => { + log.err("Failed to reach fast.com servers (network/connection error) for URL: {s}", .{url}); + return error.ConnectionTimeout; + }, + error.UnknownHostName, error.NameServerFailure, error.TemporaryNameServerFailure, error.HostLacksNetworkAddresses => { + log.err("Failed to resolve fast.com hostname (DNS/internet connection issue) for URL: {s}", .{url}); + return error.ConnectionTimeout; + }, + error.ConnectionTimedOut, error.ConnectionResetByPeer => { + log.err("Connection to fast.com servers timed out or was reset for URL: {s}", .{url}); + return error.ConnectionTimeout; + }, + error.TlsInitializationFailed => { + log.err("Failed to establish secure connection to fast.com servers for URL: {s}", .{url}); + return error.ConnectionTimeout; + }, + error.UnexpectedConnectFailure => { + log.err("Unexpected connection failure to fast.com servers for URL: {s}", .{url}); + return error.ConnectionTimeout; + }, + else => { + log.err("Network error: {} for URL: {s}", .{ err, url }); + return error.ConnectionTimeout; + }, + }; log.debug("HTTP response status: {} for URL: {s}", .{ response.status, url });