feat(auth): append fallback x-goog-api-client header and prepare v2.3.4 (#761)
* feat(auth): append fallback x-goog-api-client header
* refactor(auth): share x-goog-api-client constants and verify packageVersion against pubspec.yaml
* chore(auth): prepare googleapis_auth v2.3.4 release
* refactor(auth): unify x-goog-api-client injection into addXGoogApiClientHeader and privatize constants
* fix(auth): use _dartVersion getter to satisfy prefer_const_declarations
* refactor(auth): inline single-use constants in version.dart
diff --git a/googleapis_auth/CHANGELOG.md b/googleapis_auth/CHANGELOG.md
index 1cf4d13..fe4c017 100644
--- a/googleapis_auth/CHANGELOG.md
+++ b/googleapis_auth/CHANGELOG.md
@@ -1,5 +1,8 @@
-## 2.3.4-wip
+## 2.3.4
+- Added a fallback `x-goog-api-client` tracking header to `AuthenticatedClient`,
+ `ApiKeyClient`, `signBlob`, and OAuth/STS token requests when not already set
+ on outgoing requests.
- Updated README for the current Google Cloud Console credentials UI,
Desktop app OAuth clients, and optional client secrets.
diff --git a/googleapis_auth/lib/src/auth_http_utils.dart b/googleapis_auth/lib/src/auth_http_utils.dart
index 551fe3d..4530de5 100644
--- a/googleapis_auth/lib/src/auth_http_utils.dart
+++ b/googleapis_auth/lib/src/auth_http_utils.dart
@@ -12,6 +12,7 @@
import 'auth_functions.dart';
import 'http_client_base.dart';
import 'service_account_credentials.dart';
+import 'version.dart';
/// Will close the underlying `http.Client` depending on a constructor argument.
class AuthenticatedClient extends DelegatingClient implements AuthClient {
@@ -40,6 +41,9 @@
if (quotaProject != null) {
modifiedRequest.headers['X-Goog-User-Project'] = quotaProject!;
}
+
+ addXGoogApiClientHeader(modifiedRequest.headers);
+
final response = await baseClient.send(modifiedRequest);
final wwwAuthenticate = response.headers['www-authenticate'];
if (wwwAuthenticate != null) {
@@ -83,6 +87,7 @@
final modifiedRequest = RequestImpl(request.method, url, request.finalize())
..headers.addAll(request.headers);
+ addXGoogApiClientHeader(modifiedRequest.headers);
return baseClient.send(modifiedRequest);
}
}
diff --git a/googleapis_auth/lib/src/iam_signer.dart b/googleapis_auth/lib/src/iam_signer.dart
index d757f21..d674ca3 100644
--- a/googleapis_auth/lib/src/iam_signer.dart
+++ b/googleapis_auth/lib/src/iam_signer.dart
@@ -11,6 +11,7 @@
import 'metadata_server_stub.dart'
if (dart.library.io) 'metadata_server_io.dart';
import 'utils.dart';
+import 'version.dart';
/// Signs the given [data] using the IAM Credentials API.
///
@@ -68,7 +69,7 @@
final response = await client.post(
signBlobUrl,
- headers: {'Content-Type': 'application/json'},
+ headers: addXGoogApiClientHeader({'Content-Type': 'application/json'}),
body: requestBody,
);
diff --git a/googleapis_auth/lib/src/utils.dart b/googleapis_auth/lib/src/utils.dart
index e0858d8..bccf3cd 100644
--- a/googleapis_auth/lib/src/utils.dart
+++ b/googleapis_auth/lib/src/utils.dart
@@ -12,6 +12,7 @@
import 'access_token.dart';
import 'auth_endpoints.dart';
import 'exceptions.dart';
+import 'version.dart';
/// Due to differences of clock speed, network latency, etc. we
/// will shorten expiry dates by 20 seconds.
@@ -101,6 +102,7 @@
if (headers != null) {
request.headers.addAll(headers);
}
+ addXGoogApiClientHeader(request.headers);
switch (body) {
case null:
break;
diff --git a/googleapis_auth/lib/src/version.dart b/googleapis_auth/lib/src/version.dart
new file mode 100644
index 0000000..0fa5da0
--- /dev/null
+++ b/googleapis_auth/lib/src/version.dart
@@ -0,0 +1,20 @@
+// Copyright 2026 Google LLC
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+import 'version_fallback.dart' if (dart.library.io) 'version_io.dart';
+
+/// Must be kept in sync with `pubspec.yaml` (verified by `test/version_test.dart`).
+final _xGoogApiClientHeaderValue = 'gl-dart/$dartVersion auth/2.3.4';
+
+/// Adds the fallback `x-goog-api-client` header to [headers] if not already
+/// present, and returns [headers].
+Map<String, String> addXGoogApiClientHeader(Map<String, String> headers) {
+ const header = 'x-goog-api-client';
+ if (!headers.keys.any((k) => k.toLowerCase() == header)) {
+ headers[header] = _xGoogApiClientHeaderValue;
+ }
+ return headers;
+}
diff --git a/googleapis_auth/lib/src/version_fallback.dart b/googleapis_auth/lib/src/version_fallback.dart
new file mode 100644
index 0000000..f6a29e7
--- /dev/null
+++ b/googleapis_auth/lib/src/version_fallback.dart
@@ -0,0 +1,9 @@
+// Copyright 2026 Google LLC
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+/// Fallback Dart version as `Platform` from `dart:io` is not available when
+/// targeting JavaScript/Wasm.
+String get dartVersion => 'unknown';
diff --git a/googleapis_auth/lib/src/version_io.dart b/googleapis_auth/lib/src/version_io.dart
new file mode 100644
index 0000000..6040f6b
--- /dev/null
+++ b/googleapis_auth/lib/src/version_io.dart
@@ -0,0 +1,10 @@
+// Copyright 2026 Google LLC
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+import 'dart:io' show Platform;
+
+/// Major.minor.patch version of the current Dart SDK.
+final dartVersion = Platform.version.split(RegExp('[^0-9]')).take(3).join('.');
diff --git a/googleapis_auth/pubspec.yaml b/googleapis_auth/pubspec.yaml
index 9c428d8..d0b95a9 100644
--- a/googleapis_auth/pubspec.yaml
+++ b/googleapis_auth/pubspec.yaml
@@ -1,5 +1,5 @@
name: googleapis_auth
-version: 2.3.4-wip
+version: 2.3.4
description: Obtain Access credentials for Google services using OAuth 2.0
repository: https://github.com/google/googleapis.dart/tree/master/googleapis_auth
diff --git a/googleapis_auth/test/http_client_base_test.dart b/googleapis_auth/test/http_client_base_test.dart
index 2429de1..4d3c3a6 100644
--- a/googleapis_auth/test/http_client_base_test.dart
+++ b/googleapis_auth/test/http_client_base_test.dart
@@ -90,6 +90,7 @@
test('no-query-string adds key', () {
final mock = mockClient((Request request) {
expect('${request.url}', 'http://localhost/abc?$keyEncoded');
+ expectXGoogApiClientHeader(request);
return responseF();
});
diff --git a/googleapis_auth/test/iam_signer_test.dart b/googleapis_auth/test/iam_signer_test.dart
index b98249c..6917122 100644
--- a/googleapis_auth/test/iam_signer_test.dart
+++ b/googleapis_auth/test/iam_signer_test.dart
@@ -13,11 +13,14 @@
import 'package:http/testing.dart';
import 'package:test/test.dart';
+import 'test_utils.dart';
+
void main() {
test('signBlob posts to correct URL and returns signed blob', () async {
final client = MockClient((request) async {
if (request.url.path.contains('signBlob')) {
expect(request.url.toString(), contains('test-email%40example.com'));
+ expectXGoogApiClientHeader(request);
final body = jsonDecode(request.body) as Map<String, dynamic>;
expect(body['payload'], isNotNull);
return Response(
diff --git a/googleapis_auth/test/impersonated_auth_client_test.dart b/googleapis_auth/test/impersonated_auth_client_test.dart
index aa34c56..da32fd5 100644
--- a/googleapis_auth/test/impersonated_auth_client_test.dart
+++ b/googleapis_auth/test/impersonated_auth_client_test.dart
@@ -472,6 +472,7 @@
final customBaseClient = mockClient((request) async {
authenticatedRequestCalled = true;
expect(request.headers['Authorization'], 'Bearer impersonated-token');
+ expectXGoogApiClientHeader(request);
return http.Response('ok', 200);
}, expectClose: false);
diff --git a/googleapis_auth/test/oauth2_test.dart b/googleapis_auth/test/oauth2_test.dart
index 5cd1289..9ec50f5 100644
--- a/googleapis_auth/test/oauth2_test.dart
+++ b/googleapis_auth/test/oauth2_test.dart
@@ -329,11 +329,12 @@
expectAsync1((request) async {
expect(request.method, 'POST');
expect(request.url, url);
- expect(request.headers, hasLength(1));
+ expect(request.headers, hasLength(2));
expect(
request.headers,
containsPair('Authorization', 'Bearer bar'),
);
+ expectXGoogApiClientHeader(request);
return Response('', 204);
}),
@@ -347,7 +348,8 @@
expect(response.statusCode, 204);
});
- test('successful request with quotaProject', () async {
+ test('preserves existing x-goog-api-client header', () async {
+ const existingHeader = 'gl-dart/3.8.0 gdcl/17.0.0';
final client = authenticatedClient(
mockClient(
expectAsync1((request) async {
@@ -360,8 +362,38 @@
);
expect(
request.headers,
+ containsPair('x-goog-api-client', existingHeader),
+ );
+
+ return Response('', 204);
+ }),
+ expectClose: false,
+ ),
+ credentials,
+ );
+
+ final req = RequestImpl('POST', url)
+ ..headers['X-Goog-Api-Client'] = existingHeader;
+ final response = await client.send(req);
+ expect(response.statusCode, 204);
+ });
+
+ test('successful request with quotaProject', () async {
+ final client = authenticatedClient(
+ mockClient(
+ expectAsync1((request) async {
+ expect(request.method, 'POST');
+ expect(request.url, url);
+ expect(request.headers, hasLength(3));
+ expect(
+ request.headers,
+ containsPair('Authorization', 'Bearer bar'),
+ );
+ expect(
+ request.headers,
containsPair('x-goog-user-project', 'test-quota-project'),
);
+ expectXGoogApiClientHeader(request);
return Response('', 204);
}),
@@ -382,11 +414,12 @@
expectAsync1((request) async {
expect(request.method, 'POST');
expect(request.url, url);
- expect(request.headers, hasLength(1));
+ expect(request.headers, hasLength(2));
expect(
request.headers,
containsPair('Authorization', 'Bearer bar'),
);
+ expectXGoogApiClientHeader(request);
const headers = {'www-authenticate': 'foobar'};
return Response('', 401, headers: headers);
diff --git a/googleapis_auth/test/sts_auth_client_test.dart b/googleapis_auth/test/sts_auth_client_test.dart
index 8dfaa11..783c9f1 100644
--- a/googleapis_auth/test/sts_auth_client_test.dart
+++ b/googleapis_auth/test/sts_auth_client_test.dart
@@ -26,6 +26,7 @@
scopes: ['s1'],
baseClient: mockClient(expectClose: false, (Request request) async {
if (request.url.toString() == 'https://sts.googleapis.com/v1/token') {
+ expectXGoogApiClientHeader(request);
final body = jsonDecode(request.body) as Map<String, dynamic>;
expect(body['subjectToken'], 'my-token');
expect(body['audience'], 'my-audience');
diff --git a/googleapis_auth/test/test_utils.dart b/googleapis_auth/test/test_utils.dart
index 78af369..9f083b7 100644
--- a/googleapis_auth/test/test_utils.dart
+++ b/googleapis_auth/test/test_utils.dart
@@ -7,12 +7,23 @@
import 'package:googleapis_auth/googleapis_auth.dart';
import 'package:googleapis_auth/src/crypto/pem.dart';
import 'package:googleapis_auth/src/utils.dart';
+import 'package:googleapis_auth/src/version.dart';
import 'package:http/http.dart';
import 'package:http/testing.dart';
import 'package:test/test.dart';
const jsonContentType = {'content-type': 'application/json'};
+void expectXGoogApiClientHeader(BaseRequest request) {
+ expect(
+ request.headers,
+ containsPair(
+ 'x-goog-api-client',
+ addXGoogApiClientHeader({})['x-goog-api-client'],
+ ),
+ );
+}
+
const isServerRequestFailedException =
TypeMatcher<ServerRequestFailedException>();
diff --git a/googleapis_auth/test/version_test.dart b/googleapis_auth/test/version_test.dart
new file mode 100644
index 0000000..e017d40
--- /dev/null
+++ b/googleapis_auth/test/version_test.dart
@@ -0,0 +1,67 @@
+// Copyright 2026 Google LLC
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+@TestOn('vm')
+library;
+
+import 'dart:io';
+import 'dart:isolate';
+
+import 'package:googleapis_auth/src/version.dart';
+import 'package:test/test.dart';
+
+void main() {
+ test(
+ 'addXGoogApiClientHeader uses pubspec.yaml version and VM Dart version',
+ () async {
+ final pkgUri = await Isolate.resolvePackageUri(
+ Uri.parse('package:googleapis_auth/googleapis_auth.dart'),
+ );
+ expect(pkgUri, isNotNull, reason: 'package URI must resolve');
+
+ final pubspecFile = File.fromUri(pkgUri!.resolve('../pubspec.yaml'));
+ expect(
+ pubspecFile.existsSync(),
+ isTrue,
+ reason: 'pubspec.yaml must exist',
+ );
+
+ final content = pubspecFile.readAsStringSync();
+ final match = RegExp(
+ r'^version:\s*(\S+)',
+ multiLine: true,
+ ).firstMatch(content);
+ expect(
+ match,
+ isNotNull,
+ reason: 'version must be declared in pubspec.yaml',
+ );
+
+ final pubspecVersion = match!.group(1);
+ final expectedDartVersion = Platform.version
+ .split(RegExp('[^0-9]'))
+ .take(3)
+ .join('.');
+ expect(expectedDartVersion, matches(RegExp(r'^\d+\.\d+\.\d+$')));
+
+ final headers = addXGoogApiClientHeader({});
+ expect(headers, {
+ 'x-goog-api-client':
+ 'gl-dart/$expectedDartVersion auth/$pubspecVersion',
+ });
+ },
+ );
+
+ test(
+ 'addXGoogApiClientHeader preserves existing header case-insensitively',
+ () {
+ final headers = addXGoogApiClientHeader({
+ 'X-Goog-Api-Client': 'gl-dart/3.8.0 gdcl/17.0.0',
+ });
+ expect(headers, {'X-Goog-Api-Client': 'gl-dart/3.8.0 gdcl/17.0.0'});
+ },
+ );
+}