diff --git a/CHANGELOG.md b/CHANGELOG.md index 25b6b53b..c3764299 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ main * Require `ember-cli-rails-assets >= 0.9.0`, which serves the `include_ember_script_tags` startup tags from Vite's development server when the application is served from it +* Add an `origin` option to `dev_server`, separating the origin the browser + loads assets from, from the address the development server binds to — for + containers, where the server must bind to `0.0.0.0` but browsers refuse + that address as an origin 0.13.0.beta1 ------ diff --git a/README.md b/README.md index 122f4e48..9e121b1c 100644 --- a/README.md +++ b/README.md @@ -218,6 +218,26 @@ If something is already listening on the configured `host` and `port`, possible to run `npm start` yourself, on a port the initializer names, and have Rails serve the application from it. +`host` is both the address the development server binds to and — together with +`port` — the origin the browser loads the application's modules from. When the +two differ, configure the browser-facing origin separately with `origin`. The +typical case is Rails running in a container: the development server must bind +to `0.0.0.0` to be reachable through the container's published port, but +browsers refuse to connect to `http://0.0.0.0`: + +```rb +EmberCli.configure do |c| + c.app :frontend, dev_server: { + host: "0.0.0.0", + port: 4200, + origin: "http://localhost:4200", + } +end +``` + +With `host: "0.0.0.0"`, Rails itself reaches the development server through the +loopback interface. + The development server's output is written to `log/ember-..log`. diff --git a/UPGRADING.md b/UPGRADING.md index 916af030..001a3bce 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -36,6 +36,11 @@ following differences in how `ember-cli-rails` treats it: end ``` + When Rails runs in a container and the browser outside of it, bind the + development server to every interface and name the browser-facing origin + separately: `dev_server: { host: "0.0.0.0", port: 4200, origin: + "http://localhost:4200" }`. + To opt out of the development server, disable it: ```rb diff --git a/lib/ember_cli/dev_server.rb b/lib/ember_cli/dev_server.rb index 4c4fe5ec..1f4343e1 100644 --- a/lib/ember_cli/dev_server.rb +++ b/lib/ember_cli/dev_server.rb @@ -37,8 +37,13 @@ def timeout @timeout ||= option(:timeout) { DEFAULT_TIMEOUT }.to_f end + # The origin the browser loads the application's assets from. It defaults + # to the address the development server binds to, but can be configured + # separately for setups where the two differ — a development server bound + # to `0.0.0.0` inside a container, reached by the browser through a + # published port, for instance. def origin - "http://#{host}:#{port}" + @origin ||= option(:origin) { "http://#{host}:#{port}" }.to_s.chomp("/") end # Boots the development server unless something is already listening on @@ -96,7 +101,7 @@ def option(key) def request(request_class, path, headers) start - uri = URI.join(origin, path) + uri = URI.join("http://#{connect_host}:#{port}", path) # Pass `nil` as the proxy address so that a `http_proxy` environment # variable never routes requests for the local server through a proxy. Net::HTTP.start(uri.hostname, uri.port, nil, read_timeout: timeout) do |http| @@ -112,8 +117,19 @@ def request(request_class, path, headers) MSG end + # The address Rails connects to the development server on. `0.0.0.0` asks + # the server to listen on every interface, but is not an address to + # connect to, so requests go to the loopback interface instead. + def connect_host + if host == "0.0.0.0" + "127.0.0.1" + else + host + end + end + def listening? - Socket.tcp(host, port, connect_timeout: CONNECT_TIMEOUT, &:close) + Socket.tcp(connect_host, port, connect_timeout: CONNECT_TIMEOUT, &:close) true rescue SystemCallError, IOError diff --git a/spec/lib/ember_cli/dev_server_spec.rb b/spec/lib/ember_cli/dev_server_spec.rb index cc578cf7..c83318da 100644 --- a/spec/lib/ember_cli/dev_server_spec.rb +++ b/spec/lib/ember_cli/dev_server_spec.rb @@ -27,6 +27,24 @@ expect(origin).to eq("http://0.0.0.0:1234") end + + it "honors a configured origin" do + dev_server = build_dev_server( + options: { host: "0.0.0.0", port: 4200, origin: "http://localhost:4200" }, + ) + + origin = dev_server.origin + + expect(origin).to eq("http://localhost:4200") + end + + it "strips a trailing slash from a configured origin" do + dev_server = build_dev_server(options: { origin: "http://localhost:4200/" }) + + origin = dev_server.origin + + expect(origin).to eq("http://localhost:4200") + end end describe "#port" do @@ -64,6 +82,21 @@ expect(shell.started_with).to be_nil end + it "connects through the loopback interface when bound to every interface" do + server = null_server + server.listen("127.0.0.1", 0) + shell = FakeShell.new + dev_server = build_dev_server( + shell: shell, + options: { host: "0.0.0.0", port: server.port }, + ) + + started = dev_server.start + + expect(started).to be true + expect(shell.started_with).to be_nil + end + it "raises when the development server exits before it listens" do shell = FakeShell.new(running: false) dev_server = build_dev_server(shell: shell) @@ -120,6 +153,22 @@ expect(server.requested_paths).to eq(["/assets/logo.png?v=1"]) end + it "requests the development server itself, not the configured origin" do + server = null_server + server.listen("127.0.0.1", 0, body: "ok") + dev_server = build_dev_server( + options: { + host: "0.0.0.0", + port: server.port, + origin: "http://origin.invalid:9999", + }, + ) + + response = dev_server.get("/") + + expect(response.body).to eq("ok") + end + it "ignores a configured HTTP proxy" do server = null_server server.listen("127.0.0.1", 0, body: "ok")