2626#include < optional>
2727#include < string>
2828#include < thread>
29+ #include < cctype>
2930
3031namespace 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 ();
0 commit comments