firebase / flutterfire

🔥 A collection of Firebase plugins for Flutter apps.
https://firebase.google.com/docs/flutter/setup
BSD 3-Clause "New" or "Revised" License
8.46k stars 3.91k forks source link

[firebase_storage]: writeToFile neither throws exception nor returns TaskState.error in callback #12629

Closed uhbsksingh closed 1 month ago

uhbsksingh commented 1 month ago

Is there an existing issue for this?

Which plugins are affected?

Storage

Which platforms are affected?

Android, iOS

Description

The issue I'm encountering involves errors not being caught by the designated try-catch blocks during the execution of storageRef.writeToFile(localFile).

Other methods like storageRef.getData() is working as expected.

Additionally, the callback function of storageRef.writeToFile(localFile) which returns TaskSnapshot does not return TaskState.error and other states.

Reproducing the issue

Here is my code below,

Future<File?> _downloadFile(String filePath, File localFile) async {
    final storage = FirebaseStorage.instance;
    try {
      final storageRef = storage.ref().child(filePath);

      DownloadTask downloadTask = storageRef.writeToFile(localFile);
      TaskSnapshot taskSnapshot = await downloadTask;

      if (taskSnapshot.state == TaskState.success) {
        if (kDebugMode) {
          print("Download task: ${taskSnapshot.state}");
        }
        return localFile;
      } else {
        if (kDebugMode) {
          print("Download task: ${taskSnapshot.state}");
        }
        throw Exception("Download task: ${taskSnapshot.state}");
      }
    } catch (error) {
      if (kDebugMode) {
        print('Download task failed: $error');
      }
      return null;
    }
  }

Firebase Core version

2.24.2

Flutter Version

3.19.4

Relevant Log Output

No response

Flutter dependencies

Expand Flutter dependencies snippet
```yaml Dart SDK 3.3.2 Flutter SDK 3.19.4 easyims 1.0.0+7 dependencies: - authentication_repository 0.0.0 [cache equatable firebase_auth firebase_core firebase_core_platform_interface google_sign_in meta] - bloc 8.1.2 [meta] - cached_network_image 3.3.0 [cached_network_image_platform_interface cached_network_image_web flutter flutter_cache_manager octo_image] - const_date_time 1.1.0 - cupertino_icons 1.0.6 - easyims_repository 0.0.0 [cache cloud_firestore const_date_time equatable firebase_core firebase_storage flutter json_annotation] - equatable 2.0.5 [collection meta] - firebase_app_check 0.2.1+19 [firebase_app_check_platform_interface firebase_app_check_web firebase_core firebase_core_platform_interface flutter] - firebase_core 2.27.2 [firebase_core_platform_interface firebase_core_web flutter meta] - firebase_storage 11.5.6 [firebase_core firebase_core_platform_interface firebase_storage_platform_interface firebase_storage_web flutter] - flutter 0.0.0 [characters collection material_color_utilities meta vector_math sky_engine] - flutter_bloc 8.1.3 [bloc flutter provider] - flutter_social_button 1.1.4+1 [flutter font_awesome_flutter] - font_awesome_flutter 10.6.0 [flutter] - form_inputs 1.0.0+1 [formz] - formz 0.4.1 - google_fonts 6.1.0 [flutter http path_provider crypto] - image_cropper 5.0.1 [flutter image_cropper_platform_interface image_cropper_for_web] - image_picker 1.0.5 [flutter image_picker_android image_picker_for_web image_picker_ios image_picker_linux image_picker_macos image_picker_platform_interface image_picker_windows] - intl 0.19.0 [clock meta path] - intl_phone_number_input 0.7.4 [flutter libphonenumber_plugin equatable collection] - meta 1.11.0 - mobile_scanner 3.5.5 [flutter flutter_web_plugins js] - path_provider 2.1.2 [flutter path_provider_android path_provider_foundation path_provider_linux path_provider_platform_interface path_provider_windows] - qr_flutter 4.1.0 [flutter qr] - uuid 4.2.2 [crypto sprintf meta] dev dependencies: - bloc_test 9.1.5 [bloc diff_match_patch meta mocktail test] - flutter_lints 3.0.1 [lints] - flutter_test 0.0.0 [flutter test_api matcher path fake_async clock stack_trace vector_math leak_tracker_flutter_testing async boolean_selector characters collection leak_tracker leak_tracker_testing material_color_utilities meta source_span stream_channel string_scanner term_glyph vm_service] - mocktail 1.0.2 [collection matcher test_api] - very_good_analysis 5.1.0 transitive dependencies: - _fe_analyzer_shared 64.0.0 [meta] - _flutterfire_internals 1.3.27 [collection firebase_core firebase_core_platform_interface flutter meta] - analyzer 6.2.0 [_fe_analyzer_shared collection convert crypto glob meta package_config path pub_semver source_span watcher yaml] - args 2.4.2 - async 2.11.0 [collection meta] - boolean_selector 2.1.1 [source_span string_scanner] - cache 1.0.0 - cached_network_image_platform_interface 3.0.0 [flutter flutter_cache_manager] - cached_network_image_web 1.1.0 [cached_network_image_platform_interface flutter flutter_cache_manager] - characters 1.3.0 - clock 1.1.1 - cloud_firestore 4.13.6 [cloud_firestore_platform_interface cloud_firestore_web collection firebase_core firebase_core_platform_interface flutter meta] - cloud_firestore_platform_interface 6.0.10 [_flutterfire_internals collection firebase_core flutter meta plugin_platform_interface] - cloud_firestore_web 3.8.10 [_flutterfire_internals cloud_firestore_platform_interface collection firebase_core firebase_core_web flutter flutter_web_plugins js] - collection 1.18.0 - convert 3.1.1 [typed_data] - coverage 1.7.2 [args logging package_config path source_maps stack_trace vm_service] - cross_file 0.3.4+1 [meta web] - crypto 3.0.3 [typed_data] - diff_match_patch 0.4.1 - fake_async 1.3.1 [clock collection] - ffi 2.1.0 - file 7.0.0 [meta path] - file_selector_linux 0.9.2+1 [cross_file file_selector_platform_interface flutter] - file_selector_macos 0.9.3+3 [cross_file file_selector_platform_interface flutter] - file_selector_platform_interface 2.6.1 [cross_file flutter http plugin_platform_interface] - file_selector_windows 0.9.3+1 [cross_file file_selector_platform_interface flutter] - firebase_app_check_platform_interface 0.1.0+21 [_flutterfire_internals firebase_core flutter meta plugin_platform_interface] - firebase_app_check_web 0.1.1+1 [_flutterfire_internals firebase_app_check_platform_interface firebase_core firebase_core_web flutter flutter_web_plugins js web] - firebase_auth 4.15.3 [firebase_auth_platform_interface firebase_auth_web firebase_core firebase_core_platform_interface flutter meta] - firebase_auth_platform_interface 7.0.9 [_flutterfire_internals collection firebase_core flutter meta plugin_platform_interface] - firebase_auth_web 5.8.12 [firebase_auth_platform_interface firebase_core firebase_core_web flutter flutter_web_plugins http_parser js meta] - firebase_core_platform_interface 5.0.0 [collection flutter flutter_test meta plugin_platform_interface] - firebase_core_web 2.13.0 [firebase_core_platform_interface flutter flutter_web_plugins js meta web] - firebase_storage_platform_interface 5.1.3 [_flutterfire_internals collection firebase_core flutter meta plugin_platform_interface] - firebase_storage_web 3.6.17 [_flutterfire_internals async firebase_core firebase_core_web firebase_storage_platform_interface flutter flutter_web_plugins http js meta] - flutter_cache_manager 3.3.1 [clock collection file flutter http path path_provider rxdart sqflite uuid] - flutter_plugin_android_lifecycle 2.0.17 [flutter] - flutter_web_plugins 0.0.0 [flutter characters collection material_color_utilities meta vector_math] - frontend_server_client 3.2.0 [async path] - glob 2.1.2 [async collection file path string_scanner] - google_identity_services_web 0.3.1+1 [meta web] - google_sign_in 6.2.1 [flutter google_sign_in_android google_sign_in_ios google_sign_in_platform_interface google_sign_in_web] - google_sign_in_android 6.1.20 [flutter google_sign_in_platform_interface] - google_sign_in_ios 5.7.1 [flutter google_sign_in_platform_interface] - google_sign_in_platform_interface 2.4.3 [flutter plugin_platform_interface] - google_sign_in_web 0.12.4 [flutter flutter_web_plugins google_identity_services_web google_sign_in_platform_interface http web] - http 1.2.1 [async http_parser meta web] - http_multi_server 3.2.1 [async] - http_parser 4.0.2 [collection source_span string_scanner typed_data] - image_cropper_for_web 3.0.0 [flutter flutter_web_plugins image_cropper_platform_interface js] - image_cropper_platform_interface 5.0.0 [flutter plugin_platform_interface http] - image_picker_android 0.8.9+1 [flutter flutter_plugin_android_lifecycle image_picker_platform_interface] - image_picker_for_web 3.0.1 [flutter flutter_web_plugins image_picker_platform_interface mime] - image_picker_ios 0.8.9 [flutter image_picker_platform_interface] - image_picker_linux 0.2.1+1 [file_selector_linux file_selector_platform_interface flutter image_picker_platform_interface] - image_picker_macos 0.2.1+1 [file_selector_macos file_selector_platform_interface flutter image_picker_platform_interface] - image_picker_platform_interface 2.9.1 [cross_file flutter http plugin_platform_interface] - image_picker_windows 0.2.1+1 [file_selector_platform_interface file_selector_windows flutter image_picker_platform_interface] - io 1.0.4 [meta path string_scanner] - js 0.6.7 [meta] - json_annotation 4.8.1 [meta] - leak_tracker 10.0.0 [clock collection meta path vm_service] - leak_tracker_flutter_testing 2.0.1 [flutter leak_tracker leak_tracker_testing matcher meta] - leak_tracker_testing 2.0.1 [leak_tracker matcher meta] - libphonenumber_platform_interface 0.4.2 [flutter plugin_platform_interface] - libphonenumber_plugin 0.3.3 [flutter flutter_web_plugins libphonenumber_platform_interface libphonenumber_web] - libphonenumber_web 0.3.2 [flutter flutter_web_plugins js libphonenumber_platform_interface] - lints 3.0.0 - logging 1.2.0 - matcher 0.12.16+1 [async meta stack_trace term_glyph test_api] - material_color_utilities 0.8.0 [collection] - mime 1.0.4 - nested 1.0.0 [flutter] - node_preamble 2.0.2 - octo_image 2.0.0 [flutter] - package_config 2.1.0 [path] - path 1.9.0 - path_provider_android 2.2.1 [flutter path_provider_platform_interface] - path_provider_foundation 2.3.1 [flutter path_provider_platform_interface] - path_provider_linux 2.2.1 [ffi flutter path path_provider_platform_interface xdg_directories] - path_provider_platform_interface 2.1.1 [flutter platform plugin_platform_interface] - path_provider_windows 2.2.1 [ffi flutter path path_provider_platform_interface win32] - platform 3.1.3 - plugin_platform_interface 2.1.7 [meta] - pool 1.5.1 [async stack_trace] - provider 6.1.1 [collection flutter nested] - pub_semver 2.1.4 [collection meta] - qr 3.0.1 [meta] - rxdart 0.27.7 - shelf 1.4.1 [async collection http_parser path stack_trace stream_channel] - shelf_packages_handler 3.0.2 [path shelf shelf_static] - shelf_static 1.1.2 [convert http_parser mime path shelf] - shelf_web_socket 1.0.4 [shelf stream_channel web_socket_channel] - sky_engine 0.0.99 - source_map_stack_trace 2.1.1 [path source_maps stack_trace] - source_maps 0.10.12 [source_span] - source_span 1.10.0 [collection path term_glyph] - sprintf 7.0.0 - sqflite 2.3.0 [flutter sqflite_common path] - sqflite_common 2.5.0+2 [synchronized path meta] - stack_trace 1.11.1 [path] - stream_channel 2.1.2 [async] - string_scanner 1.2.0 [source_span] - synchronized 3.1.0+1 - term_glyph 1.2.1 - test 1.24.9 [analyzer async boolean_selector collection coverage http_multi_server io js matcher node_preamble package_config path pool shelf shelf_packages_handler shelf_static shelf_web_socket source_span stack_trace stream_channel test_api test_core typed_data web_socket_channel webkit_inspection_protocol yaml] - test_api 0.6.1 [async boolean_selector collection meta source_span stack_trace stream_channel string_scanner term_glyph] - test_core 0.5.9 [analyzer args async boolean_selector collection coverage frontend_server_client glob io meta package_config path pool source_map_stack_trace source_maps source_span stack_trace stream_channel test_api vm_service yaml] - typed_data 1.3.2 [collection] - vector_math 2.1.4 - vm_service 13.0.0 - watcher 1.1.0 [async path] - web 0.5.1 - web_socket_channel 2.4.0 [async crypto stream_channel] - webkit_inspection_protocol 1.2.1 [logging] - win32 5.1.1 [ffi] - xdg_directories 1.0.3 [meta path] - yaml 3.1.2 [collection source_span string_scanner] ```

Additional context and comments

No response

russellwheatley commented 1 month ago

Hey @uhbsksingh - is this reproducible every time?

uhbsksingh commented 1 month ago

Hey @uhbsksingh - is this reproducible every time?

Yes it is.

I tried using Stream instead of Future to catch errors. I could intercept the error on onError callback of stream listener. However, the error is still not caught on try catch. And, TaskState.error is still not available.

This is my code using Stream.


Stream<File?> _streamFile(String filePath, File localFile) async* {
    final storage = FirebaseStorage.instance;
    try {
      final storageRef = storage.ref().child(filePath);
      DownloadTask downloadTask = storageRef.writeToFile(localFile);

      final completer = Completer<File?>();

      downloadTask.snapshotEvents.listen(
        (taskSnapshot) {
          switch (taskSnapshot.state) {
            case TaskState.running:
              break;
            case TaskState.paused:
              break;
            case TaskState.success:
              completer.complete(localFile);
              break;
            case TaskState.canceled:
              break;
            case TaskState.error:
              completer.completeError(
                  Exception('Download task failed: ${taskSnapshot.metadata}'));
              break;
          }
        },
        onError: (error) => completer.completeError(
          Exception('Download task failed: $error'),
        ),
      );
      yield* Stream.fromFuture(completer.future);
    } catch (error) {
      if (kDebugMode) {
        print('Download task failed: $error');
      }
      throw Exception('Download task failed: $error');
    }
  }
russellwheatley commented 1 month ago

@uhbsksingh - I just ran your Future code on my android device:

final storage = FirebaseStorage.instance;

          final directory = await getApplicationDocumentsDirectory();

          final directoryPath = directory.path;

          final file = io.File('$directoryPath/my_image.jpg');
          TaskSnapshot? taskSnapshot;
          try {
            final storageRef = storage.ref().child('restricted').child('some-image.jpg');

            DownloadTask downloadTask = storageRef.writeToFile(file);
            taskSnapshot = await downloadTask;

            if (taskSnapshot.state == TaskState.success) {
              if (kDebugMode) {
                print("Download task: ${taskSnapshot.state}");
              }
              return;
            } else {
              if (kDebugMode) {
                print("Download task: ${taskSnapshot.state}");
              }
              throw Exception("Download task: ${taskSnapshot.state}");
            }
          } catch (error) {
            if (kDebugMode) {
              print('Download task failed: $error');
            }
            return;
          }

I used a path that is restricted (i.e. no read/write operations allowed), and I got the exception in the catch block:

I/flutter (28714): Download task failed: [firebase_storage/unauthorized] User is not authorized to perform the desired action.

Could you tell me what circumstances the exception fails to throw?

uhbsksingh commented 1 month ago

While using await

Future<File?> _downloadFile(String filePath, File localFile) async {
    final storage = FirebaseStorage.instance;
    TaskSnapshot? taskSnapshot;
    try {
      final storageRef = storage.ref().child(filePath);
      DownloadTask downloadTask = storageRef.writeToFile(localFile);
      taskSnapshot = await downloadTask;

      switch (taskSnapshot.state) {
        case TaskState.success:
          final totalBytes = taskSnapshot.totalBytes;
          final bytesTransferred = taskSnapshot.bytesTransferred;
          if (totalBytes == bytesTransferred) {
            return localFile;
          }
          break;
        case TaskState.canceled:
        case TaskState.paused:
        case TaskState.running:
        case TaskState.error:
          throw Exception("Download failed");
      }

      return null;
    } catch (error) {
      if (kDebugMode) {
        print('Download task failed: $error');
      }
      throw Exception('Download task failed: $error');
    }
  }

I get this in the debug console

E/StorageException( 8444): StorageException has occurred.
E/StorageException( 8444): Object does not exist at location.
E/StorageException( 8444):  Code: -13010 HttpResult: 404
E/StorageException( 8444): StorageException has occurred.
E/StorageException( 8444): Object does not exist at location.
E/StorageException( 8444):  Code: -13010 HttpResult: 404
E/flutter ( 8444): [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: [firebase_storage/firebase_storage] Object does not exist at location.
E/flutter ( 8444): 

So, I tried await for (TaskSnapshot taskSnapshot in downloadTask.snapshotEvents).

Future<File?> _downloadFile(String filePath, File localFile) async {
    final storage = FirebaseStorage.instance;
    try {
      final storageRef = storage.ref().child(filePath);
      DownloadTask downloadTask = storageRef.writeToFile(localFile);

      await for (TaskSnapshot taskSnapshot in downloadTask.snapshotEvents) {
        switch (taskSnapshot.state) {
          case TaskState.success:
            final totalBytes = taskSnapshot.totalBytes;
            final bytesTransferred = taskSnapshot.bytesTransferred;
            if (totalBytes == bytesTransferred) {
              return localFile;
            }
            break;
          case TaskState.canceled:
          case TaskState.paused:
          case TaskState.running:
          case TaskState.error:
            return Future<File?>.error("Download failed");
        }
      }
      return null;
    } catch (exception) {
      return Future<File?>.error(exception);
    }
  }

Now the log is as follows:

E/StorageException( 8444): StorageException has occurred.
E/StorageException( 8444): Object does not exist at location.
E/StorageException( 8444):  Code: -13010 HttpResult: 404
E/StorageException( 8444): StorageException has occurred.
E/StorageException( 8444): Object does not exist at location.
E/StorageException( 8444):  Code: -13010 HttpResult: 404
E/flutter ( 8444): [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: Download failed
E/flutter ( 8444): 
uhbsksingh commented 1 month ago

UPDATE

I had to use if (taskSnapshot.state == TaskState.success) instead of switch (taskSnapshot.state) because code inside case TaskState.error: was being executed even when there was no errors.

case TaskState.error: return Future<File?>.error("Download failed");

await for (TaskSnapshot taskSnapshot in downloadTask.snapshotEvents) {
        if (taskSnapshot.state == TaskState.success) {
          final totalBytes = taskSnapshot.totalBytes;
          final bytesTransferred = taskSnapshot.bytesTransferred;
          if (totalBytes == bytesTransferred) {
            return localFile;
          }
        }
       }
       return null;
russellwheatley commented 1 month ago
Future<File?> _downloadFile(String filePath, File localFile) async {
    final storage = FirebaseStorage.instance;
    TaskSnapshot? taskSnapshot;
    try {
      final storageRef = storage.ref().child(filePath);
      DownloadTask downloadTask = storageRef.writeToFile(localFile);
      taskSnapshot = await downloadTask;

      switch (taskSnapshot.state) {
        case TaskState.success:
          final totalBytes = taskSnapshot.totalBytes;
          final bytesTransferred = taskSnapshot.bytesTransferred;
          if (totalBytes == bytesTransferred) {
            return localFile;
          }
          break;
        case TaskState.canceled:
        case TaskState.paused:
        case TaskState.running:
        case TaskState.error:
          throw Exception("Download failed");
      }

      return null;
    } catch (error) {
      if (kDebugMode) {
        print('Download task failed: $error');
      }
      throw Exception('Download task failed: $error');
    }
  }

This will fail. taskSnapshot will be null because the downloadTask failed. I would refactor this code, first by removing the switch statement completely. If the task fails, it will throw an exception and it will be correctly handled.

uhbsksingh commented 1 month ago

I understand. I tried this.


Future<File?> _downloadFile(String filePath, File localFile) async {
    final storage = FirebaseStorage.instance;
    try {
      final storageRef = storage.ref().child(filePath);
      DownloadTask downloadTask = storageRef.writeToFile(localFile);

      await for (TaskSnapshot taskSnapshot in downloadTask.snapshotEvents) {
        if (taskSnapshot.state == TaskState.success) {
          final totalBytes = taskSnapshot.totalBytes;
          final bytesTransferred = taskSnapshot.bytesTransferred;
          if (totalBytes == bytesTransferred) {
            return localFile;
          }
        }
      }
      return null;
    } catch (exception) {
      return Future<File?>.error(exception);
    }
  }
russellwheatley commented 1 month ago

@uhbsksingh - did it correctly throw the exception for you?

uhbsksingh commented 1 month ago

@uhbsksingh - did it correctly throw the exception for you?

Yes it did. Thank you for your time and effort.

russellwheatley commented 1 month ago

Thank you for the update. Closing out as the issue has been resolved.