Skip to content

Regexp and Addressable::Template URLs produce non-matching WebMock stubs (and crash path_prefix) #104

Description

@IanVS

Warning: I don't know a ton of ruby, and most of this is generated using Claude. However, I ran it past a backend dev at the office and he agreed with the assessment.

Summary

The README documents three URL forms for WebValve.register:

  1. String + * wildcards
  2. RegexpWebValve.register FakeBank, url: %r{\Ahttp://mybank.com(/.*)?\z}
  3. Addressable::TemplateWebValve.register FakeBank, url: Addressable::Template.new("http://mybank.com{/path*}{?query}")

In practice only (1) works. For Regexp and Addressable::Template, the registered fake's WebMock stub never matches a real request — the fake is silently inert. FakeServiceConfig#path_prefix also returns garbage ("(") for Regexp and raises URI::InvalidURIError for Addressable::Template, so any request that did match would crash inside FakeServiceWrapper#call.

Tested against webvalve 2.2.0 on Ruby 3.4.

Root cause

FakeServiceConfig#service_url calls strip_basic_auth(full_url), which does url.to_s.sub(...). That stringifies the URL before ServiceUrlConverter#regexp ever sees it, so the else url branch (which handles non-String URLs) is unreachable from Manager#webmock_service:

# lib/webvalve/fake_service_config.rb
def service_url
  @service_url ||= strip_basic_auth(full_url)        # url.to_s under the hood
end

def path_prefix
  @path_prefix ||= URI.parse(service_url).path        # parses the stringified URL
end

# lib/webvalve/manager.rb
def webmock_service(config)
  WebMock.stub_request(:any, url_to_regexp(config.service_url)).to_rack(...)
  #                                            ^ String, even when input was Regexp/Template
end

def url_to_regexp(url)
  ServiceUrlConverter.new(url: url).regexp
end

# lib/webvalve/service_url_converter.rb
def regexp
  if url.is_a?(String)
    regexp_string = Regexp.escape(url)
    %r(\A#{regexp_string.gsub('\*', WILDCARD_SUBSTITUTION)}#{URL_SUFFIX_PATTERN})
  else
    url   # never reached — url is always a String here
  end
end

For a Regexp, to_s produces "(?-mix:...)", then Regexp.escape is applied to those literal characters — the resulting WebMock stub matches only URLs containing the literal string (?-mix:\Ahttp://...), i.e. nothing.

For an Addressable::Template, to_s returns the inspect string ("#<Addressable::Template:0x...>"), and URI.parse raises on it.

Why the existing test suite doesn't catch this

spec/webvalve/manager_spec.rb exercises Regexp URLs via expect(WebMock).to have_received(:stub_request).with(:any, %r{...}), which never invokes the service_url → stringification path. There's no integration spec that actually services a request through FakeServiceWrapper#call with a Regexp or Addressable::Template URL — so neither the matching bug nor the path_prefix crash fires under the test suite.

Reproduction

Save and run with ruby webvalve_repro.rb:

#!/usr/bin/env ruby
require "bundler/inline"

gemfile do
  source "https://rubygems.org"
  gem "webvalve", "2.2.0"
  gem "addressable"
end

require "webvalve"
require "addressable/template"

class FakeStub < WebValve::FakeService; end

def report(label, url, expected_match_url)
  config = WebValve::FakeServiceConfig.new(service_class_name: "FakeStub", url: url)

  service_url = config.service_url rescue "RAISED #{$!.class}: #{$!.message[0..80]}"
  path_prefix = config.path_prefix rescue "RAISED #{$!.class}: #{$!.message[0..80]}"

  webmock_pattern =
    begin
      WebValve::ServiceUrlConverter.new(url: service_url).regexp
    rescue => e
      "RAISED #{e.class}: #{e.message[0..80]}"
    end

  matches =
    if webmock_pattern.is_a?(Regexp)
      !!(expected_match_url =~ webmock_pattern)
    else
      "n/a"
    end

  puts "=" * 78
  puts label
  puts "  registered url:         #{url.inspect[0..120]}"
  puts "  url class:              #{config.full_url.class}"
  puts "  service_url:            #{service_url.inspect[0..120]}"
  puts "  path_prefix:            #{path_prefix.inspect[0..120]}"
  puts "  WebMock stub pattern:   #{webmock_pattern.inspect[0..120]}"
  puts "  matches #{expected_match_url.inspect}? #{matches}"
  puts
end

report(
  "1. STRING URL with wildcard (baseline)",
  "https://*.example.com",
  "https://api.example.com/v1/things"
)

report(
  "2. REGEXP URL",
  %r{\Ahttps://example\.com(/.*)?\z},
  "https://example.com/v1/things"
)

report(
  "3. ADDRESSABLE::TEMPLATE URL",
  Addressable::Template.new("https://example.com{/path*}"),
  "https://example.com/v1/things"
)

Output

==============================================================================
1. STRING URL with wildcard (baseline)
  registered url:         "https://*.example.com"
  url class:              String
  service_url:            "https://*.example.com"
  path_prefix:            ""
  WebMock stub pattern:   /\Ahttps:\/\/[^\.:\/\?\#@&=]*\.example\.com(([\.:\/\?\#@&=]|(?<=[\.:\/\?\#@&=])).*)?\z/
  matches "https://api.example.com/v1/things"? true

==============================================================================
2. REGEXP URL
  registered url:         /\Ahttps:\/\/example\.com(\/.*)?\z/
  url class:              Regexp
  service_url:            "(?-mix:\\Ahttps:\\/\\/example\\.com(\\/.*)?\\z)"
  path_prefix:            "("
  WebMock stub pattern:   /\A\(\?\-mix:\\Ahttps:\\\/\\\/example\\\.com\(\\\/\.[^\.:\/\?\#@&=]*\)\?\\z\)(([\.:\/\?\#@&=]|(?<=[\.:\/\?\#@&=])).*)?\z/
  registered url:         #<Addressable::Template:0x... PATTERN:https://example.com{/path*}>
  url class:              Addressable::Template
  service_url:            "#<Addressable::Template:0x...>"
  path_prefix:            "RAISED URI::InvalidURIError: bad URI (is not URI?): \"#<Addressable::Template:0x...>\""
  WebMock stub pattern:   /\A\#<Addressable::Template:0x...>(([\.:\/\?\#@&=]|(?<=[\.:\/\?\#@&=])).*)?\z/
  matches "https://example.com/v1/things"? false

Expected behavior

The README's documented examples for Regexp and Addressable::Template URLs should produce WebMock stubs that match the URLs they describe, and path_prefix should not crash at request time.

Suggested fix

Two minimal changes in lib/webvalve/fake_service_config.rb:

def service_url
  @service_url ||= begin
    raise missing_url_message if full_url.blank?
    full_url.is_a?(String) ? strip_basic_auth(full_url) : full_url
  end
end

def path_prefix
  @path_prefix ||= full_url.is_a?(String) ? URI.parse(service_url).path : ""
end

This preserves Regexp and Addressable::Template objects all the way through to ServiceUrlConverter#regexp, which already takes the correct else url branch for them, and short-circuits path_prefix for non-String URLs (no path to strip).

A complementary integration spec — registering a fake with a Regexp URL, making a real request via Faraday/Net::HTTP, and asserting the fake's response is returned — would prevent regressions.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions