Skip to content

async_http_client_adapter crashes with protocol-http >= 0.59.0 — NoMethodError: undefined method 'split' for nil #1120

Description

@renatovico

WebMockClientWrapper#build_request_signature calls request.headers.to_h (line 103 in async_http_client_adapter.rb). Starting with protocol-http 0.59.0, Headers#to_h invokes Header::Split.parse(value) on each header entry during merging. When a header has a nil value (which is valid in the Protocol::HTTP::Headers collection for internal/connection headers), Split.parse calls value.split(COMMA) on nil and raises:

NoMethodError: undefined method 'split' for nil
  protocol-http-0.59.0/lib/protocol/http/header/split.rb:23:in 'Protocol::HTTP::Header::Split.parse'
  protocol-http-0.59.0/lib/protocol/http/headers.rb:452:in 'Protocol::HTTP::Headers#merge_into'
  protocol-http-0.59.0/lib/protocol/http/headers.rb:473:in 'block in Protocol::HTTP::Headers#to_h'
  webmock-3.26.1/lib/webmock/http_lib_adapters/async_http_client_adapter.rb:103:in 'build_request_signature'

Steps to reproduce:

# Gemfile
gem "faraday"
gem "async-http-faraday"
gem "webmock"

# test
require "async/http/faraday/default"  # sets async-http as Faraday default adapter
require "webmock/rspec"

WebMock.disable_net_connect!

stub_request(:get, "https://example.com/test")
  .to_return(status: 200, body: '{"ok":true}')

# This crashes:
Faraday.get("https://example.com/test")
# => NoMethodError: undefined method 'split' for nil

Environment
webmock 3.26.1
protocol-http >= 0.59.0
async-http >= 0.94.0
async-http-faraday 0.22.1
Ruby 3.2+ / 3.4+

Root Cause
In async_http_client_adapter.rb line 103:

def build_request_signature(request)
  body = request.read
  request.body = ::Protocol::HTTP::Body::Buffered.wrap(body)
  WebMock::RequestSignature.new(
    request.method.downcase.to_sym,
    "#{request.scheme}://#{request.authority}#{request.path}",
    headers: request.headers.to_h,  # <--- crashes here
    body: body
  )
end

request.headers can contain entries with nil values. The protocol-http 0.59.0 Headers#to_h now strictly parses every value through header policy classes, which don't handle nil.

Suggested Fix
Filter out nil-valued headers before conversion:

def build_request_signature(request)
  body = request.read
  request.body = ::Protocol::HTTP::Body::Buffered.wrap(body)

  headers = {}
  request.headers.each do |key, value|
    headers[key] = value unless value.nil?
  end

  WebMock::RequestSignature.new(
    request.method.downcase.to_sym,
    "#{request.scheme}://#{request.authority}#{request.path}",
    headers: headers,
    body: body
  )
end

Workaround

if defined?(Async::HTTP::WebMockClientWrapper)
  module Async
    module HTTP
      class WebMockClientWrapper
        private

        def build_request_signature(request)
          body = request.read
          request.body = ::Protocol::HTTP::Body::Buffered.wrap(body)

          headers = {}
          request.headers.each do |key, value|
            headers[key] = value unless value.nil?
          end

          WebMock::RequestSignature.new(
            request.method.downcase.to_sym,
            "#{request.scheme}://#{request.authority}#{request.path}",
            headers: headers,
            body: body
          )
        end
      end
    end
  end
end

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions