Andrew Andrew - 1 month ago 20
Ruby Question

How to run a simple Ruby Rack app that consists of only middleware?

I have a simple Ruby Rack app that consists of only middleware. I don't really need an "app", since the middleware does everything that I want. How can I define my

config.ru
to run only the middleware as the app?

require "some_middleware"
use SomeMiddleware
run -> {|env| [200, {"Content-Type" => "text/plain"}, ["I don't need this part!"]] }

Answer

A Rack app (including middleware) is basically just an object that responds to call, accepting a hash describing the request and returning an array describing the response.

When using a rackup file, Rack uses the DSL described in Rack::Builder. The run method just sets the “base” app:

def run(app)
  @run = app
end

The use method stores the middleware classes you want to include in your app, and then when constructing the final app does something like this for each one (in reverse order of how they appear in the config.ru file):

@run = MiddleWare.new(@run, other_args)

It’s a little more complex than this, but this is the general idea. A new instance of the middleware class is created, with the existing app passed as the first argument of the constructor, and the resulting object becomes the new app. This is the (not well documented) “interface” to Rack middleware: the first argument to its initializer is the app it is wrapping, the rest are any other arguments passed to use.

The DSL expects there will always be either a run or map statement, you will get an error if you omit both.

If your middleware is written in such a way that it can handle having no arguments passed to its initializer and it will behave like a full app, then you may be able to use it directly as the app in config.ru:

run SomeMiddleware.new

This is what Sinatra does to allow it to be used as middleware. It stores the app in the initializer if it is given, and then when a request arrives that doesn’t match any route it uses the presence of @app to decide whether to behave as middleware and pass the request on, or to behave as the final app and treat it as a not found error.

If your middleware doesn’t have this flexibility then you will need to provide an app for it to wrap, as you have in your example. In this case it may also be useful to have the app for error handling, in the event that the middleware doesn’t correctly handle a request and tries to pass it on to the wrapped app.

If you want to avoid using separate use and run statements in your config.ru, you could just use run, and pass a simple app directly to your middleware:

run SomeMiddleware.new(->(e){
  [500, {'Content-type' => 'text/plain'}, ["Error: SomeMiddleware didn't handle request"]]
}

Note how this follows the interface described above for middleware: the first argument to the initializer is the app to wrap.