Skip to content

Commit b68d843

Browse files
committed
fix(cli): stabilize websocket check diagnostics
2 parents 4d5922a + 668cf17 commit b68d843

3 files changed

Lines changed: 201 additions & 26 deletions

File tree

src/commands/WsCommand.cpp

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -224,9 +224,11 @@ namespace vix::commands
224224
<< " -h, --help Show this help message\n\n"
225225
<< "Examples:\n"
226226
<< " vix ws check ws://127.0.0.1:9090/ws\n"
227-
<< " vix ws check wss://pulsegrid.softadastra.com/ws\n"
228-
<< " vix ws check --timeout 5000\n"
229-
<< " vix ws check --no-ping\n\n"
227+
<< " vix ws check ws://127.0.0.1:9090/ws --timeout 5000\n"
228+
<< " vix ws check ws://127.0.0.1:9090/ws --no-ping\n\n"
229+
<< "Notes:\n"
230+
<< " wss:// checks are not supported yet by the native checker.\n"
231+
<< " Use ws:// for local checks until TLS WebSocket support is added.\n\n"
230232
<< "Config:\n"
231233
<< " production.websocket.host\n"
232234
<< " production.websocket.port\n"

src/commands/ws/WsChecker.cpp

Lines changed: 176 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
#include <optional>
2727
#include <string>
2828
#include <thread>
29+
#include <cctype>
2930

3031
namespace vix::commands::ws::checker
3132
{
@@ -50,6 +51,19 @@ namespace vix::commands::ws::checker
5051
std::string error{};
5152
};
5253

54+
enum class WsFailureKind
55+
{
56+
Unknown,
57+
Dns,
58+
ConnectionRefused,
59+
Timeout,
60+
TlsUnsupported,
61+
BadHandshake,
62+
MissingUpgrade,
63+
BadPath,
64+
ProxyHttpResponse
65+
};
66+
5367
bool starts_with(
5468
const std::string &value,
5569
const std::string &prefix)
@@ -71,6 +85,22 @@ namespace vix::commands::ws::checker
7185
return true;
7286
}
7387

88+
bool is_valid_port_number(const std::string &value)
89+
{
90+
if (!is_digit_string(value))
91+
return false;
92+
93+
try
94+
{
95+
const unsigned long parsed = std::stoul(value);
96+
return parsed >= 1 && parsed <= 65535;
97+
}
98+
catch (...)
99+
{
100+
return false;
101+
}
102+
}
103+
74104
std::string selected_url(
75105
const WsConfig &cfg,
76106
const WsOptions &options)
@@ -84,6 +114,130 @@ namespace vix::commands::ws::checker
84114
return cfg.localUrl;
85115
}
86116

117+
std::string lower_copy(std::string value)
118+
{
119+
for (char &ch : value)
120+
ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
121+
122+
return value;
123+
}
124+
125+
bool contains_text(
126+
const std::string &value,
127+
const std::string &needle)
128+
{
129+
return lower_copy(value).find(lower_copy(needle)) != std::string::npos;
130+
}
131+
132+
WsFailureKind classify_failure(const std::string &message)
133+
{
134+
if (message.empty())
135+
return WsFailureKind::Unknown;
136+
137+
if (contains_text(message, "resolve") ||
138+
contains_text(message, "dns") ||
139+
contains_text(message, "host not found") ||
140+
contains_text(message, "name or service not known"))
141+
{
142+
return WsFailureKind::Dns;
143+
}
144+
145+
if (contains_text(message, "connection refused") ||
146+
contains_text(message, "refused"))
147+
{
148+
return WsFailureKind::ConnectionRefused;
149+
}
150+
151+
if (contains_text(message, "timeout") ||
152+
contains_text(message, "timed out"))
153+
{
154+
return WsFailureKind::Timeout;
155+
}
156+
157+
if (contains_text(message, "upgrade") ||
158+
contains_text(message, "sec-websocket-accept"))
159+
{
160+
return WsFailureKind::MissingUpgrade;
161+
}
162+
163+
if (contains_text(message, "404") ||
164+
contains_text(message, "not found"))
165+
{
166+
return WsFailureKind::BadPath;
167+
}
168+
169+
if (contains_text(message, "301") ||
170+
contains_text(message, "302") ||
171+
contains_text(message, "400") ||
172+
contains_text(message, "403") ||
173+
contains_text(message, "502") ||
174+
contains_text(message, "503") ||
175+
contains_text(message, "504") ||
176+
contains_text(message, "http"))
177+
{
178+
return WsFailureKind::ProxyHttpResponse;
179+
}
180+
181+
if (contains_text(message, "handshake"))
182+
return WsFailureKind::BadHandshake;
183+
184+
return WsFailureKind::Unknown;
185+
}
186+
187+
std::string failure_title(WsFailureKind kind)
188+
{
189+
switch (kind)
190+
{
191+
case WsFailureKind::Dns:
192+
return "DNS resolution failed.";
193+
case WsFailureKind::ConnectionRefused:
194+
return "WebSocket TCP connection was refused.";
195+
case WsFailureKind::Timeout:
196+
return "WebSocket connection timed out.";
197+
case WsFailureKind::MissingUpgrade:
198+
return "WebSocket upgrade headers are missing or invalid.";
199+
case WsFailureKind::BadPath:
200+
return "WebSocket path does not exist on the server.";
201+
case WsFailureKind::ProxyHttpResponse:
202+
return "Server returned an HTTP response instead of a WebSocket upgrade.";
203+
case WsFailureKind::BadHandshake:
204+
return "WebSocket handshake failed.";
205+
case WsFailureKind::TlsUnsupported:
206+
return "TLS WebSocket checks are not supported yet.";
207+
case WsFailureKind::Unknown:
208+
return "WebSocket check failed.";
209+
}
210+
211+
return "WebSocket check failed.";
212+
}
213+
214+
std::string failure_fix(WsFailureKind kind)
215+
{
216+
switch (kind)
217+
{
218+
case WsFailureKind::Dns:
219+
return "check the domain name, DNS record and network resolver";
220+
case WsFailureKind::ConnectionRefused:
221+
return "check that the WebSocket service is running and listening on the selected port";
222+
case WsFailureKind::Timeout:
223+
return "check firewall rules, service availability, Nginx upstream and network reachability";
224+
case WsFailureKind::MissingUpgrade:
225+
return "check Nginx proxy_set_header Upgrade and Connection headers";
226+
case WsFailureKind::BadPath:
227+
return "check the WebSocket route/path configured by the application";
228+
case WsFailureKind::ProxyHttpResponse:
229+
return "check that the endpoint is a WebSocket endpoint and not a normal HTTP route";
230+
case WsFailureKind::BadHandshake:
231+
return "check WebSocket port, route/path and proxy upgrade configuration";
232+
case WsFailureKind::TlsUnsupported:
233+
return "use ws:// for local checks until native wss:// support is added";
234+
case WsFailureKind::Unknown:
235+
return "run with --verbose and check the WebSocket server logs";
236+
}
237+
238+
return "run with --verbose and check the WebSocket server logs";
239+
}
240+
87241
std::optional<ParsedWsUrl> parse_ws_url(
88242
const std::string &url,
89243
std::string &error)
@@ -157,16 +311,22 @@ namespace vix::commands::ws::checker
157311
return std::nullopt;
158312
}
159313

160-
if (!is_digit_string(parsed.port))
314+
if (!is_valid_port_number(parsed.port))
161315
{
162-
error = "WebSocket URL port must be numeric";
316+
error = "WebSocket URL port must be between 1 and 65535";
163317
return std::nullopt;
164318
}
165319
}
166320

167321
if (parsed.target.front() != '/')
168322
parsed.target.insert(parsed.target.begin(), '/');
169323

324+
if (parsed.target.find('#') != std::string::npos)
325+
{
326+
error = "WebSocket URL fragments are not supported";
327+
return std::nullopt;
328+
}
329+
170330
return parsed;
171331
}
172332

@@ -306,14 +466,17 @@ namespace vix::commands::ws::checker
306466

307467
if (state.errored)
308468
{
469+
const std::string errorMessage = state.error;
470+
const WsFailureKind failureKind = classify_failure(errorMessage);
471+
309472
client->close();
310473

311-
output::error(std::cerr, "WebSocket handshake failed.");
474+
output::error(std::cerr, failure_title(failureKind));
312475

313-
if (!state.error.empty())
314-
output::warn(std::cerr, state.error);
476+
if (!errorMessage.empty())
477+
output::warn(std::cerr, errorMessage);
315478

316-
output::fix(std::cerr, "check WebSocket port, path and proxy upgrade headers");
479+
output::fix(std::cerr, failure_fix(failureKind));
317480
return 1;
318481
}
319482

@@ -332,22 +495,15 @@ namespace vix::commands::ws::checker
332495
if (options.ping)
333496
{
334497
output::step(std::cout, "Ping");
335-
output::command(std::cout, "send websocket ping frame");
498+
output::command(std::cout, "check ping capability");
336499

337-
try
338-
{
339-
client->send_ping();
340-
std::this_thread::sleep_for(std::chrono::milliseconds(150));
341-
output::ok(std::cout, "ping frame sent");
342-
}
343-
catch (const std::exception &e)
344-
{
345-
client->close();
500+
output::warn(
501+
std::cout,
502+
"ping diagnostic disabled");
346503

347-
output::error(std::cerr, "failed to send WebSocket ping");
348-
output::warn(std::cerr, e.what());
349-
return 1;
350-
}
504+
output::fix(
505+
std::cout,
506+
"use --no-ping or check heartbeat logs");
351507
}
352508

353509
client->close();

src/commands/ws/WsOutput.cpp

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,22 @@ namespace vix::commands::ws::output
4848

4949
return cfg.localUrl;
5050
}
51+
52+
std::string selected_url_source(
53+
const WsConfig &cfg,
54+
const WsOptions &options)
55+
{
56+
if (!options.url.empty())
57+
return "cli";
58+
59+
if (!cfg.publicUrl.empty())
60+
return "production.websocket.public_url";
61+
62+
if (!cfg.localUrl.empty())
63+
return "production.websocket.local_url";
64+
65+
return "generated from config";
66+
}
5167
}
5268

5369
void print_summary(
@@ -60,11 +76,12 @@ namespace vix::commands::ws::output
6076
vix::cli::util::kv(out, "App", cfg.appName);
6177
vix::cli::util::kv(out, "Target", target_name(options.target));
6278
vix::cli::util::kv(out, "URL", selected_url(cfg, options));
79+
vix::cli::util::kv(out, "URL Source", selected_url_source(cfg, options));
6380
vix::cli::util::kv(out, "Local URL", cfg.localUrl.empty() ? "(missing)" : cfg.localUrl);
6481
vix::cli::util::kv(out, "Public URL", cfg.publicUrl.empty() ? "(missing)" : cfg.publicUrl);
65-
vix::cli::util::kv(out, "Host", cfg.host);
66-
vix::cli::util::kv(out, "Port", std::to_string(cfg.port));
67-
vix::cli::util::kv(out, "Path", cfg.path);
82+
vix::cli::util::kv(out, "Configured Host", cfg.host);
83+
vix::cli::util::kv(out, "Configured Port", std::to_string(cfg.port));
84+
vix::cli::util::kv(out, "Configured Path", cfg.path);
6885
vix::cli::util::kv(out, "Timeout", std::to_string(cfg.timeoutMs) + "ms");
6986
vix::cli::util::kv(out, "Ping", yes_no(options.ping));
7087
vix::cli::util::kv(out, "Heartbeat", yes_no(cfg.heartbeat));

0 commit comments

Comments
 (0)