Skip to content

Commit b6e16df

Browse files
justin808claude
andcommitted
fix: address review feedback on base port feature
Addresses review comments on PR #3142 (REACT_ON_RAILS_BASE_PORT): - Validate base port against the largest derived offset so base+N stays within the valid TCP port range (1..65_535). A base of 65_534 previously passed validation but produced an invalid renderer port of 65_536. - Enforce the documented priority chain ("base port > explicit per-service env vars") in ServerManager.configure_ports. The former `||=` assignments silently preserved a pre-existing PORT/SHAKAPACKER_DEV_SERVER_PORT, making the effective Rails port disagree with the "Base port detected..." log line. When a base port is active, per-service env vars are now assigned unconditionally. - Preserve RENDERER_PORT and REACT_RENDERER_URL across Bundler.with_unbundled_env by adding them to ProcessManager::ENV_KEYS_TO_PRESERVE. Previously they were dropped for process managers launched outside the bundler context. - Derive REACT_RENDERER_URL from an explicit RENDERER_PORT in non-base-port worktrees when the user set the port but not the URL. - Remove the unused DEFAULT_RENDERER_PORT constant (the 3800 fallback only lives in Procfile shell expressions). - Add port_selector_spec cases for the max-safe and overflow base ports, and server_manager_spec cases for base-port priority override and explicit-RENDERER_PORT URL sync. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 8da5a8a commit b6e16df

5 files changed

Lines changed: 137 additions & 10 deletions

File tree

react_on_rails/lib/react_on_rails/dev/port_selector.rb

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,17 @@
55
module ReactOnRails
66
module Dev
77
class PortSelector
8-
DEFAULT_RAILS_PORT = 3000
9-
DEFAULT_WEBPACK_PORT = 3035
10-
DEFAULT_RENDERER_PORT = 3800
11-
MAX_ATTEMPTS = 100
8+
DEFAULT_RAILS_PORT = 3000
9+
DEFAULT_WEBPACK_PORT = 3035
10+
MAX_ATTEMPTS = 100
1211

1312
# Offsets from the base port when REACT_ON_RAILS_BASE_PORT (or a recognized
1413
# tool-specific equivalent like CONDUCTOR_PORT) is set. The base port block
1514
# is typically 10 consecutive ports allocated per workspace.
1615
BASE_PORT_RAILS_OFFSET = 0
1716
BASE_PORT_WEBPACK_OFFSET = 1
1817
BASE_PORT_RENDERER_OFFSET = 2
18+
MAX_BASE_PORT = 65_535 - BASE_PORT_RENDERER_OFFSET
1919

2020
# Env vars checked (in order) for a base port value.
2121
BASE_PORT_ENV_VARS = %w[REACT_ON_RAILS_BASE_PORT CONDUCTOR_PORT].freeze
@@ -96,9 +96,11 @@ def find_available_port(start_port, exclude: nil)
9696
private
9797

9898
def base_port
99+
# Upper bound accounts for the largest derived offset so base + N stays
100+
# within the valid TCP port range (1..65_535).
99101
BASE_PORT_ENV_VARS.each do |var|
100102
val = ENV[var]&.to_i
101-
return val if val&.between?(1, 65_535)
103+
return val if val&.between?(1, MAX_BASE_PORT)
102104
end
103105
nil
104106
end

react_on_rails/lib/react_on_rails/dev/process_manager.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@ class ProcessManager
1414
# before entering the block and pass them explicitly to system().
1515
# This follows the same pattern used by Rails' bundle_command (railties),
1616
# Spring's process spawning, and this codebase's own PackGenerator.
17-
ENV_KEYS_TO_PRESERVE = %w[PORT SHAKAPACKER_DEV_SERVER_PORT].freeze
17+
ENV_KEYS_TO_PRESERVE = %w[
18+
PORT
19+
SHAKAPACKER_DEV_SERVER_PORT
20+
RENDERER_PORT
21+
REACT_RENDERER_URL
22+
].freeze
1823

1924
class << self
2025
# Check if a process is available and usable in the current execution context

react_on_rails/lib/react_on_rails/dev/server_manager.rb

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -835,17 +835,36 @@ def print_procfile_info(procfile, route: nil)
835835

836836
def configure_ports
837837
selected = PortSelector.select_ports
838-
ENV["PORT"] ||= selected[:rails].to_s
839-
ENV["SHAKAPACKER_DEV_SERVER_PORT"] ||= selected[:webpack].to_s
840838
if selected[:renderer]
841-
ENV["RENDERER_PORT"] ||= selected[:renderer].to_s
842-
ENV["REACT_RENDERER_URL"] ||= "http://localhost:#{selected[:renderer]}"
839+
apply_base_port_env(selected)
840+
else
841+
apply_explicit_port_env(selected)
843842
end
844843
rescue PortSelector::NoPortAvailable => e
845844
warn e.message
846845
exit 1
847846
end
848847

848+
# Base port is active. Priority: base port > explicit per-service env vars.
849+
# Assign unconditionally so the effective ports match the "Base port
850+
# detected..." log line even when PORT/RENDERER_PORT were pre-set.
851+
def apply_base_port_env(selected)
852+
ENV["PORT"] = selected[:rails].to_s
853+
ENV["SHAKAPACKER_DEV_SERVER_PORT"] = selected[:webpack].to_s
854+
ENV["RENDERER_PORT"] = selected[:renderer].to_s
855+
ENV["REACT_RENDERER_URL"] = "http://localhost:#{selected[:renderer]}"
856+
end
857+
858+
def apply_explicit_port_env(selected)
859+
ENV["PORT"] ||= selected[:rails].to_s
860+
ENV["SHAKAPACKER_DEV_SERVER_PORT"] ||= selected[:webpack].to_s
861+
# Non-base-port worktree: if a user set RENDERER_PORT explicitly but
862+
# not REACT_RENDERER_URL, keep them in sync so Rails reaches the right port.
863+
return unless ENV["RENDERER_PORT"] && !ENV["REACT_RENDERER_URL"]
864+
865+
ENV["REACT_RENDERER_URL"] = "http://localhost:#{ENV.fetch('RENDERER_PORT')}"
866+
end
867+
849868
def procfile_port(procfile)
850869
if procfile == "Procfile.dev-prod-assets"
851870
ENV.fetch("PORT", 3001).to_i

react_on_rails/spec/react_on_rails/dev/port_selector_spec.rb

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,40 @@
8888
end
8989
end
9090

91+
context "when base port would push derived renderer port above 65535" do
92+
around do |example|
93+
old = ENV.fetch("REACT_ON_RAILS_BASE_PORT", nil)
94+
# 65_534 + BASE_PORT_RENDERER_OFFSET (2) = 65_536, which is invalid.
95+
ENV["REACT_ON_RAILS_BASE_PORT"] = "65534"
96+
example.run
97+
ENV["REACT_ON_RAILS_BASE_PORT"] = old
98+
end
99+
100+
it "falls back to normal auto-detection instead of deriving an invalid port" do
101+
allow(described_class).to receive(:port_available?).and_return(true)
102+
result = described_class.select_ports
103+
expect(result[:rails]).to eq(3000)
104+
expect(result[:renderer]).to be_nil
105+
end
106+
end
107+
108+
context "when base port is at the maximum safe value" do
109+
around do |example|
110+
old = ENV.fetch("REACT_ON_RAILS_BASE_PORT", nil)
111+
ENV["REACT_ON_RAILS_BASE_PORT"] = described_class::MAX_BASE_PORT.to_s
112+
example.run
113+
ENV["REACT_ON_RAILS_BASE_PORT"] = old
114+
end
115+
116+
it "derives all three ports within the valid TCP range" do
117+
result = described_class.select_ports
118+
expect(result[:rails]).to eq(described_class::MAX_BASE_PORT)
119+
expect(result[:webpack]).to eq(described_class::MAX_BASE_PORT + 1)
120+
expect(result[:renderer]).to eq(described_class::MAX_BASE_PORT + 2)
121+
expect(result[:renderer]).to be <= 65_535
122+
end
123+
end
124+
91125
context "when default ports are free" do
92126
it "returns the default Rails port 3000" do
93127
allow(described_class).to receive(:port_available?).and_return(true)

react_on_rails/spec/react_on_rails/dev/server_manager_spec.rb

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,73 @@ def mock_system_calls
261261
expect { described_class.start(:development) }.not_to raise_error
262262
end
263263
end
264+
265+
context "when configuring ports with a base port active" do
266+
before do
267+
mock_system_calls
268+
allow(ReactOnRails::Dev::PortSelector).to receive(:select_ports)
269+
.and_return({ rails: 5000, webpack: 5001, renderer: 5002 })
270+
end
271+
272+
around do |example|
273+
keys = %w[PORT SHAKAPACKER_DEV_SERVER_PORT RENDERER_PORT REACT_RENDERER_URL]
274+
old = keys.to_h { |k| [k, ENV.fetch(k, nil)] }
275+
keys.each { |k| ENV.delete(k) }
276+
example.run
277+
ensure
278+
old.each { |k, v| v.nil? ? ENV.delete(k) : ENV[k] = v }
279+
end
280+
281+
it "overrides a pre-existing PORT with the base-derived Rails port" do
282+
ENV["PORT"] = "3000"
283+
described_class.start(:development)
284+
expect(ENV.fetch("PORT", nil)).to eq("5000")
285+
end
286+
287+
it "overrides a pre-existing SHAKAPACKER_DEV_SERVER_PORT with the base-derived webpack port" do
288+
ENV["SHAKAPACKER_DEV_SERVER_PORT"] = "3035"
289+
described_class.start(:development)
290+
expect(ENV.fetch("SHAKAPACKER_DEV_SERVER_PORT", nil)).to eq("5001")
291+
end
292+
293+
it "overrides pre-existing RENDERER_PORT and REACT_RENDERER_URL with base-derived values" do
294+
ENV["RENDERER_PORT"] = "3800"
295+
ENV["REACT_RENDERER_URL"] = "http://localhost:3800"
296+
described_class.start(:development)
297+
expect(ENV.fetch("RENDERER_PORT", nil)).to eq("5002")
298+
expect(ENV.fetch("REACT_RENDERER_URL", nil)).to eq("http://localhost:5002")
299+
end
300+
end
301+
302+
context "when RENDERER_PORT is set explicitly without a base port" do
303+
before do
304+
mock_system_calls
305+
allow(ReactOnRails::Dev::PortSelector).to receive(:select_ports)
306+
.and_return({ rails: 3000, webpack: 3035, renderer: nil })
307+
end
308+
309+
around do |example|
310+
keys = %w[PORT SHAKAPACKER_DEV_SERVER_PORT RENDERER_PORT REACT_RENDERER_URL]
311+
old = keys.to_h { |k| [k, ENV.fetch(k, nil)] }
312+
keys.each { |k| ENV.delete(k) }
313+
example.run
314+
ensure
315+
old.each { |k, v| v.nil? ? ENV.delete(k) : ENV[k] = v }
316+
end
317+
318+
it "derives REACT_RENDERER_URL from the explicit RENDERER_PORT" do
319+
ENV["RENDERER_PORT"] = "3801"
320+
described_class.start(:development)
321+
expect(ENV.fetch("REACT_RENDERER_URL", nil)).to eq("http://localhost:3801")
322+
end
323+
324+
it "leaves a pre-existing REACT_RENDERER_URL untouched" do
325+
ENV["RENDERER_PORT"] = "3801"
326+
ENV["REACT_RENDERER_URL"] = "http://renderer.internal:3801"
327+
described_class.start(:development)
328+
expect(ENV.fetch("REACT_RENDERER_URL", nil)).to eq("http://renderer.internal:3801")
329+
end
330+
end
264331
end
265332

266333
describe "browser auto-open readiness" do

0 commit comments

Comments
 (0)