Skip to content

Commit b79564b

Browse files
justin808claude
andauthored
Wait for the app route before auto-opening bin/dev (#2885)
## Problem Fresh apps can open the browser as soon as Puma starts listening on `localhost:3000`, before Shakapacker has finished producing `manifest.json` and the first pack assets. That produces a temporary `Shakapacker::Manifest::MissingEntryError` page on cold boot even though the app would be fine a moment later. This is especially noisy for AI tools and browser automation that immediately inspect the first opened page. ## Summary - wait for the requested route to return a successful or redirect HTTP response before auto-opening the browser - keep the existing one-time auto-open behavior, but make it align with actual app readiness instead of raw port availability - cover the readiness checks in `ServerManager` specs ## Testing - bundle exec rspec react_on_rails/spec/react_on_rails/dev/server_manager_spec.rb - manual cold-start verification against a generated `--rsc` app after deleting `public/packs`, confirming browser open happens only after `GET /` returns `200 OK` <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk: changes are limited to `bin/dev` developer ergonomics (browser auto-open timing) plus additional specs; runtime/production code paths are unaffected. > > **Overview** > **Improves `bin/dev` browser auto-open reliability** by polling the target *app route* over HTTP (success/redirect) instead of just checking that the port is open, reducing premature opens during cold boot. > > Refactors route handling into normalized URL/path helpers, adds HTTP probing via `Net::HTTP` across localhost addresses, and extends `ServerManager` specs to cover route normalization and readiness behavior. Also updates the Pro TanStack Router test `compatAct` helper to require `React.act` (dropping the `react-dom/test-utils` fallback). > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit b70d747. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Improvements** * Development server’s browser auto-open now waits for the app route to respond over HTTP before launching the browser, reducing premature opens when the server port is reachable but the app isn’t ready. * **Bug Fixes** * Treats HTTP success/redirect responses as ready and non-success responses as not ready to avoid false positives. * **Tests** * Updated test coverage around browser auto-open readiness and route normalization; test helper behavior adjusted for newer React act availability. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4b73aef commit b79564b

4 files changed

Lines changed: 87 additions & 20 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ After a release, run `/update-changelog` in Claude Code to analyze commits, writ
2828

2929
- **Fresh app onboarding for `create-react-on-rails-app`**: New apps now land on a generated root page with links to the local demos, docs, OSS vs Pro guidance, the Pro quick start, and the marketplace RSC demo. `bin/dev` opens that page on first boot, `--rsc` scaffolds the same fresh-app experience, and the generated app records step-by-step educational git commits for each scaffold phase. [PR 2849](https://github.com/shakacode/react_on_rails/pull/2849) by [justin808](https://github.com/justin808).
3030

31+
#### Fixed
32+
33+
- **`bin/dev` browser auto-open now waits for route readiness**: `--open-browser` and `--open-browser-once` now poll the target app route and open the browser only after receiving a success or redirect response, reducing premature opens during boot. [PR 2885](https://github.com/shakacode/react_on_rails/pull/2885) by [justin808](https://github.com/justin808).
34+
3135
### [16.5.1] - 2026-03-27
3236

3337
#### Fixed

packages/react-on-rails-pro/tests/tanstackRouter.test.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,10 @@ type ActCallback = () => void | Promise<void>;
4040

4141
async function compatAct(callback: ActCallback): Promise<void> {
4242
const reactAct = (React as typeof React & { act?: (cb: ActCallback) => Promise<unknown> | unknown }).act;
43-
if (typeof reactAct === 'function') {
44-
await reactAct(callback);
45-
return;
43+
if (typeof reactAct !== 'function') {
44+
throw new Error('React.act is not available — React 18.3+ or 19+ is required');
4645
}
47-
48-
const { act } = await import('react-dom/test-utils');
49-
await act(callback);
46+
await reactAct(callback);
5047
}
5148

5249
describe('tanstack-router integration (Pro)', () => {

react_on_rails/lib/react_on_rails/dev/server_manager.rb

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
require "English"
44
require "fileutils"
5+
require "net/http"
56
require "open3"
67
require "optparse"
78
require "rainbow"
@@ -864,19 +865,31 @@ def schedule_browser_open_if_requested(procfile, route:, open_browser:, open_bro
864865
end
865866

866867
def build_local_url(port, route)
867-
normalized_route = route.to_s.strip
868-
return "http://localhost:#{port}" if normalized_route.empty? || normalized_route == "/"
868+
path = normalize_route_path(route)
869+
path = "" if path == "/"
870+
"http://localhost:#{port}#{path}"
871+
end
872+
873+
def build_request_path(route)
874+
normalize_route_path(route)
875+
end
876+
877+
def normalize_route_path(route)
878+
stripped = route.to_s.strip
879+
return "/" if stripped.empty? || stripped == "/"
869880

870-
normalized_route = normalized_route.sub(%r{\A/+}, "")
871-
"http://localhost:#{port}/#{normalized_route}"
881+
stripped = stripped.sub(%r{\A/+}, "")
882+
"/#{stripped}"
872883
end
873884

874885
def schedule_browser_open(port, route:, once:)
875886
return unless browser_auto_open_allowed?
876887

877888
url = build_local_url(port, route)
889+
request_path = build_request_path(route)
878890
Thread.new do
879-
next unless wait_for_server_on_port(port)
891+
Thread.current.report_on_exception = false if Thread.current.respond_to?(:report_on_exception=)
892+
next unless wait_for_app_route(port, request_path)
880893

881894
marker_state = prepare_browser_open_once_marker(once)
882895
next if marker_state == :already_opened
@@ -896,25 +909,35 @@ def browser_auto_open_allowed?
896909
!ENV.key?("CI") && $stdin.tty? && $stdout.tty?
897910
end
898911

899-
def wait_for_server_on_port(port)
912+
def wait_for_app_route(port, request_path)
900913
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + OPEN_BROWSER_WAIT_TIMEOUT
901914

902915
loop do
903-
return true if localhost_port_open?(port)
916+
return true if app_route_ready?(port, request_path)
904917
return false if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
905918

906919
sleep OPEN_BROWSER_POLL_INTERVAL
907920
end
908921
end
909922

910-
def localhost_port_open?(port)
911-
%w[127.0.0.1 ::1].any? do |host|
912-
Socket.tcp(host, port, connect_timeout: 1) do
913-
true
923+
LOCALHOST_ADDRESSES = %w[127.0.0.1 ::1].freeze
924+
private_constant :LOCALHOST_ADDRESSES
925+
926+
def app_route_ready?(port, request_path)
927+
response = http_get_localhost(port, request_path)
928+
response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPRedirection)
929+
end
930+
931+
def http_get_localhost(port, request_path)
932+
LOCALHOST_ADDRESSES.each do |host|
933+
response = Net::HTTP.start(host, port, open_timeout: 1, read_timeout: 1) do |http|
934+
http.get(request_path)
914935
end
936+
return response if response
915937
rescue StandardError
916-
false
938+
next
917939
end
940+
nil
918941
end
919942

920943
def open_browser(url)

react_on_rails/spec/react_on_rails/dev/server_manager_spec.rb

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,49 @@ def mock_system_calls
256256
end
257257
end
258258

259+
describe "browser auto-open readiness" do
260+
it "normalizes routes to request paths" do
261+
expect(described_class.send(:build_request_path, nil)).to eq("/")
262+
expect(described_class.send(:build_request_path, "/")).to eq("/")
263+
expect(described_class.send(:build_request_path, "hello_world")).to eq("/hello_world")
264+
expect(described_class.send(:build_request_path, "/hello_server")).to eq("/hello_server")
265+
end
266+
267+
it "treats a successful response as ready" do
268+
response = Net::HTTPOK.new("1.1", "200", "OK")
269+
allow(described_class).to receive(:http_get_localhost).with(3000, "/").and_return(response)
270+
271+
expect(described_class.send(:app_route_ready?, 3000, "/")).to be true
272+
end
273+
274+
it "treats a redirect response as ready" do
275+
response = Net::HTTPFound.new("1.1", "302", "Found")
276+
allow(described_class).to receive(:http_get_localhost).with(3000, "/").and_return(response)
277+
278+
expect(described_class.send(:app_route_ready?, 3000, "/")).to be true
279+
end
280+
281+
it "does not treat a server error response as ready" do
282+
response = Net::HTTPInternalServerError.new("1.1", "500", "Internal Server Error")
283+
allow(described_class).to receive(:http_get_localhost).with(3000, "/").and_return(response)
284+
285+
expect(described_class.send(:app_route_ready?, 3000, "/")).to be false
286+
end
287+
288+
it "waits for the route to respond successfully before opening the browser" do
289+
allow(described_class).to receive(:browser_auto_open_allowed?).and_return(true)
290+
original_report_on_exception = Thread.current.report_on_exception
291+
allow(Thread).to receive(:new).and_yield
292+
allow(described_class).to receive(:wait_for_app_route).with(3000, "/").and_return(true)
293+
allow(described_class).to receive(:prepare_browser_open_once_marker).with(true).and_return(:claimed)
294+
expect(described_class).to receive(:open_browser).with("http://localhost:3000").and_return(true)
295+
296+
described_class.send(:schedule_browser_open, 3000, route: "/", once: true)
297+
ensure
298+
Thread.current.report_on_exception = original_report_on_exception
299+
end
300+
end
301+
259302
describe ".kill_processes" do
260303
before do
261304
allow_any_instance_of(Kernel).to receive(:`).and_return("")
@@ -1011,7 +1054,7 @@ def mock_system_calls
10111054
stub_const("#{described_class}::OPEN_BROWSER_ONCE_MARKER", File.join(marker_dir, "browser_opened_once"))
10121055
allow(described_class).to receive_messages(
10131056
browser_auto_open_allowed?: true,
1014-
wait_for_server_on_port: true
1057+
wait_for_app_route: true
10151058
)
10161059
end
10171060

@@ -1025,7 +1068,7 @@ def mock_system_calls
10251068
end
10261069

10271070
it "warns when the browser-open thread raises unexpectedly" do
1028-
allow(described_class).to receive(:wait_for_server_on_port).and_raise(SocketError, "boom")
1071+
allow(described_class).to receive(:wait_for_app_route).and_raise(SocketError, "boom")
10291072
expect(described_class).to receive(:warn).with("[react_on_rails] Browser auto-open failed: boom")
10301073

10311074
described_class.send(:schedule_browser_open, 3000, route: "/", once: false).join

0 commit comments

Comments
 (0)