googleapis / googleapis/ruby-core-libraries
normalize_service resolves "Service" with inherit: true, breaking REST transports in apps with a top-level ::Service
- Dominant language
- Ruby
- Stars
- 14
- Forks
- 12
- PR merge metrics
- No merged PRs in 30d
Description
### Summary
`Gapic::LoggingConcerns.normalize_service` looks up the `Service` constant with `const_defined?`'s default `inherit: true`. Generated **REST** stubs have no sibling `Service` constant, so the lookup walks up the ancestry to `Object`. In any application that defines a top-level `Service` constant, it finds that instead and calls `service_name` on it, raising `NoMethodError` while constructing the client.
gRPC transports are unaffected, because the gRPC layout does define a sibling `Service`.
### Affected versions
Reproduced on `gapic-common` 1.1.0. The relevant code is identical in 1.2.0 and 1.3.0 (current latest), so I believe all three are affected.
### Reproduction
No framework needed — a plain script and a top-level `Service`:
```ruby
require "gapic/logging_concerns"
# Stand-in for an application class. Any top-level `Service` triggers this;
# it does not have to be a model or anything framework-specific.
class Service; end
# Stand-in for a generated REST client's namespace.
module Example
module V1
module ExampleService
module Rest
class ServiceStub; end
end
end
end
end
mod = Example::V1::ExampleService::Rest
puts "Service defined locally in #{mod}? #{mod.const_defined?('Service', false)}"
puts "Service visible via inherit: true? #{mod.const_defined?('Service')}"
puts "resolves to top-level ::Service? #{mod.const_get('Service').equal?(::Service)}"
Gapic::LoggingConcerns.normalize_service Example::V1::ExampleService::Rest::ServiceStub
```
Output:
```
Service defined locally in Example::V1::ExampleService::Rest? false
Service visible via inherit: true? true
resolves to top-level ::Service? true
NoMethodError: undefined method 'service_name' for class Service
gapic-common-1.1.0/lib/gapic/logging_concerns.rb:204:in 'Gapic::LoggingConcerns.normalize_service'
```
### Expected
`normalize_service` falls through to the `Rest` branch and returns the dotted service name, as it does when no unrelated `Service` constant happens to be in scope.
### Actual
`NoMethodError: undefined method 'service_name' for class Service`, raised during client construction.
### Cause
`lib/gapic/logging_concerns.rb` (1.3.0, lines 197-211):
```ruby
def normalize_service input
case input
when String
input
when Class
mod = input.name.split("::")[..-2].inject(Object) { |m, n| m.const_get n }
if mod.const_defined? "Service" # <- inherit: true
mod.const_get("Service").service_name
else
name_segments = input.name.split("::")[..-3]
mod = name_segments.inject(Object) { |m, n| m.const_get n }
name_segments.join "." if mod.const_defined? "Rest"
end
end
end
```
### Suggested fix
Scope the lookup to the module itself:
```ruby
if mod.const_defined? "Service", false
```
That keeps the gRPC path working (its `Service` is defined locally) and lets the REST path reach its intended fallback branch.
### Notes
`normalize_service` runs before the logger is configured, so setting `GOOGLE_SDK_RUBY_LOGGING_GEMS=false` (or `none`) does not avoid it — the exception is raised whether or not logging is enabled.
This surfaced when switching a Secret Manager client from gRPC to REST; the only workaround I found was defining a `Service` constant inside each generated `Rest` namespace, which has to be repeated for every nested client the library constructs (a Secret Manager REST client also builds a `Google::Cloud::Location::Locations::Rest::ServiceStub`).
Contributor guide
Assessment
This issue has not been assessed yet.