Search code examples
ruby-on-railsrubyrestrest-client

Why is this ruby string casting to_i as 200?


I'm working on a rails app that connects via RestClient to a backend data store (also ruby, not rails). I just added this method to the rails app's backend api class to get a file size from the backend:

class Backend < ActiveResource::Base
  def self.file_size(file_key)
    RestClient.get("#{SERVER_URL}/files/#{file_key}/size")
  end
end

And the corresponding code on the backend is:

get '/files/:file_id/size' do
  json_for file.size
end

For an example file, Backend.file_size(file.key) returns "198942", but when this attribute is saved to the database (in an integer field), it is cast to int as 200.

I've played with this within the rails console a bit and the output is confounding.

test_size = Backend.file_size(file.key)
=> "198942"
control_size = "198942"
=> "198942"

test_size.class
=> String
control_size.class
=> String

test_size.bytes.to_a
=> [49, 57, 56, 57, 52, 50]
control_size.bytes.to_a
=> [49, 57, 56, 57, 52, 50]

test_size.to_i
=> 200
control_size.to_i
=> 198942

Integer(test_size)
=> 198942
Integer(control_size)
=> 198942

test_size.to_s.to_i
=> 200
control_size.to_s.to_i
=> 198942

test_size.tr('^A-Za-z0-9', '').to_i
=> 198942

I also checked the encoding, and the rest response was US-ASCII, but its behavior was the same when force_encoded as UTF-8. Bizarrely, test_size casts correctly under Integer and the tr sub to remove any nonprinting characters, but the byte contents are identical to the control string.

I eventually got to the root of this question (answered below), but I'd be interested if anyone has had this experience before, and if anyone has insight into why RestClient responds this way (in this particular case).


Solution

  • After more investigation (looking at the public methods of test_size and control_size), I discovered that the return value from RestClient was a RestClient::Response, even though it looked and quacked like a String otherwise.

    test_size.is_a?(String)
    => true
    test_size.is_a?(RestClient::Response)
    => true
    
    test_size.code
    => 200
    

    This strikes me as very unexpected behavior (although I know it is somewhat unusual to send a naked value through a rest client instead of JSON), and I would be curious if anyone knows how/why RestClient::Response objects seem to be a strange hybrid object that manifests as a String and seems to have monkey patched a bunch to String methods (but not all - eg tr).

    I resolved this problem just by refactoring the front and backend API classes to pass a JSON object.

    Frontend refactored code:

    class Backend < ActiveResource::Base
      def self.file_size(file_key)
        JSON.parse(RestClient.get("#{SERVER_URL}/files/#{file_key}/size"))
      end
    end
    

    Backend code:

    get '/files/:file_id/size' do
      json_for file.size
    end
    
    class File
      def size
        {
          size: size
        }
      end
    end