I learned a very subtle Ruby trick today.
The Ruby parser will create local variables for every variable that might be set in your code before any of it is run.
irb(main):001:0> if false; x = 1; end => nil irb(main):002:0> x.inspect => "nil" irb(main):003:0>
Compare with just checking for x
:
irb(main):001:0> x
NameError: undefined local variable or method `x' for main:Object
from (irb):1
from /Users/aaronpk/.rubies/ruby-2.1.3/bin/irb:11:in `<main>'
Just to confirm what's happening:
irb(main):001:0> local_variables
=> [:_]
irb(main):002:0> if false; x = 1; end
=> nil
irb(main):003:0> local_variables
=> [:x, :_]
irb(main):004:0> x.inspect
=> "nil"
irb(main):005:0>
This may not seem particularly unusual at first, but has some surprising results when combined with, for example, Sinatra. Imagine you have this code that attempts to accept both a form-encoded and JSON post body.
post '/example' do if request.content_type.start_with? "application/json" begin params = JSON.parse(request.env["rack.input"].read) rescue return {error: "Error parsing JSON."}.to_json end end # etc etc # but params is always nil, even for form-encoded requests! end
What's wrong with this picture? Well, the Ruby interpreter sees params =
in the code and allocates a local variable. At that point, the hash that Sinatra sets isn't accessible from inside your block, so params
will be nil when you try to use it!
The trick is to avoid setting params
in the first place.
get '/example/:id' do if request.content_type == "application/json" begin payload = JSON.parse(request.env["rack.input"].read) rescue return {error: "Error parsing JSON."}.to_json end else payload = params end # etc etc # now you can use `payload` instead of params end
Thanks @donpdonp for the hint!