firebase-ios-sdk: Cannot unset/remove customMetadata keys on StorageReference objects - regression from v8

Step 0: Are you in the right place?

  • For issues or feature requests related to the code in this repository file a GitHub issue.
    • If this is a feature request please use the Feature Request template.
  • For general technical questions, post a question on StackOverflow with the firebase tag.
  • For general (non-iOS) Firebase discussion, use the firebase-talk google group.
  • For backend issues, console issues, and other non-SDK help that does not fall under one of the above categories, reach out to Firebase Support.
  • Once you’ve read this section and determined that your issue is appropriate for this repository, please delete this section.

[REQUIRED] Step 1: Describe your environment

  • Xcode version: 13.4
  • Firebase SDK version: 9.1.0
  • Installation method: CocoaPods
  • Firebase Component: Storage
  • Target platform(s): iOS

[REQUIRED] Step 2: Describe the problem

I’m forward-porting react-native-firebase to the v9+ SDK release here, and I’ve run into a problem with Storage.

Specifically, we attempt to mirror the firebase-js-sdk API surface area to react-native library consumers (for interoperability with web code) but we call down to underlying firebase-ios-sdk and firebase-android-sdk

firebase-js-sdk documents that to remove a customMetadata key/value pair on a StorageReference, you send in a null value to the updateMetadata call.

This worked fine with firebase-ios-sdk v8 and lower, but now it appears there are some Swift-y types / casts happening, and nulls will crash an app

I will attempt to highlight the important frames 7-16 from the boilerplate with a lot of newlines:


    updateMetadata
16:13:27.013 detox[39601] ERROR: [WS_ERROR] The app has crashed, see the details below:

Signal 6 was raised
(
        0   Detox                               0x0000000102125c15 +[NSThread(DetoxUtils) dtx_demangledCallStackSymbols] + 37
        1   Detox                               0x0000000102128670 __DTXHandleCrash + 464
        2   Detox                               0x0000000102128db1 __DTXHandleSignal + 59
        3   libsystem_platform.dylib            0x00007fff701bddfd _sigtramp + 29
        4   ???                                 0x00007fc7a2723a60 0x0 + 140495400614496
        5   libsystem_c.dylib                   0x00007fff2010b6b7 abort + 130
        6   libswiftCore.dylib                  0x00007fff30c9dc0c swift::fatalError(unsigned int, char const*, ...) + 252



        7   libswiftCore.dylib                  0x00007fff30c95f17 swift::swift_dynamicCastFailure(void const*, char const*, void const*, char const*, char const*) + 71
        8   libswiftCore.dylib                  0x00007fff30c95f8a swift::swift_dynamicCastFailure(swift::TargetMetadata<swift::InProcess> const*, swift::TargetMetadata<swift::InProcess> const*, char const*) + 106
        9   libswiftCore.dylib                  0x00007fff30c9a049 swift_dynamicCast + 249
        10  libswiftFoundation.dylib            0x00007fff599ae17a closure #2 (Swift.UnsafeMutableBufferPointer<A>, Swift.UnsafeMutableBufferPointer<B>) -> Swift.Int in static (extension in Foundation):Swift.Dictionary._forceBridgeFromObjectiveC(_: __C.NSDictionary, result: inout Swift.Optional<Swift.Dictionary<A, B>>) -> () + 762
        11  libswiftFoundation.dylib            0x00007fff599ae1f7 partial apply forwarder for closure #2 (Swift.UnsafeMutableBufferPointer<A>, Swift.UnsafeMutableBufferPointer<B>) -> Swift.Int in static (extension in Foundation):Swift.Dictionary._forceBridgeFromObjectiveC(_: __C.NSDictionary, result: inout Swift.Optional<Swift.Dictionary<A, B>>) -> () + 39
        12  libswiftFoundation.dylib            0x00007fff599b2403 Swift._NativeDictionary.init(_unsafeUninitializedCapacity: Swift.Int, allowingDuplicates: Swift.Bool, initializingWith: (Swift.UnsafeMutableBufferPointer<A>, Swift.UnsafeMutableBufferPointer<B>) -> Swift.Int) -> Swift._NativeDictionary<A, B> + 195
        13  libswiftFoundation.dylib            0x00007fff599acf90 static (extension in Foundation):Swift.Dictionary._unconditionallyBridgeFromObjectiveC(Swift.Optional<__C.NSDictionary>) -> Swift.Dictionary<A, B> + 448
        14  FirebaseStorage                     0x0000000102be888d @objc FirebaseStorage.StorageMetadata.customMetadata.setter : Swift.Optional<Swift.Dictionary<Swift.String, Swift.String>> + 93
        15  testing                             0x0000000101995c8a +[RNFBStorageCommon buildMetadataFromMap:] + 170
        16  testing                             0x00000001019970a2 -[RNFBStorageModule updateMetadata:::::] + 242




        17  CoreFoundation                      0x00007fff2040bfdc __invoking___ + 140
        18  CoreFoundation                      0x00007fff2040934f -[NSInvocation invoke] + 305
        19  CoreFoundation                      0x00007fff204095e2 -[NSInvocation invokeWithTarget:] + 70
        20  React                               0x00000001038bddb6 -[RCTModuleMethod invokeWithBridge:module:arguments:] + 2534
        21  React                               0x00000001038c227a facebook::react::invokeInner(RCTBridge*, RCTModuleData*, unsigned int, folly::dynamic const&, int, (anonymous namespace)::SchedulingContext) + 1402
        22  React                               0x00000001038c1b2c facebook::react::RCTNativeModule::invoke(unsigned int, folly::dynamic&&, int)::$_0::operator()() const + 156
        23  React                               0x00000001038c1a89 invocation function for block in facebook::react::RCTNativeModule::invoke(unsigned int, folly::dynamic&&, int) + 25
        24  DetoxSync                           0x0000000103586b27 ____detox_sync_dispatch_wrapper_block_invoke + 23
        25  libdispatch.dylib                   0x00007fff201148e4 _dispatch_call_block_and_release + 12
        26  libdispatch.dylib                   0x00007fff20115b25 _dispatch_client_callout + 8
        27  libdispatch.dylib                   0x00007fff2011c0df _dispatch_lane_serial_drain + 753
        28  libdispatch.dylib                   0x00007fff2011ccc1 _dispatch_lane_invoke + 400
        29  libdispatch.dylib                   0x00007fff2012797b _dispatch_workloop_worker_thread + 779
        30  libsystem_pthread.dylib             0x00007fff701c7fd0 _pthread_wqthread + 326
        31  libsystem_pthread.dylib             0x00007fff701c6f57 start_wqthread + 15
)

Steps to reproduce:

1- create a StorageReference with customMetadata = { removeMe: 'please'} 2- using that reference call updateMetadata({ removeMe: null }) 3- now the reference should have customMetadata with removeMe == undefined (that is, it should be been removed)

Relevant Code:

Here is our test that used to run okay on iOS (it had a failure on android we’re still pursuing, but iOS was working)

https://github.com/invertase/react-native-firebase/blob/52715959a375e8babd129baec38356b78ce80ab6/packages/storage/e2e/StorageReference.e2e.js#L403-L428

    // FIXME not working against android on emulator? it returns the string 'null' for the cleared customMetadata value
    it('should set removed customMetadata properties to null', async function () {
      if (device.getPlatform() === 'ios') {
        const storageReference = firebase.storage().ref(WRITE_ONLY_NAME);
        const metadata = await storageReference.updateMetadata({
          contentType: 'text/plain',
          customMetadata: {
            removeMe: 'please',
          },
        });


        metadata.customMetadata.removeMe.should.equal('please');


        const metadataAfterRemove = await storageReference.updateMetadata({
          contentType: 'text/plain',
          customMetadata: {
            removeMe: null,
          },
        });


        // FIXME this is failing the part that fails
        should.equal(metadataAfterRemove.customMetadata.removeMe, undefined);
      } else {
        this.skip();
      }
    });

Note that the documentation for writable metadata properties indicates you may delete them passing the empty string: https://firebase.google.com/docs/storage/ios/file-metadata#update_file_metadata

On Android (and web) writable metadata is removed by passing null https://firebase.google.com/docs/storage/android/file-metadata#update_file_metadata https://firebase.google.com/docs/storage/web/file-metadata#update_file_metadata

No mention is made on how to remove customMetadata keys.

If you attempt sending an empty string in for a key with customMetadata, that just results in the value being the empty string, the value is not removed, so there may be something to think about for API consistency.

But in the past at least, sending in a null for a customMetadata key removed it fine.

Here’s the iOS code that does the update:

https://github.com/invertase/react-native-firebase/blob/52715959a375e8babd129baec38356b78ce80ab6/packages/storage/ios/RNFBStorage/RNFBStorageModule.m#L145-L156

  FIRStorageReference *storageReference = [self getReferenceFromUrl:url app:firebaseApp];
  FIRStorageMetadata *storageMetadata = [RNFBStorageCommon buildMetadataFromMap:metadata];


  [storageReference
      updateMetadata:storageMetadata
          completion:^(FIRStorageMetadata *_Nullable metadata, NSError *_Nullable error) {
            if (error != nil) {
              [self promiseRejectStorageException:reject error:error];
            } else {
              resolve([RNFBStorageCommon metadataToDict:metadata]);
            }
          }];

…and the buildMetadataFromMap method is

https://github.com/invertase/react-native-firebase/blob/52715959a375e8babd129baec38356b78ce80ab6/packages/storage/ios/RNFBStorage/RNFBStorageCommon.m#L395-L399

+ (FIRStorageMetadata *)buildMetadataFromMap:(NSDictionary *)metadata {
  FIRStorageMetadata *storageMetadata = [[FIRStorageMetadata alloc] initWithDictionary:metadata];
  storageMetadata.customMetadata = [metadata[@"customMetadata"] mutableCopy];
  return storageMetadata;
}

That storageMetadata.customMetadata = line is the crash, when the FIRStorageMetadata is initialized with an NSDictionary that contains nulls

I understand that nil NSNull null etc can be bothersome in Objective-C and for interop, and who knows what the react-native bridge is translating ‘null’ from javascript in to at the objective-c layer, right? so I chucked this line in to the code:

  NSLog(@"FIRStorageMetadata removeMe key %@", metadata[@"customMetadata"]);

And it spits out this, which seems to indicate it is an NSNull if I understand correctly.

2022-05-26 17:23:08.876 Df testing[44279:500cc] FIRStorageMetadata removeMe key {
    removeMe = "<null>";
}

I tried changing the swfit StorageMetadata.swift file from this repo a bit, playing with type signatures etc but if my objective-c skills are lacking (they are) my Swift skills are almost non-existent, and making interoperable optional/nullable types is beyond me unfortunately

Help?

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 34 (34 by maintainers)

Commits related to this issue

Most upvoted comments

Going to close now since no identified next steps, but feel free to continue the conversation here.

Yep, needed to set md5hash for the assertMetadata validation. The contentEncoding updates are also kind of weird, but consistent between v8 and v9. These tests run against a real project in the cloud.

Sorry this has hung out there - I’m still actively working on the task but you are right there may not be anything actionable for firebase-ios-sdk as it’s conforming with published types now. Just firebase-android-sdk apparent behavior difference and react-native-firebase implementation shortcomings is what it looks like to me as well

Thanks for that @paulb777 - no eureka moment but for me anyway, knowing someone else has seen it work gives me the will to carry on when otherwise stumped. I’ll find the issue, and I bet it will be connected with why I can clear (most) data now but my updates aren’t taking effect.

I think the remaining items are

  • snippet may be fixed but docs have text stating empty string for clearing, not nil
  • the new customMetadata official policy appears to be that the map is updated all at once or not at all on ios which is a platform difference with android

the update all at once policy is a little uncomfortable for an implementer because we do not always have all of the metadata so now we need to fetch / process / remove keys as a facade for the ios platform, but it’s technically possible at least

Yep, make a mutableCopy of the customMetadata and use removeObjectForKey: and then update it.

I’m able to reproduce the 9.x behavior change with the following test:

Screen Shot 2022-05-27 at 9 46 10 AM

My initial assessment is that even though this is an inadvertent API breakage, 9.x is actually fixing a bug. customMetadata is documented and implemented to be a Dictionary with String keys and String values. It’s not valid to store an NSNull as a value and 8.x was only allowing it because of Objective-C’s sloppy type handling.

customMetadata should be updated just like any other Swift or Objective-C Dictionary that specifies its key and value types. Is it feasible to update the react-native bridge accordingly?

cc: @tonyjhuang for any additional thoughts about this assessment or the Android/web comments in the OP.