Skip to content

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

Description

@linyiru

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:

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):

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:

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).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

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