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!
