From 1956070d9b092b5799cf9ce3d18f6290f2cda3a2 Mon Sep 17 00:00:00 2001 From: Benjamin Sergeant Date: Mon, 9 Nov 2026 11:41:51 -0800 Subject: [PATCH] Expose rejected handshake response headers in error events --- docs/usage.md | 2 + ixwebsocket/IXWebSocket.cpp | 1 + ixwebsocket/IXWebSocketErrorInfo.h | 3 ++ test/IXWebSocketHttpHeadersTest.cpp | 71 +++++++++++++++++++++++++++++ 4 files changed, 77 insertions(+) diff --git a/docs/usage.md b/docs/usage.md index f863d475..ca8ef6fa 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -147,6 +147,8 @@ webSocket.setOnMessageCallback([](const ix::WebSocketMessagePtr& msg) A message will be fired when there is an error with the connection. The message type will be `ix::WebSocketMessageType::Error`. Multiple fields will be available on the event to describe the error. +When a server rejects the WebSocket handshake, `msg->errorInfo.headers` contains the HTTP response headers, such as `Retry-After` or custom authentication headers. Header names are case insensitive. Errors without response headers leave this map empty. + ```cpp webSocket.setOnMessageCallback([](const ix::WebSocketMessagePtr& msg) { diff --git a/ixwebsocket/IXWebSocket.cpp b/ixwebsocket/IXWebSocket.cpp index 992d9fce..d9bcb6f9 100644 --- a/ixwebsocket/IXWebSocket.cpp +++ b/ixwebsocket/IXWebSocket.cpp @@ -410,6 +410,7 @@ namespace ix connectErr.reason = status.errorStr; connectErr.http_status = status.http_status; + connectErr.headers = status.headers; _onMessageCallback(ix::make_unique(WebSocketMessageType::Error, emptyMsg, diff --git a/ixwebsocket/IXWebSocketErrorInfo.h b/ixwebsocket/IXWebSocketErrorInfo.h index 16f3215e..e4d142e0 100644 --- a/ixwebsocket/IXWebSocketErrorInfo.h +++ b/ixwebsocket/IXWebSocketErrorInfo.h @@ -6,6 +6,7 @@ #pragma once +#include "IXWebSocketHttpHeaders.h" #include #include @@ -18,5 +19,7 @@ namespace ix int http_status = 0; std::string reason; bool decompressionError = false; + // HTTP response headers available when the server rejects the handshake. + WebSocketHttpHeaders headers; }; } // namespace ix diff --git a/test/IXWebSocketHttpHeadersTest.cpp b/test/IXWebSocketHttpHeadersTest.cpp index d767e5a2..890b06f5 100644 --- a/test/IXWebSocketHttpHeadersTest.cpp +++ b/test/IXWebSocketHttpHeadersTest.cpp @@ -5,9 +5,15 @@ #include #include +#include #include +#include +#include +#include #include +#include #include +#include namespace { @@ -32,6 +38,32 @@ namespace std::string _input; size_t _offset; }; + + class RejectedHandshakeServer final : public ix::WebSocketServer + { + public: + explicit RejectedHandshakeServer(const ix::HttpResponsePtr& response) + : ix::WebSocketServer(ix::getFreePort(), "127.0.0.1") + , _response(response) + { + } + + ~RejectedHandshakeServer() override + { + stop(); + } + + private: + void handleConnection(std::unique_ptr socket, + std::shared_ptr state) override + { + if (std::get<0>(ix::Http::parseRequest(socket, 2))) + ix::Http::sendResponse(_response, socket); + state->setTerminated(); + } + + ix::HttpResponsePtr _response; + }; } // namespace TEST_CASE("HTTP header line endings", "[http][headers]") @@ -129,3 +161,42 @@ TEST_CASE("Incomplete or cancelled HTTP headers", "[http][headers]") std::unique_ptr socket(new HttpHeadersTestSocket(input)); CHECK_FALSE(ix::parseHttpHeaders(socket, [cancelled] { return cancelled; }).first); } + +TEST_CASE("Rejected WebSocket handshakes expose response headers", "[websocket][http][headers]") +{ + const int status = GENERATE(401, 503); + CAPTURE(status); + const ix::WebSocketHttpHeaders headers = { + {"X-Error-Reason", status == 401 ? "token_expired" : "server_busy"}, {"Retry-After", "60"}}; + RejectedHandshakeServer server( + std::make_shared(status, + status == 401 ? "Unauthorized" : "Service Unavailable", + ix::HttpErrorCode::Ok, + headers, + "")); + REQUIRE(server.listen().first); + server.start(); + + std::promise error; + auto result = error.get_future(); + ix::WebSocket client; + client.disableAutomaticReconnection(); + client.setUrl("ws://127.0.0.1:" + std::to_string(server.getPort()) + "/"); + client.setOnMessageCallback( + [&error](const ix::WebSocketMessagePtr& message) + { + if (message->type == ix::WebSocketMessageType::Error) + error.set_value(message->errorInfo); + }); + client.start(); + const auto ready = result.wait_for(std::chrono::seconds(2)); + client.stop(); + server.stop(); + + REQUIRE(ready == std::future_status::ready); + const auto info = result.get(); + CHECK(info.http_status == status); + CHECK_FALSE(info.reason.empty()); + CHECK(info.headers.at("x-error-reason") == headers.at("X-Error-Reason")); + CHECK(info.headers.at("retry-after") == "60"); +}