From 2fe903ca81ea777ba026e758b0f75593daf6a4bb Mon Sep 17 00:00:00 2001 From: Guillaume Bernos Date: Thu, 27 Aug 2026 12:56:54 +0200 Subject: [PATCH] fix(firestore): honour Settings.ssl when building the API endpoint `Settings.ssl` was a dead field. It was stored, carried through copyWith, and compared in == and hashCode, but nothing ever read it: the endpoint was built with `Uri.https(...)` unconditionally unless FIRESTORE_EMULATOR_HOST was set. A custom `Settings.host` therefore could not be reached over plain HTTP, and `ssl: false` did nothing. The scheme now follows `Settings.ssl` on the non-emulator path. FIRESTORE_EMULATOR_HOST keeps precedence over both `host` and `ssl`, since it is the documented way to point the SDK at an emulator; only the custom-host path changes behaviour. The default stays `ssl: true`, so production callers are unaffected. Note that test/fixtures/helpers.dart already passed `ssl: false` next to a custom host for emulator tests, and worked only because the separate FIRESTORE_EMULATOR_HOST branch happened to hardcode HTTP. Also corrects the `Settings.ssl` doc comment, which described behaviour the field did not have. --- .../lib/src/firestore.dart | 7 +- .../lib/src/firestore_http_client.dart | 16 +++- .../test/firestore_api_host_test.dart | 88 +++++++++++++++++++ 3 files changed, 105 insertions(+), 6 deletions(-) create mode 100644 packages/google_cloud_firestore/test/firestore_api_host_test.dart diff --git a/packages/google_cloud_firestore/lib/src/firestore.dart b/packages/google_cloud_firestore/lib/src/firestore.dart index ea290f45..b64958c9 100644 --- a/packages/google_cloud_firestore/lib/src/firestore.dart +++ b/packages/google_cloud_firestore/lib/src/firestore.dart @@ -137,9 +137,12 @@ class Settings { /// set this to 'localhost:8080' (or your emulator's host:port). final String? host; - /// Whether to use SSL when connecting. + /// Whether to connect over HTTPS. /// - /// Defaults to true. Set to false when using the emulator. + /// Defaults to `true`. Set it to `false` alongside [host] to reach a plain + /// HTTP endpoint, such as a locally running emulator. + /// + /// Ignored when `FIRESTORE_EMULATOR_HOST` is set, which always uses HTTP. final bool ssl; /// The credential to use for authentication. diff --git a/packages/google_cloud_firestore/lib/src/firestore_http_client.dart b/packages/google_cloud_firestore/lib/src/firestore_http_client.dart index eb6ce9b1..b73befb6 100644 --- a/packages/google_cloud_firestore/lib/src/firestore_http_client.dart +++ b/packages/google_cloud_firestore/lib/src/firestore_http_client.dart @@ -172,8 +172,15 @@ class FirestoreHttpClient { return discovered != null ? (_cachedProjectId = discovered) : null; } - /// Gets the Firestore API host URL based on emulator configuration. - Uri get _firestoreApiHost { + /// Gets the Firestore API host URL. + /// + /// `FIRESTORE_EMULATOR_HOST` takes precedence over [Settings.host], since it + /// is the documented way to redirect the SDK at an emulator. Otherwise the + /// scheme follows [Settings.ssl], which lets a custom [Settings.host] be + /// reached over plain HTTP. + @internal + @visibleForTesting + Uri get firestoreApiHost { final emulatorHost = Environment.getFirestoreEmulatorHost( _settings.environmentOverride, ); @@ -182,7 +189,8 @@ class FirestoreHttpClient { return Uri.http(emulatorHost, '/'); } - return Uri.https(_settings.host ?? 'firestore.googleapis.com', '/'); + final host = _settings.host ?? 'firestore.googleapis.com'; + return _settings.ssl ? Uri.https(host, '/') : Uri.http(host, '/'); } /// Checks if the Firestore emulator is enabled via environment variable. @@ -242,7 +250,7 @@ class FirestoreHttpClient { Future Function(firestore_v1.Firestore api, String projectId) fn, ) => _run( (client, projectId) => fn( - firestore_v1.Firestore(client: client, endPoint: _firestoreApiHost), + firestore_v1.Firestore(client: client, endPoint: firestoreApiHost), projectId, ), ); diff --git a/packages/google_cloud_firestore/test/firestore_api_host_test.dart b/packages/google_cloud_firestore/test/firestore_api_host_test.dart new file mode 100644 index 00000000..47cf8d46 --- /dev/null +++ b/packages/google_cloud_firestore/test/firestore_api_host_test.dart @@ -0,0 +1,88 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:google_cloud_firestore/google_cloud_firestore.dart'; +import 'package:google_cloud_firestore/src/environment.dart'; +import 'package:google_cloud_firestore/src/firestore_http_client.dart'; +import 'package:test/test.dart'; + +/// Builds a client for host resolution only; no request is ever sent. +/// +/// [Settings.environmentOverride] is always supplied so the ambient +/// `FIRESTORE_EMULATOR_HOST` cannot leak into these expectations. +FirestoreHttpClient _clientFor({ + String? host, + bool ssl = true, + Map environment = const {}, +}) { + return FirestoreHttpClient( + credential: Credential.fromApplicationDefaultCredentials(), + settings: Settings( + projectId: 'test-project', + host: host, + ssl: ssl, + environmentOverride: environment, + ), + ); +} + +void main() { + group('firestoreApiHost', () { + test('defaults to the production endpoint over HTTPS', () { + final uri = _clientFor().firestoreApiHost; + + expect(uri.scheme, 'https'); + expect(uri.host, 'firestore.googleapis.com'); + }); + + test('uses HTTPS for a custom host when ssl is true', () { + final uri = _clientFor(host: 'example.test:8080').firestoreApiHost; + + expect(uri.scheme, 'https'); + expect(uri.host, 'example.test'); + expect(uri.port, 8080); + }); + + test('uses HTTP for a custom host when ssl is false', () { + final uri = _clientFor( + host: '127.0.0.1:8080', + ssl: false, + ).firestoreApiHost; + + expect(uri.scheme, 'http'); + expect(uri.host, '127.0.0.1'); + expect(uri.port, 8080); + }); + + test('uses HTTP against the default host when ssl is false', () { + final uri = _clientFor(ssl: false).firestoreApiHost; + + expect(uri.scheme, 'http'); + expect(uri.host, 'firestore.googleapis.com'); + }); + + test('FIRESTORE_EMULATOR_HOST wins over host and ssl', () { + final uri = _clientFor( + host: 'example.test:9090', + environment: const { + Environment.firestoreEmulatorHost: '127.0.0.1:8080', + }, + ).firestoreApiHost; + + expect(uri.scheme, 'http'); + expect(uri.host, '127.0.0.1'); + expect(uri.port, 8080); + }); + }); +}