From 80dbf7129e4155099c742f4d99ffa80984797de5 Mon Sep 17 00:00:00 2001 From: Owens Ehimen Date: Mon, 28 Sep 2026 12:40:39 -0400 Subject: [PATCH 1/4] Fix duplicate-slash bug in request path construction Request#request applied the connection's prefix twice: once baked into the Faraday base URL via Configuration#endpoint, and again via string concatenation. With the default '/' prefix and a caller path that already starts with '/' (e.g. identity-api-client's '/api/member/details'), this produced a leading '//', which Faraday treats as a protocol-relative URL and mangles, yielding malformed URLs like https://host////api/path. Some upstream APIs reject these at the edge with a bare 403. Join prefix and path so they always meet with exactly one slash, regardless of whether either side already has one. This also fixes 5 pre-existing failures in the request spec. --- lib/vertebrae/request.rb | 6 +++++- spec/request_spec.rb | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/lib/vertebrae/request.rb b/lib/vertebrae/request.rb index 40ce49e..adda20c 100644 --- a/lib/vertebrae/request.rb +++ b/lib/vertebrae/request.rb @@ -32,7 +32,11 @@ def request(method, path, params, options) # :nodoc: end connection.options = default_options.merge(initialisation_options.merge(options)) - path = connection.configuration.prefix + '/' + path + # Join without assuming either side has (or lacks) its own leading/trailing slash, so a + # default '/' prefix combined with an already-absolute path never yields a leading '//'. + # Faraday treats a path starting with '//' as a protocol-relative URL and mangles it, + # producing malformed URLs like `https://host////api/path`. + path = File.join(connection.configuration.prefix.to_s, path.to_s.delete_prefix('/')) ::Vertebrae::Base.logger.debug "EXECUTED: #{method} - #{path} with #{params} and #{options}" diff --git a/spec/request_spec.rb b/spec/request_spec.rb index ed5c93d..ad1a15a 100644 --- a/spec/request_spec.rb +++ b/spec/request_spec.rb @@ -49,6 +49,22 @@ end end + context 'with a custom prefix and a path without a leading slash' do + let(:options) { {prefix: '/rest/v1'} } + it 'should join the prefix and path with a single slash' do + stub_request(:get, 'https://test.com/rest/v1/user/') + vb.request(:get, 'user/', {}, options) + end + end + + context 'with the default prefix and an already-absolute path' do + let(:options) { {} } + it 'should not produce a malformed URL with duplicate slashes' do + stub_request(:post, 'https://test.com/api/member/details') + vb.request(:post, '/api/member/details', {}, options) + end + end + context 'with a different host' do let(:options) { {host: 'test2.com'} } it 'should make the request to the default host' do From 96283319a31f113ed62dc2c492d275def2d262c2 Mon Sep 17 00:00:00 2001 From: Owens Ehimen Date: Mon, 28 Sep 2026 12:41:13 -0400 Subject: [PATCH 2/4] Modernize Ruby/Faraday support and gemspec declarations - Bump .ruby-version to 4.0.1; CI now runs the matrix 3.3/3.4/4.0 on actions/checkout@v4 and ruby/setup-ruby@v1 - Remove legacy .ruby-gemset (RVM artifact) - Simplify Gemfile to source + gemspec; move all dependencies into vertebrae.gemspec with explicit version constraints, drop the unused juwelier dev dependency, set required_ruby_version and rubygems_mfa_required - Simplify Rakefile to bundler/gem_tasks + rspec rake task - Add CHANGELOG.md and a Requirements section to the README - Bump version to 2.0.0 (breaking: Ruby floor raised, faraday now pinned ~> 2.0) --- .github/workflows/ci.yml | 17 +++++------ .ruby-gemset | 1 - .ruby-version | 2 +- CHANGELOG.md | 17 +++++++++++ Gemfile | 18 +++--------- README.md | 5 ++++ Rakefile | 37 ++---------------------- lib/vertebrae/version.rb | 2 +- vertebrae.gemspec | 62 ++++++++++++++++++++-------------------- 9 files changed, 71 insertions(+), 90 deletions(-) delete mode 100644 .ruby-gemset create mode 100644 CHANGELOG.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 97c0804..01b7919 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,21 +4,22 @@ on: [push] jobs: test: runs-on: ubuntu-latest + strategy: + matrix: + ruby-version: ['3.3', '3.4', '4.0'] steps: - - uses: actions/checkout@v2 - - uses: ruby/setup-ruby@477b21f02be01bcb8030d50f37cfec92bfa615b6 + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 with: - ruby-version: 2.7 + ruby-version: ${{ matrix.ruby-version }} bundler-cache: true - - run: bundle install - run: bundle exec rspec rubocop: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: ruby/setup-ruby@477b21f02be01bcb8030d50f37cfec92bfa615b6 + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 with: - ruby-version: 2.7 + ruby-version: '4.0' bundler-cache: true - - run: bundle install - run: bundle exec rubocop diff --git a/.ruby-gemset b/.ruby-gemset deleted file mode 100644 index a145603..0000000 --- a/.ruby-gemset +++ /dev/null @@ -1 +0,0 @@ -vertebrae \ No newline at end of file diff --git a/.ruby-version b/.ruby-version index be94e6f..1454f6e 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.2.2 +4.0.1 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7666415 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,17 @@ +# Changelog + +## [2.0.0] - 2026-09-28 + +### Fixed +- `Vertebrae::Request#request` applied the connection's `prefix` twice when building a request path + (once baked into `Configuration#endpoint` as the Faraday connection's base URL, again via string + concatenation in `request`). Combined with the default `/` prefix and a caller-supplied path that + already started with `/`, this produced a path starting with `//`, which Faraday treats as a + protocol-relative URL and mangles, yielding malformed URLs with multiple consecutive slashes (e.g. + `https://host////api/path`). Some upstream APIs reject these at the edge with a bare 403. Paths are + now joined so the prefix and path always meet with exactly one slash, regardless of whether either + side has its own leading or trailing slash. + +### Breaking Changes +- Dropped support for Ruby versions older than 3.3. Officially supported versions are Ruby 3.3, 3.4, and 4.0. +- Pinned `faraday` to `~> 2.0` (previously unbounded above `2.0`). diff --git a/Gemfile b/Gemfile index 340a1e9..44e12b7 100644 --- a/Gemfile +++ b/Gemfile @@ -1,16 +1,6 @@ -source "http://rubygems.org" +# frozen_string_literal: true -gem 'activesupport', '>= 5.1.4' -gem 'faraday', '> 2.0' -gem 'faraday-mashify' -gem 'faraday-multipart' +source 'https://rubygems.org' -# Add dependencies to develop your gem here. -# Include everything needed to run rake, tests, features, etc. -group :development do - gem "rspec" - gem "bundler" - gem "webmock" - gem "rspec-its" - gem "rubocop" -end +# Specify your gem's dependencies in vertebrae.gemspec +gemspec diff --git a/README.md b/README.md index 9495850..559ef4a 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,11 @@ Some basic infrastructure for writing beautiful API clients. See tijuana\_client [![CI Status](https://github.com/controlshift/vertebrae/actions/workflows/ci.yml/badge.svg)](https://github.com/controlshift/vertebrae/actions/workflows/ci.yml) +## Requirements + +- Ruby 3.3, 3.4, or 4.0 +- Faraday ~> 2.0 + ## Development After checking out the repo, run `bundle install` to install dependencies. Then, run `rake spec` to run the tests. diff --git a/Rakefile b/Rakefile index 39261f9..82bb534 100644 --- a/Rakefile +++ b/Rakefile @@ -1,39 +1,8 @@ -# encoding: utf-8 +# frozen_string_literal: true -require 'rubygems' -require 'bundler' require 'bundler/gem_tasks' - - -begin - Bundler.setup(:default, :development) -rescue Bundler::BundlerError => e - $stderr.puts e.message - $stderr.puts "Run `bundle install` to install missing gems" - exit e.status_code -end -require 'rake' - -require 'rspec/core' require 'rspec/core/rake_task' -RSpec::Core::RakeTask.new(:spec) do |spec| - spec.pattern = FileList['spec/**/*_spec.rb'] -end - -desc "Code coverage detail" -task :simplecov do - ENV['COVERAGE'] = "true" - Rake::Task['spec'].execute -end - -task :default => :spec -require 'rdoc/task' -Rake::RDocTask.new do |rdoc| - version = File.exist?('VERSION') ? File.read('VERSION') : "" +RSpec::Core::RakeTask.new(:spec) - rdoc.rdoc_dir = 'rdoc' - rdoc.title = "vertebrae #{version}" - rdoc.rdoc_files.include('README*') - rdoc.rdoc_files.include('lib/**/*.rb') -end +task default: :spec diff --git a/lib/vertebrae/version.rb b/lib/vertebrae/version.rb index d931217..72c2a50 100644 --- a/lib/vertebrae/version.rb +++ b/lib/vertebrae/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Vertebrae - VERSION = '1.0.5' + VERSION = '2.0.0' end diff --git a/vertebrae.gemspec b/vertebrae.gemspec index f769c79..0d93f03 100644 --- a/vertebrae.gemspec +++ b/vertebrae.gemspec @@ -4,45 +4,45 @@ lib = File.expand_path('lib', __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'vertebrae/version' -Gem::Specification.new do |s| - s.name = "vertebrae".freeze - s.version = Vertebrae::VERSION - - s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= - s.require_paths = ["lib".freeze] - s.authors = ["Nathan Woodhull".freeze] - s.date = "2021-07-23" - s.description = "A set of low level infrastructure and reusable code for building API clients".freeze - s.email = "nathan@controlshiftlabs.com".freeze - s.extra_rdoc_files = [ - "LICENSE.txt", - "README.md" +Gem::Specification.new do |spec| + spec.name = 'vertebrae' + spec.version = Vertebrae::VERSION + spec.authors = ['Nathan Woodhull', 'Owens Ehimen'] + spec.email = ['talk@controlshiftlabs.com'] + + spec.summary = 'API Client Infrastructure' + spec.description = 'A set of low level infrastructure and reusable code for building API clients' + spec.homepage = 'https://github.com/controlshift/vertebrae' + spec.license = 'MIT' + + spec.extra_rdoc_files = [ + 'LICENSE.txt', + 'README.md' ] # Specify which files should be added to the gem when it is released. # The `git ls-files -z` loads the files in the RubyGem that have been added into git. - s.files = Dir.chdir(File.expand_path(__dir__)) do + spec.files = Dir.chdir(File.expand_path(__dir__)) do `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } end + spec.require_paths = ['lib'] - s.homepage = "http://github.com/controlshift/vertebrae".freeze - s.licenses = ["MIT".freeze] - s.summary = "API Client Infrastructure".freeze + spec.required_ruby_version = ['>= 3.3', '< 5.0'] - if s.respond_to? :specification_version then - s.specification_version = 4 - end + # Runtime dependencies + spec.add_runtime_dependency 'activesupport', '>= 6.1' + spec.add_runtime_dependency 'faraday', '~> 2.0' + spec.add_runtime_dependency 'faraday-mashify', '~> 1.0' + spec.add_runtime_dependency 'faraday-multipart', '~> 1.0' + + # Development dependencies + spec.add_development_dependency 'bundler', '>= 2.0', '< 5.0' + spec.add_development_dependency 'rake', '~> 13.0' + spec.add_development_dependency 'rspec', '~> 3.0' + spec.add_development_dependency 'rspec-its', '~> 2.0' + spec.add_development_dependency 'rubocop', '~> 1.0' + spec.add_development_dependency 'webmock', '~> 3.0' - s.add_runtime_dependency(%q.freeze, [">= 5.1.4"]) - s.add_runtime_dependency(%q.freeze, ["~> 2"]) - s.add_runtime_dependency(%q.freeze, [">= 0"]) - s.add_runtime_dependency(%q.freeze, [">= 0"]) - - s.add_development_dependency(%q.freeze, [">= 0"]) - s.add_development_dependency(%q.freeze, [">= 0"]) - s.add_development_dependency(%q.freeze, [">= 0"]) - s.add_development_dependency(%q.freeze, [">= 0"]) - s.add_development_dependency(%q.freeze, [">= 0"]) - s.add_development_dependency(%q.freeze, [">= 0"]) + spec.metadata['rubygems_mfa_required'] = 'true' end From a89cee9c611ab49ee96ac4141c69f2ae74744d68 Mon Sep 17 00:00:00 2001 From: Owens Ehimen Date: Mon, 28 Sep 2026 12:48:36 -0400 Subject: [PATCH 3/4] Trim comment and CHANGELOG wording --- CHANGELOG.md | 12 ++++-------- lib/vertebrae/request.rb | 5 +---- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7666415..556fafc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,14 +3,10 @@ ## [2.0.0] - 2026-09-28 ### Fixed -- `Vertebrae::Request#request` applied the connection's `prefix` twice when building a request path - (once baked into `Configuration#endpoint` as the Faraday connection's base URL, again via string - concatenation in `request`). Combined with the default `/` prefix and a caller-supplied path that - already started with `/`, this produced a path starting with `//`, which Faraday treats as a - protocol-relative URL and mangles, yielding malformed URLs with multiple consecutive slashes (e.g. - `https://host////api/path`). Some upstream APIs reject these at the edge with a bare 403. Paths are - now joined so the prefix and path always meet with exactly one slash, regardless of whether either - side has its own leading or trailing slash. +- The default `/` prefix was applied twice when building request paths, so a caller path that + already started with `/` produced a leading `//`. Faraday treats that as a protocol-relative URL + and mangles it, yielding malformed URLs like `https://host////api/path` that some upstream APIs + reject at the edge with a bare 403. ### Breaking Changes - Dropped support for Ruby versions older than 3.3. Officially supported versions are Ruby 3.3, 3.4, and 4.0. diff --git a/lib/vertebrae/request.rb b/lib/vertebrae/request.rb index adda20c..d580fe7 100644 --- a/lib/vertebrae/request.rb +++ b/lib/vertebrae/request.rb @@ -32,10 +32,7 @@ def request(method, path, params, options) # :nodoc: end connection.options = default_options.merge(initialisation_options.merge(options)) - # Join without assuming either side has (or lacks) its own leading/trailing slash, so a - # default '/' prefix combined with an already-absolute path never yields a leading '//'. - # Faraday treats a path starting with '//' as a protocol-relative URL and mangles it, - # producing malformed URLs like `https://host////api/path`. + # Avoid a leading '//' here — Faraday treats it as a protocol-relative URL and mangles the path. path = File.join(connection.configuration.prefix.to_s, path.to_s.delete_prefix('/')) ::Vertebrae::Base.logger.debug "EXECUTED: #{method} - #{path} with #{params} and #{options}" From aa22506c1e31c0b227caccb8389b0dbcb70345e6 Mon Sep 17 00:00:00 2001 From: Owens Ehimen Date: Tue, 29 Sep 2026 13:38:29 -0400 Subject: [PATCH 4/4] Drop Ruby 3.3 support per review feedback Officially supported versions are now 3.4 and 4.0. --- .github/workflows/ci.yml | 2 +- CHANGELOG.md | 2 +- README.md | 2 +- vertebrae.gemspec | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 01b7919..3fcb839 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - ruby-version: ['3.3', '3.4', '4.0'] + ruby-version: ['3.4', '4.0'] steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 556fafc..53fac5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,5 +9,5 @@ reject at the edge with a bare 403. ### Breaking Changes -- Dropped support for Ruby versions older than 3.3. Officially supported versions are Ruby 3.3, 3.4, and 4.0. +- Dropped support for Ruby versions older than 3.4. Officially supported versions are Ruby 3.4 and 4.0. - Pinned `faraday` to `~> 2.0` (previously unbounded above `2.0`). diff --git a/README.md b/README.md index 559ef4a..cc6453d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Some basic infrastructure for writing beautiful API clients. See tijuana\_client ## Requirements -- Ruby 3.3, 3.4, or 4.0 +- Ruby 3.4 or 4.0 - Faraday ~> 2.0 ## Development diff --git a/vertebrae.gemspec b/vertebrae.gemspec index 0d93f03..af69c23 100644 --- a/vertebrae.gemspec +++ b/vertebrae.gemspec @@ -27,7 +27,7 @@ Gem::Specification.new do |spec| end spec.require_paths = ['lib'] - spec.required_ruby_version = ['>= 3.3', '< 5.0'] + spec.required_ruby_version = ['>= 3.4', '< 5.0'] # Runtime dependencies spec.add_runtime_dependency 'activesupport', '>= 6.1'