Rails initializer that runs *after* routes are loaded?
K

3

24

I want to set a class attribute when my Rails app starts up. It requires inspecting some routes, so the routes need to be loaded before my custom code runs. I am having trouble finding a reliable place to hook in.

This works PERFECTLY in the "test" environment:

config.after_initialize do
  Rails.logger.info "#{Rails.application.routes.routes.map(&:path)}"
end

But it doesn't work in the "development" environment (the routes are empty)

For now I seem to have things working in development mode by running the same code in config.to_prepare which I understand happens before every request. Unfortunately using to_prepare alone doesn't seem to work in test mode, hence the duplication.

I'm curious why the routes are loaded before after_initialize in test mode, but not in development mode. And really, what is the best hook for this? Is there a single hook that will work for all environments?

*EDIT*

mu's suggestion of reloading the routes was great. It gave me consistent access to the routes within after_initialize in all environments. For my use case though, I think I still need to run the code from to_prepare as well, since I'm setting a class attribute on a model and the models are reloaded before each request.

So here's what I ended up doing.

[:after_initialize, :to_prepare].each do |hook|
  config.send(hook) do
    User.invalid_usernames += Rails.application.routes.routes.map(&:path).join("\n").scan(/\s\/(\w+)/).flatten.compact.uniq
  end 
end 

It seems a bit messy to me. I think I'd rather do something like:

config.after_initialize do
  User.exclude_routes_from_usernames!
end

config.to_prepare do
  User.exclude_routes_from_usernames!
end

But I'm not sure if User is the right place to be examining Rails.application.routes. I guess I could do the same thing with code in lib/ but I'm not sure if that's right either.

Another option is to just apply mu's suggestion on to_prepare. That works but there seems to be a noticeable delay reloading the routes on every request in my dev environment, so I'm not sure if this is a good call, although it's DRY, at least.

config.to_prepare do
  Rails.application.reload_routes!
  User.invalid_usernames += Rails.application.routes.routes.map(&:path).join("\n").scan(/\s\/(\w+)/).flatten.compact.uniq
end
Klenk answered 3/1, 2012 at 3:7 Comment(0)
B
28

You can force the routes to be loaded before looking at Rails.application.routes with this:

Rails.application.reload_routes!

So try this in your config/application.rb:

config.after_initialize do
  Rails.application.reload_routes!
  Rails.logger.info "#{Rails.application.routes.routes.map(&:path)}"
end

I've done similar things that needed to check the routes (for conflicts with /:slug routes) and I ended up putting the reload_routes! and the checking in a config.after_initialize like you're doing.

Bencion answered 3/1, 2012 at 3:47 Comment(3)
Great idea! Turns out we're actually using this for the same thing (I'm updating a class_attribute called User.invalid_usernames that is used as an exclusion list with validates_exclusion_of). I think I still need the to_prepare for development mode though. Without it, it works fine on the first request (now that I'm using your suggestion), but after that I think my User.invalid_usernames = Set.new is clobbering it. It sounds like you're only using after_initialize though, so I'm wondering if there is some clever way you got around that?Klenk
This answer helped a lot so I'm accepting it although I didn't end up using it (open to criticism if you think I should though!) Here is my full solution, would love your feedback if you have time: https://mcmap.net/q/583051/-rails-validatation-to-ensure-a-username-does-not-clash-with-an-existing-routeKlenk
@poochenza: I used after_initialize to make sure no new conflicts appeared between releases: if you add a /pancakes route and publish two weeks later, you want to know if someone created a "pancakes" user on the production system in the mean time. Then I compare new usernames against the routes as the usernames are created or updated.Bencion
L
4

If you're trying to run code in an initializer after the routes have loaded, you can try using the after: option:

initializer "name_of_initializer", after: :add_routing_paths do |app|
  # do custom logic here
end

You can find initialization events here: http://guides.rubyonrails.org/configuring.html#initialization-events

Lozengy answered 6/3, 2016 at 5:31 Comment(1)
This initializer will never be executed for me in rails 4.1.14.1Influential
M
2

Rails do have the config callback called config.after_routes_loaded which I was expecting to be called on both application initialization and routes reloading. You can find its documentation here:

https://guides.rubyonrails.org/configuring.html#config-after-routes-loaded

On Rails 7.1.2, it is not called during the application initialization; only if routes change, it is called. This is fixed in Rails 7.1.3. The related issue is:

https://github.com/rails/rails/issues/50720

On Rails 7.1.2, I circumvented this problem by adding a config.after_initialize similar to the one in the accepted answer:

    config.after_initialize do
      Rails.application.reload_routes!
      ActiveSupport.run_load_hooks(:after_routes_loaded, self)
    end

after that, I can define my config.after_routes_loaded block, and it gets called in all cases, and at the moment it is called, all routes were already loaded.

Obs.: both configs were defined inside the file config/application.rb

Madeline answered 12/1 at 3:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.