XeroAPI/xero-ruby

undefined method `strip' for an instance of Hash

Opened this issue · 4 comments

After much, much, much debugging, while getting the mysterious error undefined method strip' for an instance of Hash` (btw, whoever decided to catch that error and print it out and carry on without providing the stack trace... I hope you take a good long look at yourself in the mirror some day)...

I ended up diagnosing it as an issue with a parameter has being passed into net/http.

The full stack trace:

undefined method `strip' for an instance of Hash
======== /Users/danieltenner/.rvm/rubies/ruby-3.3.1/lib/ruby/3.3.0/net/http/header.rb:194:in `block in initialize_http_header'
/Users/danieltenner/.rvm/rubies/ruby-3.3.1/lib/ruby/3.3.0/net/http/header.rb:189:in `each'
/Users/danieltenner/.rvm/rubies/ruby-3.3.1/lib/ruby/3.3.0/net/http/header.rb:189:in `initialize_http_header'
/Users/danieltenner/.rvm/rubies/ruby-3.3.1/lib/ruby/3.3.0/net/http/generic_request.rb:51:in `initialize'
/Users/danieltenner/.rvm/gems/ruby-3.3.1/gems/faraday-net_http-3.1.1/lib/faraday/adapter/net_http.rb:79:in `new'
/Users/danieltenner/.rvm/gems/ruby-3.3.1/gems/faraday-net_http-3.1.1/lib/faraday/adapter/net_http.rb:79:in `create_request'
/Users/danieltenner/.rvm/gems/ruby-3.3.1/gems/faraday-net_http-3.1.1/lib/faraday/adapter/net_http.rb:112:in `block in request_with_wrapped_block'
/Users/danieltenner/.rvm/rubies/ruby-3.3.1/lib/ruby/3.3.0/net/http.rb:1570:in `start'
/Users/danieltenner/.rvm/gems/ruby-3.3.1/gems/faraday-net_http-3.1.1/lib/faraday/adapter/net_http.rb:111:in `request_with_wrapped_block'
/Users/danieltenner/.rvm/gems/ruby-3.3.1/gems/faraday-net_http-3.1.1/lib/faraday/adapter/net_http.rb:101:in `perform_request'
/Users/danieltenner/.rvm/gems/ruby-3.3.1/gems/faraday-net_http-3.1.1/lib/faraday/adapter/net_http.rb:65:in `block in call'
/Users/danieltenner/.rvm/gems/ruby-3.3.1/gems/faraday-2.10.1/lib/faraday/adapter.rb:45:in `connection'
/Users/danieltenner/.rvm/gems/ruby-3.3.1/gems/faraday-net_http-3.1.1/lib/faraday/adapter/net_http.rb:64:in `call'
/Users/danieltenner/.rvm/gems/ruby-3.3.1/gems/faraday-2.10.1/lib/faraday/middleware.rb:56:in `call'
/Users/danieltenner/.rvm/gems/ruby-3.3.1/gems/faraday-2.10.1/lib/faraday/rack_builder.rb:152:in `build_response'
/Users/danieltenner/.rvm/gems/ruby-3.3.1/gems/faraday-2.10.1/lib/faraday/connection.rb:444:in `run_request'
/Users/danieltenner/.rvm/gems/ruby-3.3.1/gems/faraday-2.10.1/lib/faraday/connection.rb:200:in `get'
/Users/danieltenner/.rvm/gems/ruby-3.3.1/gems/xero-ruby-9.2.0/lib/xero-ruby/api_client.rb:319:in `public_send'
/Users/danieltenner/.rvm/gems/ruby-3.3.1/gems/xero-ruby-9.2.0/lib/xero-ruby/api_client.rb:319:in `call_api'
/Users/danieltenner/.rvm/gems/ruby-3.3.1/gems/xero-ruby-9.2.0/lib/xero-ruby/api/accounting_api.rb:6339:in `get_accounts_with_http_info'
/Users/danieltenner/.rvm/gems/ruby-3.3.1/gems/xero-ruby-9.2.0/lib/xero-ruby/api/accounting_api.rb:6275:in `get_accounts'
/Users/danieltenner/dev/[...]/app/apis/xero_api.rb:60:in `get_accounts'

This is happening because the initheader parameter passed to net/http/header.rb's initialize_http_header method is:

{"Content-Type"=>"application/json", "User-Agent"=>"xero-ruby-9.2.0", 
"Accept"=>"application/json", "Xero-tenant-id"=>{"id"=>"[snip]", "authEventId"=>"[snip]", 
"tenantId"=>"[snip]", "tenantType"=>"ORGANISATION", "tenantName"=>"[snip]", 
"createdDateUtc"=>"2024-09-04T15:17:17.9469550", "updatedDateUtc"=>"2024-09-04T15:17:17.9483740"}, "Authorization"=>"Bearer [snip]", 
"accept-encoding"=>"gzip;q=1.0,deflate;q=0.6,identity;q=0.3"}

Notice Xero-tenant-id is a hash.

initialize_http_header then dutifully chokes on it because it expects string parameters:

  def initialize_http_header(initheader) #:nodoc:
    @header = {}
    return unless initheader
    initheader.each do |key, value|
      warn "net/http: duplicated HTTP header: #{key}", uplevel: 3 if key?(key) and $VERBOSE
      if value.nil?
        warn "net/http: nil HTTP header: #{key}", uplevel: 3 if $VERBOSE
      else
        value = value.strip # raise error for invalid byte sequences
        if key.to_s.bytesize > MAX_KEY_LENGTH
          raise ArgumentError, "too long (#{key.bytesize} bytes) header: #{key[0, 30].inspect}..."
        end
        if value.to_s.bytesize > MAX_FIELD_LENGTH
          raise ArgumentError, "header #{key} has too long field value: #{value.bytesize}"
        end
        if value.count("\r\n") > 0
          raise ArgumentError, "header #{key} has field value #{value.inspect}, this cannot include CR/LF"
        end
        @header[key.downcase.to_s] = [value]
      end
    end
  end

Haven't got a solution or workaround yet, but thought i'd put this here in case it helps someone. Will report back once I do have a solution, whatever it is.

PETOSS-568

Thanks for raising an issue, a ticket has been created to track your request

I believe I have figured this out! Documenting here as it may help others.

The README suggests that xero_client.last_connection "returns the xero-tenant-id of the most recently connected Xero org".

But that's not true. It returns a hash. And inside that is ["id"] - which is the thing you need to pass to, e.g., get_accounts.

So this bug is fixed by changing:

@api_client.accounting_api.get_accounts(@api_client.last_connection)

To

@api_client.accounting_api.get_accounts(@api_client.last_connection["id"])

Easy fix but fiendishly hard to debug. Hope this helps someone!

Actually perhaps worth reopening as this is an easy documentation fix.