Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ CMakeUserPresets.json
**/CMakeCache.txt
**/Testing/

# Claude Code
.claude/

# IDE files
.idea/
.vs/
Expand Down
84 changes: 84 additions & 0 deletions cmake/asio-iocp-fix.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Patch for ASIO issue #312: IOCP out-of-resources error reporting.
# https://github.com/chriskohlhoff/asio/issues/312
#
# When PostQueuedCompletionStatus fails due to resource exhaustion, the
# completion key (overlapped_contains_result) is lost. The operation falls
# back to an internal queue but do_one() dispatches it with the wrong
# error code and bytes_transferred, causing undefined behavior / crashes.
#
# Fix: store the completion key on the operation object before posting,
# so it survives the fallback path.
#
# Based on MongoDB's patch:
# https://github.com/mongodb-forks/asio/commit/d03c2e7002131305645374e735a8ece4191f2fc5

function(apply_asio_iocp_fix asio_include_dir)
set(OP_HEADER "${asio_include_dir}/asio/detail/win_iocp_operation.hpp")
set(CTX_IMPL "${asio_include_dir}/asio/detail/impl/win_iocp_io_context.ipp")

if(NOT EXISTS "${OP_HEADER}" OR NOT EXISTS "${CTX_IMPL}")
message(WARNING "ASIO IOCP fix: headers not found at ${asio_include_dir}, skipping patch")
return()
endif()

# Check if already patched (look for our added member)
file(READ "${OP_HEADER}" op_content)
if(op_content MATCHES "completionKey_")
message(STATUS "ASIO IOCP fix: already applied")
return()
endif()

message(STATUS "Applying ASIO IOCP fix (issue #312)")

# --- Patch win_iocp_operation.hpp ---
# Add completionKey_ member and accessor

# Add member after "long ready_;"
string(REPLACE
"long ready_;"
"long ready_;\n ULONG_PTR completionKey_;"
op_content "${op_content}")

# Initialize in constructor: find "ready_(0)" and add completionKey_(0)
string(REPLACE
"ready_(0)"
"ready_(0), completionKey_(0)"
op_content "${op_content}")

# Add accessor method before the "private:" or "protected:" section
# Find "func_type func_;" and add the accessor before the member block
string(REPLACE
"win_iocp_operation* next_;"
"ULONG_PTR& completionKey() { return completionKey_; }\n\n win_iocp_operation* next_;"
op_content "${op_content}")

file(WRITE "${OP_HEADER}" "${op_content}")

# --- Patch win_iocp_io_context.ipp ---
file(READ "${CTX_IMPL}" ctx_content)

# Fix post_deferred_completion: pass op->completionKey() instead of 0 for the key
# Original: PostQueuedCompletionStatus(iocp_.handle, 0, 0, op)
# Fixed: PostQueuedCompletionStatus(iocp_.handle, 0, op->completionKey(), op)
string(REPLACE
"::PostQueuedCompletionStatus(iocp_.handle, 0, 0, op)"
"::PostQueuedCompletionStatus(iocp_.handle, 0, op->completionKey(), op)"
ctx_content "${ctx_content}")

# Fix on_pending/on_completion: store key on op before posting
# Original pattern:
# if (!::PostQueuedCompletionStatus(iocp_.handle,
# 0, overlapped_contains_result, op))
# Fixed pattern:
# op->completionKey() = overlapped_contains_result;
# if (!::PostQueuedCompletionStatus(iocp_.handle,
# 0, op->completionKey(), op))
string(REPLACE
"if (!::PostQueuedCompletionStatus(iocp_.handle,\n 0, overlapped_contains_result, op))"
"op->completionKey() = overlapped_contains_result;\n if (!::PostQueuedCompletionStatus(iocp_.handle,\n 0, op->completionKey(), op))"
ctx_content "${ctx_content}")

file(WRITE "${CTX_IMPL}" "${ctx_content}")

message(STATUS "ASIO IOCP fix applied successfully")
endfunction()
37 changes: 32 additions & 5 deletions include/glaze/net/websocket_client.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,32 @@ namespace glz
resolver_.reset();
}

// Reset socket pointers (sockets already closed via force_close above)
// Cancel pending operations on raw sockets before destroying them.
// During the connection/handshake phase, the websocket_connection doesn't
// exist yet, so force_close() above is a no-op. We must cancel any pending
// operations on the raw sockets directly.
{
asio::error_code ec;
if (tcp_socket_) {
tcp_socket_->cancel(ec);
tcp_socket_->close(ec);
}
#ifdef GLZ_ENABLE_SSL
if (ssl_socket_) {
ssl_socket_->lowest_layer().cancel(ec);
ssl_socket_->lowest_layer().close(ec);
}
#endif
}

// Drain any pending completion handlers (e.g. IOCP cancellation completions)
// so they are processed before the io_context or sockets are destroyed.
if (ctx) {
ctx->restart();
ctx->poll();
}

// Now safe to reset socket pointers - all pending operations have completed
tcp_socket_.reset();
#ifdef GLZ_ENABLE_SSL
ssl_socket_.reset();
Expand Down Expand Up @@ -452,17 +477,19 @@ namespace glz
ws_conn->set_client_mode(true);
ws_conn->set_max_message_size(self->max_message_size);

// Set handlers before processing initial data, so that any
// frames in the initial data are properly dispatched.
if (self->on_message && *self->on_message) ws_conn->on_message(*self->on_message);
if (self->on_close && *self->on_close) ws_conn->on_close(*self->on_close);
if (self->on_error && *self->on_error) ws_conn->on_error(*self->on_error);

if (response_buf->size() > 0) {
std::string_view initial_data{
static_cast<const char*>(response_buf->data().data()),
response_buf->size()};
ws_conn->set_initial_data(initial_data);
}

if (self->on_message && *self->on_message) ws_conn->on_message(*self->on_message);
if (self->on_close && *self->on_close) ws_conn->on_close(*self->on_close);
if (self->on_error && *self->on_error) ws_conn->on_error(*self->on_error);

ws_conn->start_read();

{
Expand Down
7 changes: 7 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ else()
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(asio)

# Apply IOCP fix for issue #312 (Windows crash in op->complete)
if(WIN32)
Copy link
Copy Markdown

@GTruf GTruf Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stephenberry, Was the original IOCP problem finally solved?

include(${CMAKE_CURRENT_SOURCE_DIR}/../cmake/asio-iocp-fix.cmake)
apply_asio_iocp_fix("${asio_SOURCE_DIR}/asio/include")
endif()

add_library(glz_asio INTERFACE)
target_include_directories(glz_asio INTERFACE ${asio_SOURCE_DIR}/asio/include)
else()
Expand Down
9 changes: 9 additions & 0 deletions tests/networking_tests/websocket_test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ target_link_libraries(${PROJECT_NAME} PRIVATE glz_test_exceptions)

add_test(NAME ${PROJECT_NAME} COMMAND ${PROJECT_NAME})

# WebSocket client lifetime tests (issue #2409)
project(websocket_client_lifetime_test)

add_executable(${PROJECT_NAME} ${PROJECT_NAME}.cpp)

target_link_libraries(${PROJECT_NAME} PRIVATE glz_test_exceptions)

add_test(NAME ${PROJECT_NAME} COMMAND ${PROJECT_NAME})

# Shared Context Bug Test
project(shared_context_bug_test)

Expand Down
Loading
Loading