liked

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!

Posted a response on your own website? Send a Webmention:

(Even better - implement automatic Webmention sending on your website. And set up indie-config to make reply/repost/like buttons work.)