WebMock adapter: body.present? returns false even when body has content after v1.7.1

After upgrading from httpx 1.7.0 to 1.7.1, the ResponseBodyMethods override in the WebMock adapter causes body.present? to return false even when the response body contains data. This breaks code that checks for body presence before parsing.

Environment

  • httpx version: 1.7.1
  • Ruby version: 3.4.8
  • WebMock: Enabled (using httpx's built-in adapter)
  • Test framework: Minitest with VCR

Steps to Reproduce

require 'webmock'
require 'httpx'
require 'httpx/adapters/webmock'

WebMock.enable!
WebMock.disable_net_connect!

# Stub a request with JSON error response
WebMock.stub_request(:post, "https://example.com/api")
  .to_return_json(
    status: 429,
    body: { error: { message: "Too many requests." } }
  )

# Make the request
response = HTTPX.post("https://example.com/api")

# Check body state
puts "response.status: #{response.status}"           # => 429
puts "response.body.class: #{response.body.class}"   # => HTTPX::Response::Body(plugin)/WebMock::HttpLibAdapters::Plugin
puts "response.body.present?: #{response.body.present?}"  # => false ❌ (UNEXPECTED)
puts "response.body.empty?: #{response.body.empty?}"      # => true ❌ (UNEXPECTED)
puts "response.body.to_s: '#{response.body.to_s}'"        # => '{"error":{"message":"Too many requests."}}' ✓ (CORRECT)
puts "response.body.to_s.empty?: #{response.body.to_s.empty?}" # => false ✓ (CORRECT)

Expected Behavior

When a mocked response has body content, body.present? should return true.

Actual Behavior

body.present? returns false even though body.to_s returns the correct content.

Root Cause

In httpx 1.7.1, the @length tracking was moved from write() into decode_chunk():

Before (v1.7.0)

  def write(chunk)
    chunk = decode_chunk(chunk)
    size = chunk.bytesize
    @length += size              # ← @length updated in write()
    transition(:open)
    @buffer.write(chunk)
    ...
  end

  def decode_chunk(chunk)
    @inflaters.reverse_each { |inflater| chunk = inflater.call(chunk) }
    chunk
  end

After (v1.7.1) ref

  def write(chunk)
    chunk = decode_chunk(chunk)  # ← @length now updated inside here
    transition(:open)
    @buffer.write(chunk)
    ...
  end

  def decode_chunk(chunk)
    @inflaters.reverse_each { |inflater| chunk = inflater.call(chunk) }
    @length += chunk.bytesize    # ← MOVED HERE
    chunk
  end

The Problem

The WebMock adapter overrides decode_chunk to skip inflation for mocked responses ref:

 module ResponseBodyMethods
    def decode_chunk(chunk)
      return chunk if @response.mocked?  # ← Returns early!
      super                              # ← Never reaches parent's @length increment
    end
  end

The Chain Reaction

  1. WebMock's decode_chunk returns early for mocked responses
  2. Parent's @length += chunk.bytesize never executes
  3. @length stays at 0
  4. empty? returns true (checks @length.zero?)
  5. present? returns false (calls !blank? → empty?)
  6. Code that checks body.present? fails to parse the body

Meanwhile: The actual content is still written to @buffer (line 139 of webmock.rb), so body.to_s works fine.

Workaround

As a workaround, I do body.to_s.present? instead of body.present?

Additional Context

The issue specifically affects error responses (4xx, 5xx) in tests that use WebMock, where the response body contains error details that need to be parsed. The body content is present and accessible via to_s, but standard presence checks fail.

This creates an inconsistency where the same code behaves differently in tests (with WebMock) versus production (real HTTP responses).

Assignee Loading
Time tracking Loading