googleapis / googleapis/ruby-core-libraries

normalize_service resolves "Service" with inherit: true, breaking REST transports in apps with a top-level ::Service

Open
#70 0 comments 0 reactions 1 assignee Claimed by @viacheslav-rostovtsev View on GitHub
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.