public
Description: Ruby on Rails
Homepage: http://rubyonrails.org
Clone URL: git://github.com/rails/rails.git
Make generated Metal bits a pure rack endpoint application (not middleware)

Instead of calling super to pass the request on, return a 404.
The modified app looks like this:

  # app/metal/poller.rb
  class Poller
    def self.call(env)
      if env["PATH_INFO"] =~ /^\/poller/
        [200, {"Content-Type" => "text/html"}, "Hello, World!"]
      else
        [404, {"Content-Type" => "text/html"}, "Not Found"]
      end
    end
  end

But you aren't locked in to just Rails:

  # app/metal/api.rb
  require 'sinatra'
  Sinatra::Application.default_options.merge!(:run => false, :env => 
  :production)
  Api = Sinatra.application unless defined? Api

  get '/interesting/new/ideas' do
    'Hello Sinatra!'
  end
josh (author)
Wed Dec 17 07:53:56 -0800 2008
commit  61a41154f7d50099da371e0d2f22fd25ab9113c2
tree    257314af12fd4b66473752159b14e288af9864ee
parent  97a178bfa4d5101dca73ae931cc9c77385d8c97e
...
536
537
538
539
540
541
 
542
543
544
...
536
537
538
 
 
 
539
540
541
542
0
@@ -536,9 +536,7 @@ Run `rake gems:install` to install the missing gems.
0
     end
0
 
0
     def initialize_metal
0
-      Dir["#{configuration.root_path}/app/metal/*.rb"].each do |file|
0
-        configuration.middleware.use(File.basename(file, '.rb').camelize)
0
-      end
0
+      configuration.middleware.use Rails::Rack::Metal
0
     end
0
 
0
     # Initializes framework-specific settings for each of the loaded frameworks
...
1
2
3
 
 
 
 
 
 
 
 
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
 
 
 
 
 
 
 
 
 
 
 
 
15
16
17
0
@@ -1,21 +1,17 @@
0
 module Rails
0
   module Rack
0
     class Metal
0
+      def self.new(app)
0
+        apps = Dir["#{Rails.root}/app/metal/*.rb"].map do |file|
0
+          File.basename(file, '.rb').camelize.constantize
0
+        end
0
+        apps << app
0
+        ::Rack::Cascade.new(apps)
0
+      end
0
+
0
       NotFound = lambda { |env|
0
         [404, {"Content-Type" => "text/html"}, "Not Found"]
0
       }
0
-
0
-      def self.call(env)
0
-        new(NotFound).call(env)
0
-      end
0
-
0
-      def initialize(app)
0
-        @app = app
0
-      end
0
-
0
-      def call(env)
0
-        @app.call(env)
0
-      end
0
     end
0
   end
0
 end
...
1
2
3
4
5
 
 
6
7
 
8
9
 
10
11
12
...
1
2
3
 
 
4
5
6
 
7
8
 
9
10
11
12
0
@@ -1,12 +1,12 @@
0
 # Allow the metal piece to run in isolation
0
 require(File.dirname(__FILE__) + "/../../config/environment") unless defined?(Rails)
0
 
0
-class <%= class_name %> < Rails::Rack::Metal
0
-  def call(env)
0
+class <%= class_name %>
0
+  def self.call(env)
0
     if env["PATH_INFO"] =~ /^\/<%= file_name %>/
0
-      [200, {"Content-Type" => "text/html"}, "Hello, World!"]
0
+      [200, {"Content-Type" => "text/html"}, ["Hello, World!"]]
0
     else
0
-      super
0
+      [404, {"Content-Type" => "text/html"}, ["Not Found"]]
0
     end
0
   end
0
 end

Comments

trevorturk Wed Dec 17 08:05:28 -0800 2008

Just curious why the generator has been changed to return an array instead of a string now…?

BTW – that you can use Sinatra this easily is 100% awesome. Thank you.

josh Wed Dec 17 08:08:34 -0800 2008

@trevorturk Returning strings is being removed from the Rack spec since Ruby 1.9 doesn’t have String#each. Long story: http://groups.google.com/group/rack-devel/browse_thread/thread/e6132c3509438df

bryanl Wed Dec 17 08:25:58 -0800 2008

I like this much better. Great work Josh!

jnewland Wed Dec 17 08:33:20 -0800 2008

@josh Awesome, it’s easier to understand the distinction between Metal and Middleware now. Updated my blog post to match.

tjogin Wed Dec 17 08:40:05 -0800 2008

The distinction between metal and middleware became clearer, but IMHO the useage turned towards the ugly side.

Here’s how I’d like it to look:

  1. app/metal/poller.rb class Poller def self.call(env) if env[“PATH_INFO”] =~ /^\/poller/ [200, {"Content-Type" => “text/html”}, “Hello, World!”] end end end
josh Wed Dec 17 08:43:02 -0800 2008

@tjogin The initial Rails::Metal class hid this stuff from you. However the initial confusion made me decide to expose it. Nothing magic anymore, thats just a class.

heycarsten Wed Dec 17 09:00:37 -0800 2008

This is awesome.

tjogin Wed Dec 17 09:14:38 -0800 2008

@josh: Personally, I liked it hidden. :P

matthewrudy Wed Dec 17 11:59:24 -0800 2008

yeah. I also liked the way it was.

I think that the default 404 is going to cause the most confusion, It makes me think “I dont want to return a 404!”, and I’d probably delete it.

Saying that, a magical “super” is probably equally as brittle.

Anyway, its good stuff.

foca Wed Dec 17 19:05:33 -0800 2008

What about defining a constant Metal::PassThru = [404, {"Content-Type" => "text/html"}, ["Not Found"]]

And then in the code you can just return Metal::PassThru and voila, you keep the logic plain and simple, and the “magic” 404 becomes a name which clearly states your intentions.

foca Wed Dec 17 19:09:20 -0800 2008

or maybe it’s clearer if PassThru is an exception and the code that adds each metal class on the call chain rescues PassThru and does a next on the iterator? (I didn’t look at the implementation, so maybe this won’t work)

But you get the idea.

bumi Thu Dec 18 04:22:33 -0800 2008

@foca +1

pkoch Sun Dec 21 05:56:36 -0800 2008

what if I really want to 404?

skade Sun Jan 18 15:31:55 -0800 2009

One question: the Rack specification says that Rack-Applications are not allowed to be Classes. AFAIK, it doesn’t enforce this, but wouldn’t it be good to stick to the Spec?

I’m okay with using Cascade and 404.

josh Sun Jan 18 16:04:36 -0800 2009

@skade A valid rack application is anything that responds to “call”, there are no type requirements. Even if you wanted to restrict this to objects, in Ruby classes are objects ;)

pager Mon Jan 19 05:16:13 -0800 2009

@josh http://rack.rubyforge.org/doc/files/SPEC.html “A Rack application is an Ruby object (not a class) that responds to call”

skade Mon Jan 19 09:05:54 -0800 2009

@josh: yes, ruby classes are objects. Thanks for the lesson… But they are special objects with subtle differences. There is a distinction, although classes behave like objects in almost every case. For example: classes are (usually) globally unique, objects (usually) not. And that might just be the reason why the spec explicitly forbids classes.

kairichard Mon Jan 19 12:17:23 -0800 2009

woot, this is gonna be huge +1

cypher Tue Jan 20 03:23:57 -0800 2009

@skade, pager: “object, not a class” means that Rack won’t try to instantiate anything for you. As Josh correctly points out, all it wants is an object that has a “call” method, and classes are objects too.

skade Tue Jan 20 05:40:30 -0800 2009

@cypher, josh: this may be up to interpretation, but I don’t really want to push this.

On the other hand: if you skip Metals implementing #call on a class level and just instantiate them, you even save the need for self. The default construction for Objects is an empty one, so this is easy. I find it a much better interface, especially because of the symmetry to controller actions.

foca Fri Jan 23 05:28:38 -0800 2009

@skade you can always just say

class MyMetal
  def self.call(env)
    new.call(env)
  end

  def call(env)
    # do your stuff here
  end
end
jduff Thu Jun 18 13:47:28 -0700 2009

Does anyone know of an actual workaround or patch so that I can return a 404 from my metal? The docs(http://guides.rubyonrails.org/rails_on_rack.html) say to use some other Rack middleware but I'm not really sure how I'm supposed to differentiate between the 404 my metal returns to say pass on to rails and the 404 I actually want to return to the user.