Grape::API::Instance.to_s returns "" instead of its own class name. name is still correct, so the two disagree — which breaks anything that renders a class by its string representation.
Regression introduced in 3.0.0, still present in 3.3.4 and on master.
Repro
require "bundler/inline"
gemfile(true, quiet: true) do
source "https://rubygems.org"
gem "grape", ENV.fetch("GRAPE_VERSION", "3.3.4")
end
require "grape"
class MyAPI < Grape::API
get("/ping") { "pong" }
end
puts "Grape::API::Instance.name #{Grape::API::Instance.name.inspect}"
puts "Grape::API::Instance.to_s #{Grape::API::Instance.to_s.inspect}"
puts "MyAPI.base_instance.to_s #{MyAPI.base_instance.to_s.inspect}"
puts "interpolated: 'class #{Grape::API::Instance}'"
grape 3.3.4 (ruby 3.4.5):
Grape::API::Instance.name "Grape::API::Instance"
Grape::API::Instance.to_s ""
MyAPI.base_instance.to_s "MyAPI"
interpolated: 'class '
grape 2.3.0, same script:
Grape::API::Instance.name "Grape::API::Instance"
Grape::API::Instance.to_s "Grape::API::Instance"
MyAPI.base_instance.to_s "MyAPI"
interpolated: 'class Grape::API::Instance'
Cause
lib/grape/api/instance.rb delegates to_s to @base:
def_delegators :@base, :to_s
@base is only assigned through base=, which runs when an API subclass builds its mount instance. On the bare Grape::API::Instance class it is never set, so the delegator evaluates nil.to_s and yields "".
Before 3.0.0 the same delegation existed as an explicit method with a nil guard:
def to_s
base&.to_s || super
end
It was replaced in 3e46a25 ("Delegate to_s to base in instance"). The delegation itself is fine and clearly intended — note in the output above that MyAPI.base_instance.to_s returns "MyAPI" under both versions. The old form already produced that result; what the rewrite changed is only the nil case, where || super used to fall back to Module#to_s.
Impact
Any tooling that enumerates loaded classes and renders them by string representation gets an empty name. We hit this through Sorbet/Tapioca: a DSL compiler iterating all classes emitted an RBI containing a bare class with no name, which then failed to parse:
sorbet/rbi/dsl/grape/api/instance.rbi:12: unexpected token tNL
That was on a grape 2.3 → 3.3 upgrade, and it took a while to attribute because name is correct and only to_s is blank — the two disagreeing is what makes it surprising. It also degrades grape's own diagnostics, since anything interpolating the class into a message renders nothing.
Suggested fix
Restore the nil fallback while keeping the delegation behavior:
def to_s
@base&.to_s || super
end
Happy to open a PR with a spec if you'd like.
Grape::API::Instance.to_sreturns""instead of its own class name.nameis still correct, so the two disagree — which breaks anything that renders a class by its string representation.Regression introduced in 3.0.0, still present in 3.3.4 and on
master.Repro
grape 3.3.4 (ruby 3.4.5):
grape 2.3.0, same script:
Cause
lib/grape/api/instance.rbdelegatesto_sto@base:@baseis only assigned throughbase=, which runs when an API subclass builds its mount instance. On the bareGrape::API::Instanceclass it is never set, so the delegator evaluatesnil.to_sand yields"".Before 3.0.0 the same delegation existed as an explicit method with a nil guard:
It was replaced in 3e46a25 ("Delegate to_s to base in instance"). The delegation itself is fine and clearly intended — note in the output above that
MyAPI.base_instance.to_sreturns"MyAPI"under both versions. The old form already produced that result; what the rewrite changed is only thenilcase, where|| superused to fall back toModule#to_s.Impact
Any tooling that enumerates loaded classes and renders them by string representation gets an empty name. We hit this through Sorbet/Tapioca: a DSL compiler iterating all classes emitted an RBI containing a bare
classwith no name, which then failed to parse:That was on a grape 2.3 → 3.3 upgrade, and it took a while to attribute because
nameis correct and onlyto_sis blank — the two disagreeing is what makes it surprising. It also degrades grape's own diagnostics, since anything interpolating the class into a message renders nothing.Suggested fix
Restore the nil fallback while keeping the delegation behavior:
Happy to open a PR with a spec if you'd like.