Skip to content

Grape::API::Instance.to_s returns "" since 3.0.0 (nil-guard lost when converted to def_delegators) #2832

Description

@sneako

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions