44

I'm in the process of refactoring some logic built into a Rails application into middleware, and one annoyance I've run into is a seeming lack of convention for where to put them.

Currently I've settled on app/middleware but I could just as easily move it to vendor/middleware or maybe vendor/plugins/middleware...

The biggest problem is having to require the individual files at the top of config/environment.rb

require "app/middleware/system_message"
require "app/middleware/rack_backstage"

or else I get uninitialized constant errors on the config.middleware.use lines. That could get messy very quickly. I'd rather this was tucked away in an initializer somewhere.

Is there a conventional place to put this stuff?


The specific answer I'm looking for with this bounty is: where can I put the require lines so that they are not cluttering the environment.rb file but still get loaded before the config.middleware.use calls? Everything I have tried leads to uninitialized constant errors.


Update: Now that we're using Rails 3.0, I treat a Rails app like any other Rack app; code files for middleware go in lib (or a gem listed in Gemfile) and are required and loaded in config.ru.

Adam Lassek
  • 35,156
  • 14
  • 91
  • 107

7 Answers7

48

As of Rails 3.2, Rack middleware belongs in the app/middleware directory.

It works "out-of-the-box" without any explicit require statements.

Quick example:

I'm using a middleware class called CanonicalHost which is implemented in app/middleware/canonical_host.rb. I've added the following line to production.rb (note that the middleware class is explicitly given, rather than as a quoted string, which works for any environment-specific config files):

config.middleware.use CanonicalHost, "example.com"

If you're adding middleware to application.rb, you'll need to include quotes, as per @mltsy's comment.

config.middleware.use "CanonicalHost", "example.com"
Community
  • 1
  • 1
Mike Jarema
  • 1,187
  • 1
  • 11
  • 16
  • 4
    Could you please add any link for reference? It's not working out of the box for me and Google / Rails guides aren't helping ... thanks! – dgilperez Nov 08 '12 at 12:09
  • I got this working out of the box, after I realized I made a dumb mistake of placing it in my root folder instead of (the already specified) app folder. So, foo_app/app/middleware/file.rb. – d3vkit Mar 28 '13 at 00:05
  • I had to enclose my middleware class name in quotes to get this to work; see my answer below. – Craig Walker Apr 25 '13 at 04:23
  • 5
    Note: this only works (without quotes) if you're adding middleware in an environment-specific file (i.e. `production.rb`) - if you want to add it to all environments by putting it in `application.rb`, you have to use quotes (or explicitly require the file that defines your class), because application.rb is one of the first things that is loaded - before any of the dynamic initialization code loads the `app/middleware` files. – mltsy Jul 07 '14 at 19:22
  • 2
    Note, in Rails 5+ you can no longer use the String for the name of the middleware. You need to use the proper class name. Additionally, according to @rafaelfranca (Rails core), _"middleware can't be in app because they can't be reloaded. They should be in lib and if you put them in lib, require_relative will work."_ https://github.com/rails/rails/issues/25525#issuecomment-479941866 – Joshua Pinter Nov 02 '20 at 02:58
24

You can put it in lib/tableized/file_name.rb. As long as the class you're trying to load is discoverable by its filename, Rails will automatically load the file necessary. So, for example:

config.middleware.use "MyApp::TotallyAwesomeMiddleware"

You would keep in:

lib/my_app/totally_awesome_middleware.rb

Rails catches const_missing and attemts to load files corresponding to the missing constants automatically. Just make sure your names match and you're gravy. Rails even provides nifty helpers that'll help you identify the path for a file easily:

>> ChrisHeald::StdLib.to_s.tableize.singularize
=> "chris_heald/std_lib"

So my stdlib lives in lib/chris_heald/std_lib.rb, and is autoloaded when I reference it in code.

Adam Lassek
  • 35,156
  • 14
  • 91
  • 107
Chris Heald
  • 61,439
  • 10
  • 123
  • 137
11

In my Rails 3.2 app, I was able to get my middleware TrafficCop loading by putting it at app/middleware/traffic_cop.rb, just as @MikeJarema described. I then added this line to my config/application.rb, as instructed:

config.middleware.use TrafficCop

However, upon application start, I kept getting this error:

uninitialized constant MyApp::Application::TrafficCop

Explicitly specifying the root namespace didn't help either:

config.middleware.use ::TrafficCop
# uninitialized constant TrafficCop

For some reason (which I've yet to discover), at this point in the Rails lifecycle, app/middleware wasn't included in the load paths. If I removed the config.middleware.use line, and ran the console, I could access the TrafficCop constant without any issue. But it couldn't find it in app/middleware at config time.

I fixed this by enclosing the middleware class name in quotes, like so:

config.middleware.use "TrafficCop"

This way, I would avoid the uninitialized constant error, since Rails isn't trying to find the TrafficCop class just yet. But, when it starts to build the middleware stack, it will constantize the string. By this time, app/middleware is in the load paths, and so the class will load correctly.

Craig Walker
  • 49,871
  • 54
  • 152
  • 212
  • I'm on rails 3.2.18 and have the same issue when I put this in application.rb - but when I put it in a specific environment config, then it worked fine! IF you follow the load path starting with config.ru, it turns out application.rb is loaded before most other things, then the application is initialized (which loads all the dynamic initializers, including middlewares), and the other environment files are loaded after that, so by then the middleware constants are defined! – mltsy Jul 07 '14 at 19:19
  • 1
    Note, in Rails 5+ you can no longer use the String for the name of the middleware. You need to use the proper class name. Additionally, according to @rafaelfranca (Rails core), _"middleware can't be in app because they can't be reloaded. They should be in lib and if you put them in lib, require_relative will work."_ https://github.com/rails/rails/issues/25525#issuecomment-479941866 – Joshua Pinter Nov 02 '20 at 02:58
5

For Rails 3:

#config/application.rb
require 'lib/rack/my_adapter.rb'
module MyApp
  class Application < Rails::Application
    config.middleware.use Rack::MyAdapter
  end
end
yfeldblum
  • 65,165
  • 12
  • 129
  • 169
  • This still works - and is better than all the other String answers - in Rails 5+ where they disallowed the use of Strings for the name of the middleware. – Joshua Pinter Nov 02 '20 at 03:00
0

I'm not aware of a convention, but why not put it in the /lib directory? Files in there get automatically loaded by Rails.

John Topley
  • 113,588
  • 46
  • 195
  • 237
  • Not until after environment.rb is run, if at all. Moving them to lib didn't solve the uninitialized constant errors. – Adam Lassek Aug 08 '10 at 02:26
0

You could create an initializer which requires the necessary files and then leave the files wherever you want.

According to this the initializers are executed before the rack middleware is loaded.

Jean
  • 21,329
  • 5
  • 46
  • 64
  • Looks like I was a bit optimistic when reading that. I guess config.ru doesn't match you requirements either ? – Jean Aug 11 '10 at 22:23
  • well, that would require using rackup instead of script/server, right? I could do that if I had to but I honestly thought it would be easier than this. – Adam Lassek Aug 12 '10 at 00:02
0

The working solution I have so far is moving the middleware requires to config/middleware.rb and requiring that file in environment.rb, reducing it to a single require which I can live with.

I'd still like to hear how other people have solved this seemingly basic problem of adding middleware to Rails.

Adam Lassek
  • 35,156
  • 14
  • 91
  • 107