lostisland/faraday · error · TypeError

expected #{value_type.name} (got #{context[subkey].class.nam

Error message

expected #{value_type.name} (got #{context[subkey].class.name}) for param `#{subkey}'

What it means

Raised while NestedParamsEncoder#decode parses an incoming query string into a Hash. When a later key nests through a subkey that already holds a scalar (or an Array where a Hash is required), new_context refuses to overwrite the mismatched type: it wants Hash for a[b]-style keys and Array for a[]-style keys, and anything else raises TypeError naming the offending subkey. In short, the query string assigns the same key as both a leaf value and a container.

Source

Thrown at lib/faraday/encoders/nested_params_encoder.rb:134

        context = prepare_context(context, subkey, is_array, last_subkey)
        add_to_context(is_array, context, value, subkey) if last_subkey
      end
    end

    def prepare_context(context, subkey, is_array, last_subkey)
      if !last_subkey || is_array
        context = new_context(subkey, is_array, context)
      end
      if context.is_a?(Array) && !is_array
        context = match_context(context, subkey)
      end
      context
    end

    def new_context(subkey, is_array, context)
      value_type = is_array ? Array : Hash
      if context[subkey] && !context[subkey].is_a?(value_type)
        raise TypeError, "expected #{value_type.name} " \
                         "(got #{context[subkey].class.name}) for param `#{subkey}'"
      end

      context[subkey] ||= value_type.new
    end

    def match_context(context, subkey)
      context << {} if !context.last.is_a?(Hash) || context.last.key?(subkey)
      context.last
    end

    def add_to_context(is_array, context, value, subkey)
      is_array ? context << value : context[subkey] = value
    end

    def validate_params_depth!(depth)
      return unless @param_depth_limit && depth > @param_depth_limit

View on GitHub (pinned to b25b1b26cc)

Solutions

  1. Treat the input as invalid at your trust boundary: rescue TypeError around the decode call and reject with 400 / skip the params, since the string is ambiguous by construction.
  2. If you control the producer, stop emitting mixed forms for one key — never send both a=1 and a[b]=2 for the same prefix.
  3. Decode untrusted query strings with Faraday::FlatParamsEncoder instead (params_encoder option or Faraday::Utils.parse_query with the flat decoder) — flat decoding never builds containers, so the conflict cannot occur.
  4. Sanitize before parsing: reject query strings where a key appears both with and without bracket suffixes.

Example fix

# before
params = Faraday::Utils.parse_query('a=1&a[b]=2')
# => TypeError: expected Hash (got String) for param `a'

# after
begin
  params = Faraday::Utils.parse_query(raw_query)
rescue TypeError
  return [400, {}, ['invalid query string']]
end
# or decode untrusted input flat, which cannot conflict:
flat = Faraday::FlatParamsEncoder.decode(raw_query) # {"a"=>"1", "a[b]"=>"2"}
Defensive patterns

Strategy: try-catch

Validate before calling

keys  = raw_query.split('&').map { |kv| kv.split('=', 2).first.to_s }
flat  = keys.reject { |k| k.include?('[') }
prefs = keys.select { |k| k.include?('[') }.map { |k| k.split('[', 2).first }
raise 'ambiguous query string' if (flat.uniq & prefs.uniq).any?
params = Faraday::NestedParamsEncoder.decode(raw_query)

Type guard

def safely_nestable?(raw_query)
  keys  = raw_query.split('&').map { |kv| kv.split('=', 2).first.to_s }
  flat  = keys.reject { |k| k.include?('[') }
  prefs = keys.select { |k| k.include?('[') }.map { |k| k.split('[', 2).first }
  (flat.uniq & prefs.uniq).empty?
end

Try / catch

begin
  params = Faraday::NestedParamsEncoder.decode(raw_query)
rescue TypeError => e
  raise unless e.message.start_with?('expected')
  params = Faraday::FlatParamsEncoder.decode(raw_query) # degrade to flat keys like "a[b]"
end

Prevention

When it happens

Trigger: Decoding 'a=1&a[b]=2' (a is already the String '1', then a[b] needs a Hash); decoding 'a=1&a[]=2' (a is a String where an Array is required); reverse order 'a[b]=1&a[c][d]=2' is fine, but 'a[]=1&a[b]=2' mixes Array and Hash for the same key. Any code path that calls Faraday::Utils.parse_query or the nested decoder on request URLs or response data with hostile input.

Common situations: A server or middleware parsing untrusted URLs — security scanners routinely send str=abc&str[x]=y probes; API clients that flatten and re-append params, accidentally emitting both flat and nested forms of one key; log replay or webhook payloads whose query strings were assembled by string concatenation; mixing Faraday's nested encoding with a partner system's flat encoding for the same key.

Related errors


AI-assisted analysis of lostisland/faraday@b25b1b26cc (2026-08-21). Data as JSON: /api/errors/d859198a00c82161. Report an issue: GitHub.