Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
1 change: 1 addition & 0 deletions ixwebsocket/IXWebSocket.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,7 @@ namespace ix

connectErr.reason = status.errorStr;
connectErr.http_status = status.http_status;
connectErr.headers = status.headers;

_onMessageCallback(ix::make_unique<WebSocketMessage>(WebSocketMessageType::Error,
emptyMsg,
Expand Down
3 changes: 3 additions & 0 deletions ixwebsocket/IXWebSocketErrorInfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

#pragma once

#include "IXWebSocketHttpHeaders.h"
#include <cstdint>
#include <string>

Expand All @@ -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
71 changes: 71 additions & 0 deletions test/IXWebSocketHttpHeadersTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,15 @@

#include <algorithm>
#include <catch_amalgamated.hpp>
#include <chrono>
#include <cstring>
#include <future>
#include <ixwebsocket/IXGetFreePort.h>
#include <ixwebsocket/IXHttp.h>
#include <ixwebsocket/IXSocket.h>
#include <ixwebsocket/IXWebSocket.h>
#include <ixwebsocket/IXWebSocketHttpHeaders.h>
#include <ixwebsocket/IXWebSocketServer.h>

namespace
{
Expand All @@ -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<ix::Socket> socket,
std::shared_ptr<ix::ConnectionState> 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]")
Expand Down Expand Up @@ -129,3 +161,42 @@ TEST_CASE("Incomplete or cancelled HTTP headers", "[http][headers]")
std::unique_ptr<ix::Socket> 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<ix::HttpResponse>(status,
status == 401 ? "Unauthorized" : "Service Unavailable",
ix::HttpErrorCode::Ok,
headers,
""));
REQUIRE(server.listen().first);
server.start();

std::promise<ix::WebSocketErrorInfo> 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");
}
Loading