Set api.version when the version is the only path segment - #2826
Open
ericproulx wants to merge 2 commits into
Open
Set api.version when the version is the only path segment#2826ericproulx wants to merge 2 commits into
ericproulx wants to merge 2 commits into
Conversation
ericproulx
force-pushed
the
fix-path-versioner-root-version
branch
from
July 26, 2026 19:35
604d65c to
cf8d5fe
Compare
Danger ReportNo issues found. |
dblock
reviewed
Jul 27, 2026
ericproulx
force-pushed
the
fix-path-versioner-root-version
branch
3 times, most recently
from
July 27, 2026 05:35
5676a76 to
a875aa3
Compare
ericproulx
force-pushed
the
fix-path-versioner-root-version
branch
from
July 27, 2026 05:38
a875aa3 to
106cd9d
Compare
The path versioner only extracted the version when a second slash
followed it (path_info.index('/', 1)), so the root route of a
path-versioned API (GET /v1, or /v1.json with a format suffix) matched
and ran without env['api.version'] — the `version` helper returned nil
and entity `version:` embeds were omitted.
When the path is a single segment, record the version on an exact match
against the declared versions, trimming a trailing .format suffix
(right-to-left, so dotted versions like v1.2 still match). An unmatched
segment is left for the router to resolve — never a 404 from here.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Split the dotted version and prefix test cases into separate nested contexts to make it explicit that they test different scenarios (different options overrides). This answers the question: why do we need to override options[:versions] or options[:prefix] mid-context? Answer: we don't — each scenario gets its own context with its own let(:options). Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
ericproulx
force-pushed
the
fix-path-versioner-root-version
branch
from
July 27, 2026 05:49
106cd9d to
031124d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Grape::Middleware::Versioner::Pathextracts the version from the first path segment — but only when a second slash follows it:For the root route of a path-versioned API there is no second segment, so the middleware returned early and never set
env['api.version']:GET /v1→200with{"version_seen":null}. The route matches (the version lives in the compiled route pattern), but everything that readsenv['api.version']sees nothing: theversionhelper,Grape::DSL::Entity#entity_representation_for(entities lose theirversion:embed, breaking conditional exposures), andErrorFormatter::Base#present. The same happens with a format suffix (GET /v1.json) and behind a prefix (GET /api/v1).Only the versioner middlewares write
api.version, so nothing downstream recovers it from the route's version capture either. The logic is identical at least as far back as v2.4.0 — long-standing, not a regression.Fix
When the (prefix-stripped) path is a single segment, record the version on an exact match against the declared versions, trimming a trailing
.formatsuffix right-to-left so dotted versions still work:/v1→v1/v1.json→v1(suffix trimmed)/v1.2.jsonwithversion 'v1.2'→v1.2(rightmost dot trimmed first)/awesome→ no version, no 404 — the segment is left for the router to resolveThe single-segment branch deliberately never raises
version_not_found!: with no following segment there is nothing to disambiguate a version from a plain path, so an unknown segment falls through to the router's 404 exactly as before. Multi-segment behavior (/v3/foo→ 404 withX-Cascade: passfor an undeclared version) is unchanged.Tests
versioner/path_spec.rb.sets the API version for the root routeexample added to the shared versioning examples, so the root route is now covered end-to-end for all four strategies (:path— previously failing — plus:param,:header,:accept_version_header).🤖 Generated with Claude Code