Repository: firebase/firebase-ios-sdk (language: cpp)

Issue:
When the remoteConfig load fails on the first call, the SDK returns the code 8002 (throttled) for the other calls
### **[REQUIRED] Step 1: Describe your environment**

Xcode version: Xcode 12.0.1
Firebase SDK version: 6.33.0
Firebase Component: Remote Config
Component version: 4.9.0
Installation method: CocoaPods
Platform: iOS

### **[REQUIRED] Step 2: Describe the problem**

When I call the fetchAndActivate method and the phone has no network then I reactivate the network to restart the request I get an error 8002 (throttled) while I have no problem when on the first call the network is present.

When the problem occurs I have to wait several minutes for the request to work again.

This is problematic because I want when the minimumFetchInterval is reached the request to the Firebase servers is a success to allow access to my application.

How can I solve this problem?

Here is my code:

```
let settings = RemoteConfigSettings()
settings.fetchTimeout = 30
settings.minimumFetchInterval = 60*60
#if DEBUG
settings.minimumFetchInterval = 0
#endif
remoteConfigService.configSettings = settings

remoteConfigService.fetchAndActivate(completionHandler: { (status, error) -> Void in
	if status != .error {
		// Load my config
	} else {
	 	// Manage error
	}
})
```

Thank you in advance.


Discussion / hints:
I found a few problems with this issue:
  * I couldn't figure out how to label this issue, so I've labeled it for a human to triage. Hang tight.
  * This issue does not seem to follow the issue template. Make sure you provide all the required information.
@karenyz can you take a look at this issue?
This might be related to the internal b/184356915

Fix patch:
diff --git a/FirebaseRemoteConfig/CHANGELOG.md b/FirebaseRemoteConfig/CHANGELOG.md
index 82a85f3d52d..6c3996b0d99 100644
--- a/FirebaseRemoteConfig/CHANGELOG.md
+++ b/FirebaseRemoteConfig/CHANGELOG.md
@@ -1,3 +1,6 @@
+# Unreleased
+- [fixed] Fixed throttling issue when fetch fails due to no network. (#6628)
+
 # v7.10.0
 - [changed] Throw exception if projectID is missing from FirebaseOptions. (#7725)
 
diff --git a/FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h b/FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h
index 0d077c5550a..ffae0317c6f 100644
--- a/FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h
+++ b/FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h
@@ -114,6 +114,10 @@
 /// @param fetchSuccess True if fetch was successful.
 - (void)updateMetadataWithFetchSuccessStatus:(BOOL)fetchSuccess;
 
+/// Increases the throttling time. Should only be called if the fetch error indicates a server
+/// issue.
+- (void)updateExponentialBackoffTime;
+
 /// Returns true if we are in exponential backoff mode and it is not yet the next request time.
 - (BOOL)shouldThrottle;
 
diff --git a/FirebaseRemoteConfig/Sources/RCNConfigFetch.m b/FirebaseRemoteConfig/Sources/RCNConfigFetch.m
index 666e818d985..f991662a0b8 100644
--- a/FirebaseRemoteConfig/Sources/RCNConfigFetch.m
+++ b/FirebaseRemoteConfig/Sources/RCNConfigFetch.m
@@ -387,6 +387,7 @@ - (void)fetchWithUserProperties:(NSDictionary *)userProperties
               statusCode == kRCNFetchResponseHTTPStatusCodeInternalError ||
               statusCode == kRCNFetchResponseHTTPStatusCodeServiceUnavailable ||
               statusCode == kRCNFetchResponseHTTPStatusCodeGatewayTimeout) {
+            [strongSelf->_settings updateExponentialBackoffTime];
             if ([strongSelf->_settings shouldThrottle]) {
               // Must set lastFetchStatus before FailReason.
               strongSelf->_settings.lastFetchStatus = FIRRemoteConfigFetchStatusThrottled;
@@ -404,8 +405,8 @@ - (void)fetchWithUserProperties:(NSDictionary *)userProperties
                                                 withStatus:strongSelf->_settings.lastFetchStatus
                                                  withError:error];
             }
-          }  // Response error code 429, 500, 503
-        }    // StatusCode != kRCNFetchResponseHTTPStatusCodeOK
+          }
+        }
         // Return back the received error.
         // Must set lastFetchStatus before setting Fetch Error.
         strongSelf->_settings.lastFetchStatus = FIRRemoteConfigFetchStatusFailure;
diff --git a/FirebaseRemoteConfig/Sources/RCNConfigSettings.m b/FirebaseRemoteConfig/Sources/RCNConfigSettings.m
index bef4d60a94e..989deda4bba 100644
--- a/FirebaseRemoteConfig/Sources/RCNConfigSettings.m
+++ b/FirebaseRemoteConfig/Sources/RCNConfigSettings.m
@@ -234,10 +234,6 @@ - (void)updateExponentialBackoffTime {
 
 - (void)updateMetadataWithFetchSuccessStatus:(BOOL)fetchSuccess {
   FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000056", @"Updating metadata with fetch result.");
-  if (!fetchSuccess) {
-    [self updateExponentialBackoffTime];
-  }
-
   [self updateFetchTimeWithSuccessFetch:fetchSuccess];
   _lastFetchStatus =
       fetchSuccess ? FIRRemoteConfigFetchStatusSuccess : FIRRemoteConfigFetchStatusFailure;
diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m
index aa94598816f..3a976334d55 100644
--- a/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m
+++ b/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m
@@ -733,6 +733,83 @@ __unsafe_unretained void (^handler)(FIRRemoteConfigFetchStatus status,
                                }];
 }
 
+- (void)testFetchFailedNoNetworkErrorDoesNotThrottle {
+  RCNConfigContent *configContent = [[RCNConfigContent alloc] initWithDBManager:_DBManager];
+  NSString *currentAppName = RCNTestsDefaultFIRAppName;
+  FIROptions *currentOptions = [self firstAppOptions];
+  NSString *currentNamespace = RCNTestsFIRNamespace;
+  NSString *fullyQualifiedNamespace =
+      [NSString stringWithFormat:@"%@:%@", currentNamespace, currentAppName];
+
+  RCNUserDefaultsManager *userDefaultsManager =
+      [[RCNUserDefaultsManager alloc] initWithAppName:currentAppName
+                                             bundleID:[NSBundle mainBundle].bundleIdentifier
+                                            namespace:fullyQualifiedNamespace];
+  userDefaultsManager.lastFetchTime = 0;
+
+  FIRRemoteConfig *config = OCMPartialMock([[FIRRemoteConfig alloc] initWithAppName:currentAppName
+                                                                         FIROptions:currentOptions
+                                                                          namespace:currentNamespace
+                                                                          DBManager:_DBManager
+                                                                      configContent:configContent
+                                                                          analytics:nil]);
+  RCNConfigSettings *settings =
+      [[RCNConfigSettings alloc] initWithDatabaseManager:_DBManager
+                                               namespace:fullyQualifiedNamespace
+                                         firebaseAppName:currentAppName
+                                             googleAppID:currentOptions.googleAppID];
+  dispatch_queue_t queue = dispatch_queue_create(
+      [[NSString stringWithFormat:@"testqueue"] cStringUsingEncoding:NSUTF8StringEncoding],
+      DISPATCH_QUEUE_SERIAL);
+  RCNConfigFetch *configFetch =
+      OCMPartialMock([[RCNConfigFetch alloc] initWithContent:configContent
+                                                   DBManager:_DBManager
+                                                    settings:settings
+                                                   analytics:nil
+                                                  experiment:nil
+                                                       queue:queue
+                                                   namespace:fullyQualifiedNamespace
+                                                     options:currentOptions]);
+
+  OCMStub([configFetch fetchConfigWithExpirationDuration:43200 completionHandler:OCMOCK_ANY])
+      .andDo(^(NSInvocation *invocation) {
+        __unsafe_unretained void (^handler)(FIRRemoteConfigFetchStatus status,
+                                            NSError *_Nullable error) = nil;
+        [invocation getArgument:&handler atIndex:3];
+        [configFetch fetchWithUserProperties:[[NSDictionary alloc] init] completionHandler:handler];
+      });
+  _responseData[0] = [NSJSONSerialization dataWithJSONObject:@{} options:0 error:nil];
+
+  // A no network error is accompanied with an HTTP status code of 0.
+  _URLResponse[0] =
+      [[NSHTTPURLResponse alloc] initWithURL:[NSURL URLWithString:@"https://firebase.com"]
+                                  statusCode:0
+                                 HTTPVersion:nil
+                                headerFields:@{@"etag" : @"etag1"}];
+  [config updateWithNewInstancesForConfigFetch:configFetch
+                                 configContent:configContent
+                                configSettings:settings
+                              configExperiment:nil];
+
+  XCTestExpectation *expectation =
+      [self expectationWithDescription:@"Network error doesn't increase throttle interval"];
+  XCTAssertEqual(config.lastFetchStatus, FIRRemoteConfigFetchStatusNoFetchYet);
+
+  FIRRemoteConfigFetchCompletion fetchCompletion =
+      ^void(FIRRemoteConfigFetchStatus status, NSError *error) {
+        XCTAssertEqual(config.lastFetchStatus, FIRRemoteConfigFetchStatusFailure);
+        XCTAssertEqual(settings.exponentialBackoffRetryInterval, 0);
+        [expectation fulfill];
+      };
+
+  [config fetchWithExpirationDuration:43200 completionHandler:fetchCompletion];
+
+  [self waitForExpectationsWithTimeout:_expectationTimeout
+                               handler:^(NSError *error) {
+                                 XCTAssertNil(error);
+                               }];
+}
+
 // Activate should return false if a fetch response returns 200 with NO_CHANGE as the response body.
 - (void)testActivateOnFetchNoChangeStatus {
   // Override the setup values to return back an error status.


Test patch: