From 45da614dddcceacbdc491ee6c73a935b85702670 Mon Sep 17 00:00:00 2001 From: yhirose Date: Wed, 1 Jul 2026 21:38:11 -0400 Subject: [PATCH] Omit default port from WebSocket handshake Host header (Fix #2480) The WebSocket upgrade request always appended ":port" to the Host header, violating RFC 6455 Section 4.1 which says the port should be included only when it is not the default (80 for ws, 443 for wss). Some CDNs alter routing when the Host header carries an explicit default port. Build the Host header with detail::make_host_and_port_string, which also brackets IPv6 literal hosts correctly. --- httplib.h | 15 +++++++++++---- test/test.cc | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/httplib.h b/httplib.h index e2d7b170..d30f0d68 100644 --- a/httplib.h +++ b/httplib.h @@ -8905,7 +8905,8 @@ inline bool is_field_value(const std::string &s) { return is_field_content(s); } } // namespace fields inline bool perform_websocket_handshake(Stream &strm, const std::string &host, - int port, const std::string &path, + int port, bool is_ssl, + const std::string &path, const Headers &headers, std::string &selected_subprotocol) { // Validate path and host @@ -8931,7 +8932,7 @@ inline bool perform_websocket_handshake(Stream &strm, const std::string &host, // Build upgrade request std::string req_str = "GET " + path + " HTTP/1.1\r\n"; - req_str += "Host: " + host + ":" + std::to_string(port) + "\r\n"; + req_str += "Host: " + make_host_and_port_string(host, port, is_ssl) + "\r\n"; req_str += "Upgrade: websocket\r\n"; req_str += "Connection: Upgrade\r\n"; req_str += "Sec-WebSocket-Key: " + client_key + "\r\n"; @@ -20570,9 +20571,15 @@ inline bool WebSocketClient::connect() { return false; } +#ifdef CPPHTTPLIB_SSL_ENABLED + auto is_ssl = is_ssl_; +#else + auto is_ssl = false; +#endif + std::string selected_subprotocol; - if (!detail::perform_websocket_handshake(*strm, host_, port_, path_, headers_, - selected_subprotocol)) { + if (!detail::perform_websocket_handshake(*strm, host_, port_, is_ssl, path_, + headers_, selected_subprotocol)) { shutdown_and_close(); return false; } diff --git a/test/test.cc b/test/test.cc index 85f9cbc8..d3f8e25a 100644 --- a/test/test.cc +++ b/test/test.cc @@ -18975,6 +18975,47 @@ TEST(WebSocketTest, QueryStringInHandshake) { t.join(); } +TEST(WebSocketTest, HostHeaderInHandshake) { + Server svr; + + std::mutex mtx; + std::string received_host; + + svr.WebSocket("/ws", [&](const Request &req, ws::WebSocket &ws) { + { + std::lock_guard lock(mtx); + received_host = req.get_header_value("Host"); + } + std::string msg; + while (ws.read(msg)) { + ws.send(msg); + } + }); + + auto port = svr.bind_to_any_port("localhost"); + std::thread t([&]() { svr.listen_after_bind(); }); + svr.wait_until_ready(); + + ws::WebSocketClient client("ws://localhost:" + std::to_string(port) + "/ws"); + ASSERT_TRUE(client.connect()); + // Round-trip ensures the handler has run and captured the request. + ASSERT_TRUE(client.send("hello")); + std::string msg; + ASSERT_TRUE(client.read(msg)); + client.close(); + + { + std::lock_guard lock(mtx); + // Non-default port must be present in the Host header. Default ports + // (80/443) are omitted; that logic is covered by + // MakeHostAndPortStringTest. + EXPECT_EQ("localhost:" + std::to_string(port), received_host); + } + + svr.stop(); + t.join(); +} + #ifdef CPPHTTPLIB_OPENSSL_SUPPORT class WebSocketSSLIntegrationTest : public ::testing::Test { protected: