1313// limitations under the License.
1414
1515import 'dart:async' ;
16+ import 'dart:convert' ;
1617
1718import 'package:google_cloud/constants.dart' as google_cloud;
1819import 'package:google_cloud/google_cloud.dart' as google_cloud;
@@ -23,8 +24,89 @@ import 'package:meta/meta.dart';
2324
2425import '../google_cloud_firestore.dart' ;
2526import 'environment.dart' ;
27+ import 'firestore.dart'
28+ show Serializer, kInfinitySentinel, kNaNSentinel, kNegInfinitySentinel;
2629import 'firestore_exception.dart' ;
2730
31+ // Matches a complete Firestore Value JSON object whose doubleValue is one of
32+ // the three special IEEE 754 strings the REST API emits. Anchoring on the
33+ // surrounding braces prevents false-positive matches inside user string fields
34+ // that happen to contain the text `"doubleValue":"Infinity"`.
35+ final _specialDoublePattern = RegExp (
36+ r'\{\s*"doubleValue"\s*:\s*"(Infinity|-Infinity|NaN)"\s*\}' ,
37+ );
38+
39+ /// HTTP client wrapper that rewrites special IEEE 754 double values in
40+ /// Firestore REST API responses before the googleapis library parses them.
41+ ///
42+ /// The Firestore REST API encodes [double.infinity] , [double.negativeInfinity] ,
43+ /// and [double.nan] as the JSON strings `"Infinity"` , `"-Infinity"` , and
44+ /// `"NaN"` respectively (since those values are not representable in standard
45+ /// JSON). The googleapis-generated [firestore_v1.Value.fromJson] does a hard `as num` cast
46+ /// on the `doubleValue` field and therefore throws when it encounters a string.
47+ ///
48+ /// This client intercepts every response body and replaces those patterns with
49+ /// a `stringValue` sentinel that [Serializer.decodeValue] understands.
50+ /// The sentinel strings are defined alongside [Serializer.decodeValue] in
51+ /// `serializer.dart` .
52+ class _SpecialDoubleClient extends BaseClient {
53+ _SpecialDoubleClient (this ._inner);
54+
55+ final Client _inner;
56+
57+ @override
58+ Future <StreamedResponse > send (BaseRequest request) async {
59+ final response = await _inner.send (request);
60+
61+ final contentType = response.headers['content-type' ] ?? '' ;
62+ if (! contentType.contains ('application/json' )) return response;
63+
64+ final body = await response.stream.bytesToString ();
65+ if (! body.contains ('"doubleValue"' )) return _rebuild (response, body);
66+
67+ final patched = body.replaceAllMapped (_specialDoublePattern, (m) {
68+ final sentinel = switch (m.group (1 )) {
69+ 'Infinity' => kInfinitySentinel,
70+ '-Infinity' => kNegInfinitySentinel,
71+ _ => kNaNSentinel, // 'NaN'
72+ };
73+ return '{"stringValue":"$sentinel "}' ;
74+ });
75+
76+ return _rebuild (response, patched);
77+ }
78+
79+ StreamedResponse _rebuild (StreamedResponse original, String body) {
80+ final bytes = utf8.encode (body);
81+ final headers = Map <String , String >.from (original.headers)
82+ ..['content-length' ] = bytes.length.toString ();
83+ return StreamedResponse (
84+ Stream .value (bytes),
85+ original.statusCode,
86+ contentLength: bytes.length,
87+ headers: headers,
88+ request: original.request,
89+ reasonPhrase: original.reasonPhrase,
90+ );
91+ }
92+ }
93+
94+ /// An [googleapis_auth.AuthClient] wrapper that applies [_SpecialDoubleClient]
95+ /// response rewriting while preserving the [credentials] required by the
96+ /// googleapis request pipeline.
97+ class _SpecialDoubleAuthClient extends _SpecialDoubleClient
98+ implements googleapis_auth.AuthClient {
99+ _SpecialDoubleAuthClient (this ._authInner) : super (_authInner);
100+
101+ final googleapis_auth.AuthClient _authInner;
102+
103+ @override
104+ googleapis_auth.AccessCredentials get credentials => _authInner.credentials;
105+
106+ @override
107+ void close () => _authInner.close ();
108+ }
109+
28110/// Internal HTTP request implementation that wraps a stream.
29111///
30112/// This is used by [EmulatorClient] to create modified requests with
@@ -114,22 +196,28 @@ class FirestoreHttpClient {
114196 /// Creates the appropriate HTTP client based on emulator configuration.
115197 Future <googleapis_auth.AuthClient > _createClient () async {
116198 if (_isUsingEmulator) {
117- // Emulator: Create unauthenticated client
118- return EmulatorClient (Client ());
199+ // Emulator: Create unauthenticated client, wrapped to rewrite special
200+ // doubles before EmulatorClient adds auth headers.
201+ return EmulatorClient (_SpecialDoubleClient (Client ()));
119202 }
120203
121- // Production: Create authenticated client
204+ // Production: Create authenticated client, then wrap to rewrite special
205+ // doubles in responses before googleapis parses them.
122206 final serviceAccountCreds = credential.serviceAccountCredentials;
123207 if (serviceAccountCreds != null ) {
124- return googleapis_auth.clientViaServiceAccount (serviceAccountCreds, [
125- firestore_v1.FirestoreApi .cloudPlatformScope,
126- ]);
208+ final authClient = await googleapis_auth.clientViaServiceAccount (
209+ serviceAccountCreds,
210+ [firestore_v1.FirestoreApi .cloudPlatformScope],
211+ );
212+ return _SpecialDoubleAuthClient (authClient);
127213 }
128214
129215 // Fall back to Application Default Credentials
130- return googleapis_auth.clientViaApplicationDefaultCredentials (
131- scopes: [firestore_v1.FirestoreApi .cloudPlatformScope],
132- );
216+ final authClient = await googleapis_auth
217+ .clientViaApplicationDefaultCredentials (
218+ scopes: [firestore_v1.FirestoreApi .cloudPlatformScope],
219+ );
220+ return _SpecialDoubleAuthClient (authClient);
133221 }
134222
135223 Future <R > _run <R >(
0 commit comments