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
- 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.
- 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.
- 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.
- 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
- Never decode untrusted query strings with the nested decoder without a rescue; treat TypeError as a 400-level input rejection.
- Keep one param style per key when building query strings by hand — never emit a=1 and a[b]=2 together.
- Use FlatParamsEncoder for decoding external input; reserve nested decoding for strings you encoded yourself.
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
- Can't convert #{params.class} into Hash.
- Can't convert #{params.class} into Hash.
- exceeded nested parameter depth limit of #{@param_depth_limi
- Expected :read, :write, :open. Got #{type.inspect} :(
- no stubbed request for #{env[:method]} #{env[:url]} #{env[:b
AI-assisted analysis of lostisland/faraday@b25b1b26cc (2026-08-21).
Data as JSON: /api/errors/d859198a00c82161.
Report an issue: GitHub.