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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions