Is there an existing issue for this?
Which plugins are affected?
Cloud Functions
Which platforms are affected?
Web
Description
On web, HttpsCallable.stream() yields two kinds of events: intermediate Chunks (from response.sendChunk(...) on the server) and a terminal Result (the value the server function returns). Chunks are correctly converted into plain Dart types (Map<String, dynamic>, List, primitives), but the terminal Result's .data is handed back as the raw JSObject / JSAny returned by streamResult.data.toDart, skipping the _dartify helper that is applied to chunks.
Consequence: downstream code that does finalResult is Map<String, dynamic> (or the many common variants such as finalResult as Map, (finalResult as Map)['foo'], jsonEncode(finalResult), etc.) fails on web even though the exact same code works on Android / iOS / macOS, where the native channel already delivers a Dart map. Plugin users end up either writing manual JS-interop dartify() calls in every callsite, or seeing silent empty-map fallbacks (which is how we noticed — a boolean field in the response came back as null on web only, and the client logic branched into an error path).
The fix should mirror what the plugin already does for chunks: run the _dartify helper on the final result before yielding it.
Offending code
packages/cloud_functions/cloud_functions_web/lib/interop/functions.dart, lines 95–107 (same on master and on the released cloud_functions_web 5.1.5):
await for (final value in streamResult.stream.asStream()) {
// ignore: invalid_runtime_check_with_js_interop_types
final message = value is JSObject
? HttpsCallableStreamResult.getInstance(
value as functions_interop.HttpsStreamIterableResult,
).data // <-- goes through _dartify via HttpsCallableStreamResult._fromJsObject
: value;
yield {'message': message};
}
final result = await streamResult.data.toDart;
yield {'result': result}; // <-- NOT dartified; raw JSAny
Note that HttpsCallableResult._fromJsObject (used by the non-streaming .call() path) does run _dartify(jsObject.data), so non-streaming callables are unaffected — it is specifically the streaming terminal result that is broken.
Suggested fix
final result = await streamResult.data.toDart;
yield {'result': _dartify(result)};
Happy to open a PR if helpful.
Reproducing the issue
Minimal repro:
Cloud Function (TypeScript, firebase-functions v5+):
import { onCall } from 'firebase-functions/https'
export const pingStream = onCall(async (_req, response) => {
await response?.sendChunk({ phase: 'working' })
return { success: true, message: 'done' }
})
Flutter client:
final callable = FirebaseFunctions.instance.httpsCallable('pingStream');
await for (final event in callable.stream<dynamic, dynamic>()) {
switch (event) {
case Chunk(:final partialData):
print('chunk: ${partialData.runtimeType} / $partialData');
case Result(:final result):
final data = result.data;
print('result: ${data.runtimeType} / $data');
print('is Map<String, dynamic>: ${data is Map<String, dynamic>}');
print('is Map: ${data is Map}');
}
}
Observed on web (Chrome, Flutter 3.41.6, cloud_functions 6.2.0):
chunk: _Map<String, dynamic> / {phase: working}
result: JSObject / [object Object]
is Map<String, dynamic>: false
is Map: false
Observed on Android / iOS / macOS (same app, same code):
chunk: _Map<String, dynamic> / {phase: working}
result: _Map<String, dynamic> / {success: true, message: done}
is Map<String, dynamic>: true
is Map: true
Firebase Core version
4.7.0
Flutter Version
3.41.6
Relevant Log Output
Flutter dependencies
Expand Flutter dependencies snippet
- cloud_functions 6.2.0 [cloud_functions_platform_interface cloud_functions_web firebase_core firebase_core_platform_interface flutter]
- cloud_functions_platform_interface 5.8.12
- cloud_functions_web 5.1.5
- firebase_core 4.7.0 [firebase_core_platform_interface firebase_core_web flutter meta]
Additional context and comments
Current workaround on the client side is to conditionally call .dartify() on the result when running on web:
// conditional import — native stub returns `value` unchanged, web variant
// imports `dart:js_interop` and calls `(value as JSAny).dartify()`.
import 'dartify_stub.dart' if (dart.library.js_interop) 'dartify_web.dart';
case Result(:final result):
yield CloudStreamResult(dartifyCloudFunctionResult(result.data));
…but this really belongs inside cloud_functions_web so every streaming-callable consumer doesn't have to reinvent it.
Is there an existing issue for this?
Which plugins are affected?
Cloud Functions
Which platforms are affected?
Web
Description
On web,
HttpsCallable.stream()yields two kinds of events: intermediateChunks (fromresponse.sendChunk(...)on the server) and a terminalResult(the value the server functionreturns). Chunks are correctly converted into plain Dart types (Map<String, dynamic>,List, primitives), but the terminalResult's.datais handed back as the rawJSObject/JSAnyreturned bystreamResult.data.toDart, skipping the_dartifyhelper that is applied to chunks.Consequence: downstream code that does
finalResult is Map<String, dynamic>(or the many common variants such asfinalResult as Map,(finalResult as Map)['foo'],jsonEncode(finalResult), etc.) fails on web even though the exact same code works on Android / iOS / macOS, where the native channel already delivers a Dart map. Plugin users end up either writing manual JS-interopdartify()calls in every callsite, or seeing silent empty-map fallbacks (which is how we noticed — a boolean field in the response came back asnullon web only, and the client logic branched into an error path).The fix should mirror what the plugin already does for chunks: run the
_dartifyhelper on the final result before yielding it.Offending code
packages/cloud_functions/cloud_functions_web/lib/interop/functions.dart, lines 95–107 (same onmasterand on the releasedcloud_functions_web 5.1.5):Note that
HttpsCallableResult._fromJsObject(used by the non-streaming.call()path) does run_dartify(jsObject.data), so non-streaming callables are unaffected — it is specifically the streaming terminal result that is broken.Suggested fix
Happy to open a PR if helpful.
Reproducing the issue
Minimal repro:
Cloud Function (TypeScript, firebase-functions v5+):
Flutter client:
Observed on web (Chrome, Flutter 3.41.6, cloud_functions 6.2.0):
Observed on Android / iOS / macOS (same app, same code):
Firebase Core version
4.7.0
Flutter Version
3.41.6
Relevant Log Output
Flutter dependencies
Expand
Flutter dependenciessnippetAdditional context and comments
Current workaround on the client side is to conditionally call
.dartify()on the result when running on web:…but this really belongs inside
cloud_functions_webso every streaming-callable consumer doesn't have to reinvent it.