Skip to content

Commit 1dbb048

Browse files
Fix HTTPX thread-safety issue with thread-local connections for incremental rendering
Problem: The HTTPX stream_bidi plugin has a critical limitation - only the thread that creates the bidirectional streaming connection can write chunks to the request stream. This causes race conditions in multi-threaded production environments (e.g., Puma) where different HTTP requests run on different threads. When the first request creates a connection on Thread A and stores it in an instance variable, subsequent requests on Thread B cannot write to that connection, causing connection errors and unpredictable behavior. Solution: Implement thread-local storage for incremental rendering connections. Each thread now gets its own persistent bidirectional streaming connection stored in Thread.current instead of a shared instance variable. Changes: - Modified `incremental_connection` to use `Thread.current[:react_on_rails_incremental_connection]` - Added `reset_thread_local_incremental_connections` to properly clean up all thread-local connections - Updated `reset_connection` to call the new cleanup method - Removed `@incremental_connection` instance variable Trade-offs: - ✅ Eliminates race conditions and thread-safety issues - ✅ Simple implementation using Ruby's built-in thread-local storage - ✅ Each thread has isolated, persistent connection - ⚠️ Higher memory usage (one connection per Puma worker thread) - ⚠️ Not optimal for high-scale production with many threads This is a temporary solution. Long-term options: 1. Fix HTTPX to support multi-threaded bidirectional streaming 2. Migrate to async-http gem which is thread-safe by design Fixes #2115 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 8703f5d commit 1dbb048

File tree

1 file changed

+13
-3
lines changed
  • react_on_rails_pro/lib/react_on_rails_pro

1 file changed

+13
-3
lines changed

react_on_rails_pro/lib/react_on_rails_pro/request.rb

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,8 @@ class Request # rubocop:disable Metrics/ClassLength
1010
class << self
1111
def reset_connection
1212
@standard_connection&.close
13-
@incremental_connection&.close
1413
@standard_connection = nil
15-
@incremental_connection = nil
14+
reset_thread_local_incremental_connections
1615
end
1716

1817
def render_code(path, js_code, send_bundle)
@@ -135,8 +134,19 @@ def connection
135134
end
136135
# rubocop:enable Naming/MemoizedInstanceVariableName
137136

137+
# Thread-local connection for incremental rendering
138+
# Each thread gets its own persistent connection to avoid connection pool issues
138139
def incremental_connection
139-
@incremental_connection ||= create_incremental_connection
140+
Thread.current[:react_on_rails_incremental_connection] ||= create_incremental_connection
141+
end
142+
143+
def reset_thread_local_incremental_connections
144+
# Close all thread-local incremental connections
145+
Thread.list.each do |thread|
146+
conn = thread[:react_on_rails_incremental_connection]
147+
conn&.close
148+
thread[:react_on_rails_incremental_connection] = nil
149+
end
140150
end
141151

142152
def perform_request(path, **post_options) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity

0 commit comments

Comments
 (0)