From 7658d022c145b9f1a348235c10af4e87bb86a027 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Wed, 1 Jul 2026 14:53:10 -0700 Subject: [PATCH 1/7] attempt deduplicating x-goog-api-client headers --- .../google/api_core/gapic_v1/method.py | 27 +++++++++++++------ .../google/api_core/grpc_helpers.py | 19 +++++++++---- .../google-auth/google/auth/transport/grpc.py | 6 ++++- .../google-auth/tests/transport/test_grpc.py | 27 +++++++++++++++++++ 4 files changed, 65 insertions(+), 14 deletions(-) diff --git a/packages/google-api-core/google/api_core/gapic_v1/method.py b/packages/google-api-core/google/api_core/gapic_v1/method.py index b4481ca198a4..f4cc3eaca471 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/method.py +++ b/packages/google-api-core/google/api_core/gapic_v1/method.py @@ -114,14 +114,25 @@ def __call__( # Add the user agent metadata to the call. if self._metadata is not None: - metadata = kwargs.get("metadata", []) - # Due to the nature of invocation, None should be treated the same - # as not specified. - if metadata is None: - metadata = [] - metadata = list(metadata) - metadata.extend(self._metadata) - kwargs["metadata"] = metadata + metadata = kwargs.get("metadata") + if not metadata: + kwargs["metadata"] = self._metadata + else: + # Merge user-supplied metadata with library-supplied metadata. + # All keys in gRPC metadata are already lowercase. + from itertools import chain + metadata = list(metadata) + api_client_values = [] + merged_metadata = [] + for key, val in chain(metadata, self._metadata): + if key == "x-goog-api-client": + api_client_values.append(val) + else: + merged_metadata.append((key, val)) + if api_client_values: + merged_metadata.append(("x-goog-api-client", " ".join(api_client_values))) + kwargs["metadata"] = merged_metadata + if self._compression is not None: kwargs["compression"] = compression diff --git a/packages/google-api-core/google/api_core/grpc_helpers.py b/packages/google-api-core/google/api_core/grpc_helpers.py index 30ba19c54f1a..01b9092a2a26 100644 --- a/packages/google-api-core/google/api_core/grpc_helpers.py +++ b/packages/google-api-core/google/api_core/grpc_helpers.py @@ -254,11 +254,20 @@ def _create_composite_credentials( request = google.auth.transport.requests.Request() # Create the metadata plugin for inserting the authorization header. - metadata_plugin = google.auth.transport.grpc.AuthMetadataPlugin( - credentials, - request, - default_host=default_host, - ) + try: + metadata_plugin = google.auth.transport.grpc.AuthMetadataPlugin( + credentials, + request, + default_host=default_host, + suppress_metrics_header=True, + ) + except TypeError: + # Support older versions of google-auth that do not accept suppress_metrics_header + metadata_plugin = google.auth.transport.grpc.AuthMetadataPlugin( + credentials, + request, + default_host=default_host, + ) # Create a set of grpc.CallCredentials using the metadata plugin. google_auth_credentials = grpc.metadata_call_credentials(metadata_plugin) diff --git a/packages/google-auth/google/auth/transport/grpc.py b/packages/google-auth/google/auth/transport/grpc.py index e541d20ca0a4..3952bbeae63b 100644 --- a/packages/google-auth/google/auth/transport/grpc.py +++ b/packages/google-auth/google/auth/transport/grpc.py @@ -49,7 +49,7 @@ class AuthMetadataPlugin(grpc.AuthMetadataPlugin): account credentials. """ - def __init__(self, credentials, request, default_host=None): + def __init__(self, credentials, request, default_host=None, suppress_metrics_header=False): # pylint: disable=no-value-for-parameter # pylint doesn't realize that the super method takes no arguments # because this class is the same name as the superclass. @@ -57,6 +57,7 @@ def __init__(self, credentials, request, default_host=None): self._credentials = credentials self._request = request self._default_host = default_host + self._suppress_metrics_header = suppress_metrics_header def _get_authorization_headers(self, context): """Gets the authorization headers for a request. @@ -80,6 +81,9 @@ def _get_authorization_headers(self, context): self._request, context.method_name, context.service_url, headers ) + if self._suppress_metrics_header and "x-goog-api-client" in headers: + del headers["x-goog-api-client"] + return list(headers.items()) def __call__(self, context, callback): diff --git a/packages/google-auth/tests/transport/test_grpc.py b/packages/google-auth/tests/transport/test_grpc.py index 7ebd14758e55..6907d3be8b3c 100644 --- a/packages/google-auth/tests/transport/test_grpc.py +++ b/packages/google-auth/tests/transport/test_grpc.py @@ -132,6 +132,33 @@ def test__get_authorization_headers_with_service_account_and_default_host(self): "https://{}/".format(default_host) ) + def test_suppress_metrics_header(self): + credentials = mock.create_autospec(service_account.Credentials) + # Mock credentials before_request that adds metric and authorization + def mock_before_request(request, method, url, headers): + headers["x-goog-api-client"] = "foo" + headers["authorization"] = "Bearer token" + credentials.before_request.side_effect = mock_before_request + request = mock.create_autospec(transport.Request) + + # By default, suppress_metrics_header=False + plugin = google.auth.transport.grpc.AuthMetadataPlugin(credentials, request) + context = mock.create_autospec(grpc.AuthMetadataContext, instance=True) + context.method_name = "methodName" + context.service_url = "https://pubsub.googleapis.com/methodName" + + headers = dict(plugin._get_authorization_headers(context)) + assert "x-goog-api-client" in headers + assert headers["x-goog-api-client"] == "foo" + + # With suppress_metrics_header=True + plugin_suppressed = google.auth.transport.grpc.AuthMetadataPlugin( + credentials, request, suppress_metrics_header=True + ) + headers_suppressed = dict(plugin_suppressed._get_authorization_headers(context)) + assert "x-goog-api-client" not in headers_suppressed + assert headers_suppressed["authorization"] == "Bearer token" + @mock.patch( "google.auth.transport._mtls_helper.get_client_ssl_credentials", autospec=True From 7d266a352bb1e188f79f2eaf3397e9d824104701 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Wed, 1 Jul 2026 15:27:36 -0700 Subject: [PATCH 2/7] added test --- .../tests/unit/gapic/test_method.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/packages/google-api-core/tests/unit/gapic/test_method.py b/packages/google-api-core/tests/unit/gapic/test_method.py index 29e8fc2173bd..a6e7f03e7b0d 100644 --- a/packages/google-api-core/tests/unit/gapic/test_method.py +++ b/packages/google-api-core/tests/unit/gapic/test_method.py @@ -121,6 +121,39 @@ def test_invoke_wrapped_method_with_metadata_as_none(): assert len(metadata) == 1 +def test_invoke_wrapped_method_with_duplicate_x_goog_api_client_metadata(): + method = mock.Mock(spec=["__call__"]) + + # Create a custom ClientInfo with defined properties so we know exactly what is returned + client_info = google.api_core.gapic_v1.client_info.ClientInfo( + user_agent="custom-user-agent/1.0", + python_version="3.14.0", + grpc_version="1.76.0", + api_core_version="2.29.0" + ) + + wrapped_method = google.api_core.gapic_v1.method.wrap_method(method, client_info=client_info) + + # Invoke the wrapped method with an explicit user-provided custom header + wrapped_method( + mock.sentinel.request, + metadata=[("x-goog-api-client", "override-client/2.0"), ("other-header", "value")] + ) + + method.assert_called_once_with(mock.sentinel.request, metadata=mock.ANY) + metadata = method.call_args[1]["metadata"] + + # There should only be one "x-goog-api-client" header, containing both values joined by space, + # plus the other-header. + assert len(metadata) == 2 + metadata_dict = dict(metadata) + assert "other-header" in metadata_dict + assert metadata_dict["other-header"] == "value" + assert "x-goog-api-client" in metadata_dict + # Verify both the user-provided override value and the library system telemetry are merged explicitly + assert metadata_dict["x-goog-api-client"] == "override-client/2.0 custom-user-agent/1.0 gl-python/3.14.0 grpc/1.76.0 gax/2.29.0" + + @mock.patch("time.sleep") def test_wrap_method_with_default_retry_and_timeout_and_compression(unused_sleep): method = mock.Mock( From 852c5b1d6e073f8da3f1c06ce0cc3b66a380a390 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Wed, 1 Jul 2026 15:27:52 -0700 Subject: [PATCH 3/7] optimized for hard-coded metadata --- .../google/api_core/gapic_v1/method.py | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/packages/google-api-core/google/api_core/gapic_v1/method.py b/packages/google-api-core/google/api_core/gapic_v1/method.py index f4cc3eaca471..49fa7f591f15 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/method.py +++ b/packages/google-api-core/google/api_core/gapic_v1/method.py @@ -91,6 +91,14 @@ def __init__( self._timeout = timeout self._compression = compression self._metadata = metadata + # separate x-goog-api-client header from provided metadata + self._arbitrary_metadata = [] + self._metrics_values = "" + for key, val in metadata: + if key == client_info.METRICS_METADATA_KEY: + self._metrics_values = val + else: + self._arbitrary_metadata.append((key, val)) def __call__( self, *args, timeout=DEFAULT, retry=DEFAULT, compression=DEFAULT, **kwargs @@ -116,21 +124,22 @@ def __call__( if self._metadata is not None: metadata = kwargs.get("metadata") if not metadata: - kwargs["metadata"] = self._metadata + if self._metrics_values: + kwargs["metadata"] = [(client_info.METRICS_METADATA_KEY, self._metrics_values), *self._arbitrary_metadata] + else: + kwargs["metadata"] = self._arbitrary_metadata else: # Merge user-supplied metadata with library-supplied metadata. - # All keys in gRPC metadata are already lowercase. - from itertools import chain metadata = list(metadata) - api_client_values = [] + metric_values = [self._metrics_values] if self._metrics_values else [] merged_metadata = [] - for key, val in chain(metadata, self._metadata): - if key == "x-goog-api-client": - api_client_values.append(val) + for key, val in metadata: + if key == client_info.METRICS_METADATA_KEY: + metric_values.append(val) else: merged_metadata.append((key, val)) - if api_client_values: - merged_metadata.append(("x-goog-api-client", " ".join(api_client_values))) + if metric_values: + merged_metadata.append((client_info.METRICS_METADATA_KEY, " ".join(metric_values))) kwargs["metadata"] = merged_metadata if self._compression is not None: From 0d8865a1dda0df0ba6e9c6f3de00415ef0134f18 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Wed, 1 Jul 2026 15:34:29 -0700 Subject: [PATCH 4/7] cleaning up code --- .../google/api_core/gapic_v1/method.py | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/packages/google-api-core/google/api_core/gapic_v1/method.py b/packages/google-api-core/google/api_core/gapic_v1/method.py index 49fa7f591f15..486dbb3a478f 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/method.py +++ b/packages/google-api-core/google/api_core/gapic_v1/method.py @@ -91,14 +91,17 @@ def __init__( self._timeout = timeout self._compression = compression self._metadata = metadata - # separate x-goog-api-client header from provided metadata + + # Pre-extract the client metrics header from the initialized metadata. + # This avoids repeating this work on every single RPC request invocation. self._arbitrary_metadata = [] self._metrics_values = "" - for key, val in metadata: - if key == client_info.METRICS_METADATA_KEY: - self._metrics_values = val - else: - self._arbitrary_metadata.append((key, val)) + if metadata: + for key, val in metadata: + if key == client_info.METRICS_METADATA_KEY: + self._metrics_values = val + else: + self._arbitrary_metadata.append((key, val)) def __call__( self, *args, timeout=DEFAULT, retry=DEFAULT, compression=DEFAULT, **kwargs @@ -124,22 +127,28 @@ def __call__( if self._metadata is not None: metadata = kwargs.get("metadata") if not metadata: + # Fast path: in 99% of calls, the user did not pass any custom metadata, + # so we can directly assign the pre-extracted metadata and skip any merging overhead. if self._metrics_values: - kwargs["metadata"] = [(client_info.METRICS_METADATA_KEY, self._metrics_values), *self._arbitrary_metadata] + kwargs["metadata"] = [(client_info.METRICS_METADATA_KEY, self._metrics_values)] + self._arbitrary_metadata else: kwargs["metadata"] = self._arbitrary_metadata else: # Merge user-supplied metadata with library-supplied metadata. + # All keys in gRPC metadata are already lowercase. metadata = list(metadata) - metric_values = [self._metrics_values] if self._metrics_values else [] + api_client_values = [] merged_metadata = [] for key, val in metadata: if key == client_info.METRICS_METADATA_KEY: - metric_values.append(val) + api_client_values.append(val) else: merged_metadata.append((key, val)) - if metric_values: - merged_metadata.append((client_info.METRICS_METADATA_KEY, " ".join(metric_values))) + if self._metrics_values: + api_client_values.append(self._metrics_values) + if api_client_values: + merged_metadata.append((client_info.METRICS_METADATA_KEY, " ".join(api_client_values))) + merged_metadata.extend(self._arbitrary_metadata) kwargs["metadata"] = merged_metadata if self._compression is not None: From 76274cab6d73b01a27c54b27e3f052808f0f743e Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Wed, 1 Jul 2026 16:33:08 -0700 Subject: [PATCH 5/7] pulled out helper method --- .../google/api_core/gapic_v1/method.py | 54 +++++++++++-------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/packages/google-api-core/google/api_core/gapic_v1/method.py b/packages/google-api-core/google/api_core/gapic_v1/method.py index 486dbb3a478f..278dad9cf51c 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/method.py +++ b/packages/google-api-core/google/api_core/gapic_v1/method.py @@ -57,6 +57,34 @@ def _apply_decorators(func, decorators): return func +def _extract_metrics_header(metadata): + """Extract x-google-api-client header from metadata list. + + Args: + metadata (Sequence[Tuple[str, str]]): The metadata to extract from. + + Returns: + Tuple[List[Tuple[str, str]], List[str]]: A tuple containing: + - A list of remaining metadata tuples. + - A list of metrics header values found. + """ + if not metadata: + return [], [] + + for i, (key, val) in enumerate(metadata): + if key == client_info.METRICS_METADATA_KEY: + # Key located. Check the rest of the list for duplicate entries + arbitrary_metadata = list(metadata[:i]) + metric_values = [val] + for k, v in metadata[i+1:]: + if k == client_info.METRICS_METADATA_KEY: + metric_values.append(v) + else: + arbitrary_metadata.append((k, v)) + return arbitrary_metadata, metric_values + # No key found + return list(metadata), [] + class _GapicCallable(object): """Callable that applies retry, timeout, and metadata logic. @@ -91,17 +119,9 @@ def __init__( self._timeout = timeout self._compression = compression self._metadata = metadata - - # Pre-extract the client metrics header from the initialized metadata. - # This avoids repeating this work on every single RPC request invocation. - self._arbitrary_metadata = [] - self._metrics_values = "" - if metadata: - for key, val in metadata: - if key == client_info.METRICS_METADATA_KEY: - self._metrics_values = val - else: - self._arbitrary_metadata.append((key, val)) + # Pre-extract the x-goog-api-client header from the initialized metadata. + self._arbitrary_metadata, metric_values = _extract_metrics_header(metadata) + self._metrics_values = " ".join(metric_values) if metric_values else "" def __call__( self, *args, timeout=DEFAULT, retry=DEFAULT, compression=DEFAULT, **kwargs @@ -127,23 +147,13 @@ def __call__( if self._metadata is not None: metadata = kwargs.get("metadata") if not metadata: - # Fast path: in 99% of calls, the user did not pass any custom metadata, - # so we can directly assign the pre-extracted metadata and skip any merging overhead. if self._metrics_values: kwargs["metadata"] = [(client_info.METRICS_METADATA_KEY, self._metrics_values)] + self._arbitrary_metadata else: kwargs["metadata"] = self._arbitrary_metadata else: # Merge user-supplied metadata with library-supplied metadata. - # All keys in gRPC metadata are already lowercase. - metadata = list(metadata) - api_client_values = [] - merged_metadata = [] - for key, val in metadata: - if key == client_info.METRICS_METADATA_KEY: - api_client_values.append(val) - else: - merged_metadata.append((key, val)) + merged_metadata, api_client_values = _extract_metrics_header(metadata) if self._metrics_values: api_client_values.append(self._metrics_values) if api_client_values: From e42f0a3d8359b077abaa6a44ad1c645cb46de9d5 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Wed, 1 Jul 2026 16:49:02 -0700 Subject: [PATCH 6/7] added docstring --- packages/google-auth/google/auth/transport/grpc.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/google-auth/google/auth/transport/grpc.py b/packages/google-auth/google/auth/transport/grpc.py index 3952bbeae63b..93f392731163 100644 --- a/packages/google-auth/google/auth/transport/grpc.py +++ b/packages/google-auth/google/auth/transport/grpc.py @@ -47,9 +47,11 @@ class AuthMetadataPlugin(grpc.AuthMetadataPlugin): default_host (Optional[str]): A host like "pubsub.googleapis.com". This is used when a self-signed JWT is created from service account credentials. + suppress_metrics_header (bool): When enabled, ``x-goog-api-client`` + will be stripped from authorization headers. """ - def __init__(self, credentials, request, default_host=None, suppress_metrics_header=False): + def __init__(self, credentials, request, default_host=None, *, suppress_metrics_header=False): # pylint: disable=no-value-for-parameter # pylint doesn't realize that the super method takes no arguments # because this class is the same name as the superclass. From fc05e08dd21f0f7e86d444a7f93905c0450fccd6 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Wed, 1 Jul 2026 17:04:42 -0700 Subject: [PATCH 7/7] fixed format --- .../google/api_core/gapic_v1/method.py | 11 ++++++++--- .../tests/unit/gapic/test_method.py | 16 ++++++++++++---- .../google-auth/google/auth/transport/grpc.py | 4 +++- .../google-auth/tests/transport/test_grpc.py | 2 ++ 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/packages/google-api-core/google/api_core/gapic_v1/method.py b/packages/google-api-core/google/api_core/gapic_v1/method.py index 278dad9cf51c..b56463cf1770 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/method.py +++ b/packages/google-api-core/google/api_core/gapic_v1/method.py @@ -76,7 +76,7 @@ def _extract_metrics_header(metadata): # Key located. Check the rest of the list for duplicate entries arbitrary_metadata = list(metadata[:i]) metric_values = [val] - for k, v in metadata[i+1:]: + for k, v in metadata[i + 1 :]: if k == client_info.METRICS_METADATA_KEY: metric_values.append(v) else: @@ -85,6 +85,7 @@ def _extract_metrics_header(metadata): # No key found return list(metadata), [] + class _GapicCallable(object): """Callable that applies retry, timeout, and metadata logic. @@ -148,7 +149,9 @@ def __call__( metadata = kwargs.get("metadata") if not metadata: if self._metrics_values: - kwargs["metadata"] = [(client_info.METRICS_METADATA_KEY, self._metrics_values)] + self._arbitrary_metadata + kwargs["metadata"] = [ + (client_info.METRICS_METADATA_KEY, self._metrics_values) + ] + self._arbitrary_metadata else: kwargs["metadata"] = self._arbitrary_metadata else: @@ -157,7 +160,9 @@ def __call__( if self._metrics_values: api_client_values.append(self._metrics_values) if api_client_values: - merged_metadata.append((client_info.METRICS_METADATA_KEY, " ".join(api_client_values))) + merged_metadata.append( + (client_info.METRICS_METADATA_KEY, " ".join(api_client_values)) + ) merged_metadata.extend(self._arbitrary_metadata) kwargs["metadata"] = merged_metadata diff --git a/packages/google-api-core/tests/unit/gapic/test_method.py b/packages/google-api-core/tests/unit/gapic/test_method.py index a6e7f03e7b0d..337451e0ed83 100644 --- a/packages/google-api-core/tests/unit/gapic/test_method.py +++ b/packages/google-api-core/tests/unit/gapic/test_method.py @@ -129,15 +129,20 @@ def test_invoke_wrapped_method_with_duplicate_x_goog_api_client_metadata(): user_agent="custom-user-agent/1.0", python_version="3.14.0", grpc_version="1.76.0", - api_core_version="2.29.0" + api_core_version="2.29.0", ) - wrapped_method = google.api_core.gapic_v1.method.wrap_method(method, client_info=client_info) + wrapped_method = google.api_core.gapic_v1.method.wrap_method( + method, client_info=client_info + ) # Invoke the wrapped method with an explicit user-provided custom header wrapped_method( mock.sentinel.request, - metadata=[("x-goog-api-client", "override-client/2.0"), ("other-header", "value")] + metadata=[ + ("x-goog-api-client", "override-client/2.0"), + ("other-header", "value"), + ], ) method.assert_called_once_with(mock.sentinel.request, metadata=mock.ANY) @@ -151,7 +156,10 @@ def test_invoke_wrapped_method_with_duplicate_x_goog_api_client_metadata(): assert metadata_dict["other-header"] == "value" assert "x-goog-api-client" in metadata_dict # Verify both the user-provided override value and the library system telemetry are merged explicitly - assert metadata_dict["x-goog-api-client"] == "override-client/2.0 custom-user-agent/1.0 gl-python/3.14.0 grpc/1.76.0 gax/2.29.0" + assert ( + metadata_dict["x-goog-api-client"] + == "override-client/2.0 custom-user-agent/1.0 gl-python/3.14.0 grpc/1.76.0 gax/2.29.0" + ) @mock.patch("time.sleep") diff --git a/packages/google-auth/google/auth/transport/grpc.py b/packages/google-auth/google/auth/transport/grpc.py index 93f392731163..df3f8a7dcf54 100644 --- a/packages/google-auth/google/auth/transport/grpc.py +++ b/packages/google-auth/google/auth/transport/grpc.py @@ -51,7 +51,9 @@ class AuthMetadataPlugin(grpc.AuthMetadataPlugin): will be stripped from authorization headers. """ - def __init__(self, credentials, request, default_host=None, *, suppress_metrics_header=False): + def __init__( + self, credentials, request, default_host=None, *, suppress_metrics_header=False + ): # pylint: disable=no-value-for-parameter # pylint doesn't realize that the super method takes no arguments # because this class is the same name as the superclass. diff --git a/packages/google-auth/tests/transport/test_grpc.py b/packages/google-auth/tests/transport/test_grpc.py index 6907d3be8b3c..15f7e6b62842 100644 --- a/packages/google-auth/tests/transport/test_grpc.py +++ b/packages/google-auth/tests/transport/test_grpc.py @@ -134,10 +134,12 @@ def test__get_authorization_headers_with_service_account_and_default_host(self): def test_suppress_metrics_header(self): credentials = mock.create_autospec(service_account.Credentials) + # Mock credentials before_request that adds metric and authorization def mock_before_request(request, method, url, headers): headers["x-goog-api-client"] = "foo" headers["authorization"] = "Bearer token" + credentials.before_request.side_effect = mock_before_request request = mock.create_autospec(transport.Request)