I think this effect comes from the way ActionController::Reloader
is written. Here's ActionController::Reloader#call
from 2.3.3, note the comment:
def call(env)
Dispatcher.reload_application
status, headers, body = @app.call(env)
# We do not want to call 'cleanup_application' in an ensure block
# because the returned Rack response body may lazily generate its data. This
# is for example the case if one calls
#
# render :text => lambda { ... code here which refers to application models ... }
#
# in an ActionController.
#
# Instead, we will want to cleanup the application code after the request is
# completely finished. So we wrap the body in a BodyWrapper class so that
# when the Rack handler calls #close during the end of the request, we get to
# run our cleanup code.
[status, headers, BodyWrapper.new(body)]
end
Dispatcher.reload_application
doesn't remove auto-loaded constants, Dispatcher.cleanup_application
does. BodyWrapper#close
is written with possible exceptions in mind:
def close
@body.close if @body.respond_to?(:close)
ensure
Dispatcher.cleanup_application
end
However this doesn't help, because if @app.call
in ActionController::Reloader#call
throws an exception, BodyWrapper
doesn't get instantiated, and Dispatcher.cleanup_application
doesn't get called.
Imagine the following scenario:
- I make changes in one of my files which affects API call
- I hit API call and see error, at this point all files including the one with a bug aren't unloaded
- I make a codefix and hit the same API call to check if it worked
- call gets routed the same way as before, to old classes/objects/modules. This throws same error and again leaves loaded constants in memory
This doesn't happen when traditional controllers raise errors because those are handled by ActionController::Rescue
. Such exceptions do not hit ActionController::Reloader
.
Simplest solution would be to put fallback rescue clause into API routing middleware, some variation of this:
def call(env)
# route API call
resuce Exception
Dispatcher.cleanup_application
raise
end
Note that this is my answer to 3 year old question and I followed call stack of 2.3.3. Newer versions of rails may handle things differently.