Repository navigation
Raise ArgumentError on a version that is not a String, Symbol or Integer - #3060
Merged
Merged
Conversation
ericproulx
force-pushed
the
fix/version-positional-options
branch
from
November 6, 2026 23:09
6490de5 to
fc2c719
Compare
Danger ReportNo issues found. |
ericproulx
force-pushed
the
fix/version-positional-options
branch
from
November 6, 2026 23:16
fc2c719 to
7e76ca7
Compare
Member
|
I’m an AI posting on behalf of @dblock. Should we define which types are valid version names rather than only rejecting Hash? Separately, the operations are currently out of order: validation happens before flattening, so a Hash nested in an array bypasses the check, e.g. |
ericproulx
force-pushed
the
fix/version-positional-options
branch
from
November 7, 2026 15:49
7e76ca7 to
e07080d
Compare
ericproulx
force-pushed
the
fix/version-positional-options
branch
2 times, most recently
from
November 8, 2026 20:15
79ee4ab to
83a5a29
Compare
Since #2618 replaced extract_options! with keyword arguments (3.0.0), a trailing braced options Hash such as version 'v1', { using: :header, vendor: 'acme' } lands in the splat. Grape registered a version literally named `{using: :header, vendor: "acme"}` and kept the API on the default :path strategy, with no warning. Raise ArgumentError instead, as auth, http_basic and desc have done since 4.0.0. Redispatching the Hash as keywords, as #2853 did for requires, optional and use, would quietly move an API that has been serving /v1/... since 3.0 onto header versioning in a minor release. Raising stops it at boot and points at the line. Every other version was stringified too, so check them all against an explicit set: a non-empty String, Symbol or Integer. A Float drops its trailing zeros (1.10 registered "1.1"), and nil, an empty name or any other object registered a version no request could reach. The arguments are flattened before the check, so a Hash or nil inside an Array of versions raises as well, as does an empty Array. UPGRADING notes the break and how to keep path versioning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dblock
approved these changes
Nov 8, 2026
ericproulx
force-pushed
the
fix/version-positional-options
branch
from
November 9, 2026 09:09
83a5a29 to
d610d5e
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
Since #2618 (3.0.0) replaced
extract_options!with keyword arguments, a trailing braced options Hash passed toversionlands in the splat. Ruby 3 only turns a brace-less trailing Hash into keywords:The Hash became a version name, and the API silently stayed on the default
:pathstrategy, with no warning. I found it while triaging #1801: its spec passesversion_optionsthis way, and on Ruby 3 every non-path context failed for this reason alone.The Hash wasn't the only case. Each version was passed through
to_s, whatever its type:version …1"1"/1/foo→ 200 (works)1.10"1.1"/1.10/foo→ 404nil,'',:''""true"true"/true/foo→ 200Object.new"#<Object:0x…>"[]['v1', { using: :header }]"v1","{using: :header}"flattenFix
versionflattens its arguments first and then checks every version. A Hash keeps its own message, since the options are what the caller meant:Anything else that isn't a non-empty String, Symbol or Integer raises:
An empty list raises
`version` needs at least one version.Integer is accepted because it round-trips exactly through the path, header and param strategies (
version 2serves/2/...). A code search found no numeric versions in about 200 Grape APIs, but they work today, and rejecting them would break working code for nothing. Float is rejected because it doesn't round-trip:version '1.10'is the fix.This is how
auth,http_basicanddeschave handled a positional Hash since 4.0.0. I first tried redispatching the Hash as keyword arguments behind a deprecation warning, as #2853 did forrequires,optionalanduse. That would quietly move an API that has served/v1/...since 3.0 onto header versioning in a minor release. Raising stops it at boot and points at the line instead.The README now names the accepted types. UPGRADING (4.2.0) describes the break and how to keep path versioning when clients have been using
/v1/...:version 'v1', orusing: :path.Tests
New examples under
Grape::API .version:/2/hello.nil,true, a Float, an empty String, an empty Symbol and an arbitrary object each raise, both as an argument and nested in an Array.version [[]]raises.Each rule was mutation-checked by reverting it: flatten-then-validate (9 failures), Float rejected (2), empty name rejected (4), empty list rejected (1), Hash message (4).
bundle exec rake: 379 files with no offenses, 3203 examples, 0 failures.🤖 Generated with Claude Code