From 694deb9e69a3a1b9f22c4a2e2c77b02c3395b9f7 Mon Sep 17 00:00:00 2001 From: Hartley McGuire Date: Fri, 30 Jan 2026 15:49:11 -0500 Subject: [PATCH] Fix spawn_on_env vars leaking from server to app Previously, variables specified by `spawn_on_env` would behave inconsistently when the Spring server was created with one of those variables set: ``` VAR_FROM_BOOT=before bin/rails ... ``` would cause future clients' apps to be booted with `VAR_FROM_BOOT=before` set, even if the client is created without that variable: ``` bin/rails ... ``` and additionally, Spring would hide that the app was _booted_ with that var set by _unsetting_ it when the client attached to its app (making it look like the correct ENV). The issue was caused by how `spawn_on_env` was passed from client to server to app: - The client would slice `spawn_on_env` keys from its `ENV`, meaning `bin/rails ...` would have an empty `spawn_on_env` hash. - The server (which has `VAR_FROM_BOOT` set), forks an app with `spawn_on_env` values set (none, because its an empty hash) - The forked app inherits its parent's `ENV`, so it boots with `VAR_FROM_BOOT` set - The client gets a forked app process, and cleans up environment variables from the server process This is specifically an issue for `spawn_on_env` because the goal is to have apps booted with different environments. This commit fixes the issue by always passing all `Spring.spawn_on_env` values from the client to the server (including `nil` ones) so that the forked app will have those values cleared if necessary. `compact` is added to the `SPRING_SPAWN_ENV` value because it is only used to display the `spawn_on_env` values in `spring status` and showing all of the `nil` values is noisy/unnecessary. --- lib/spring/application_manager.rb | 2 +- lib/spring/client/run.rb | 4 +++- test/support/acceptance_test.rb | 12 ++++++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/spring/application_manager.rb b/lib/spring/application_manager.rb index ce8c63bc..1a3325c8 100644 --- a/lib/spring/application_manager.rb +++ b/lib/spring/application_manager.rb @@ -102,7 +102,7 @@ def start_child(preload = false) "RACK_ENV" => app_env, "SPRING_ORIGINAL_ENV" => JSON.dump(Spring::ORIGINAL_ENV), "SPRING_PRELOAD" => preload ? "1" : "0", - "SPRING_SPAWN_ENV" => JSON.dump(spawn_env), + "SPRING_SPAWN_ENV" => JSON.dump(spawn_env.compact), **spawn_env, }, "ruby", diff --git a/lib/spring/client/run.rb b/lib/spring/client/run.rb index 52ec20cf..fcb72b19 100644 --- a/lib/spring/client/run.rb +++ b/lib/spring/client/run.rb @@ -254,7 +254,9 @@ def default_rails_env end def spawn_env - ENV.slice(*Spring.spawn_on_env) + Spring.spawn_on_env.to_h do |key| + [key, ENV[key]] + end end end end diff --git a/test/support/acceptance_test.rb b/test/support/acceptance_test.rb index 6043e4f4..c481c531 100644 --- a/test/support/acceptance_test.rb +++ b/test/support/acceptance_test.rb @@ -663,6 +663,18 @@ def exec_name assert_success %(bin/rails runner 'p ENV.key?("FOO")'), stdout: "false" end + test "spawn_on_env variables are cleared when unset" do + File.write(app.spring_client_config, "Spring.spawn_on_env << 'VAR_FROM_BOOT'") + File.write(app.application_config, "#{app.application_config.read}\nRails.configuration.x.var_from_boot = ENV['VAR_FROM_BOOT']") + + app.env["VAR_FROM_BOOT"] = "before" + assert_success %(bin/rails runner 'p Rails.configuration.x.var_from_boot.inspect'), stdout: "before" + + app.env.delete "VAR_FROM_BOOT" + + assert_success %(bin/rails runner 'p Rails.configuration.x.var_from_boot.inspect'), stdout: "nil" + end + test "Kernel.raise remains private" do expr = "p Kernel.private_instance_methods.include?(:raise)" assert_success %(bin/rails runner '#{expr}'), stdout: "true"