Skip to content

Commit 179dff2

Browse files
FIRInstallationsAPIService: Retry API request on HTTP 500. (#4052)
1 parent 046e299 commit 179dff2

File tree

7 files changed

+229
-113
lines changed

7 files changed

+229
-113
lines changed

FirebaseInstallations/Source/Library/Errors/FIRInstallationsHTTPError.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,5 @@ typedef NS_ENUM(NSInteger, FIRInstallationsRegistrationHTTPCode) {
4242
FIRInstallationsRegistrationHTTPCodeTooManyRequests = 429,
4343
FIRInstallationsRegistrationHTTPCodeServerInternalError = 500
4444
};
45+
46+
extern NSInteger const kFIRInstallationsAPIInternalErrorHTTPCode;

FirebaseInstallations/Source/Library/Errors/FIRInstallationsHTTPError.m

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,5 @@ + (BOOL)supportsSecureCoding {
7676
}
7777

7878
@end
79+
80+
NSInteger const kFIRInstallationsAPIInternalErrorHTTPCode = 500;

FirebaseInstallations/Source/Library/InstallationsAPI/FIRInstallationsAPIService.m

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -105,16 +105,13 @@ - (instancetype)initWithURLSession:(NSURLSession *)URLSession
105105
});
106106
}
107107

108-
- (FBLPromise<NSNull *> *)deleteInstallation:(FIRInstallationsItem *)installation {
108+
- (FBLPromise<FIRInstallationsItem *> *)deleteInstallation:(FIRInstallationsItem *)installation {
109109
NSURLRequest *request = [self deleteInstallationRequestWithInstallation:installation];
110-
return [self sendURLRequest:request]
111-
.then(^id(FIRInstallationsURLSessionResponse *response) {
112-
return [self validateHTTPResponseStatusCode:response];
113-
})
114-
.then(^id(id result) {
110+
return [[self sendURLRequest:request]
111+
then:^id _Nullable(FIRInstallationsURLSessionResponse *_Nullable value) {
115112
// Return the original installation on success.
116113
return installation;
117-
});
114+
}];
118115
}
119116

120117
#pragma mark - Register Installation
@@ -137,7 +134,7 @@ - (NSURLRequest *)registerRequestWithInstallation:(FIRInstallationsItem *)instal
137134
- (FBLPromise<FIRInstallationsItem *> *)
138135
registeredInstallationWithInstallation:(FIRInstallationsItem *)installation
139136
serverResponse:(FIRInstallationsURLSessionResponse *)response {
140-
return [self validateHTTPResponseStatusCode:response].then(^id(id result) {
137+
return [FBLPromise do:^id {
141138
FIRLogDebug(kFIRLoggerInstallations, kFIRInstallationsMessageCodeParsingAPIResponse,
142139
@"Parsing server response for %@.", response.HTTPResponse.URL);
143140
NSError *error;
@@ -156,7 +153,7 @@ - (NSURLRequest *)registerRequestWithInstallation:(FIRInstallationsItem *)instal
156153
kFIRInstallationsMessageCodeAPIResponseParsingInstallationSucceed,
157154
@"FIRInstallationsItem parsed successfully.");
158155
return registeredInstallation;
159-
});
156+
}];
160157
}
161158

162159
#pragma mark - Auth token
@@ -177,7 +174,7 @@ - (NSURLRequest *)authTokenRequestWithInstallation:(FIRInstallationsItem *)insta
177174

178175
- (FBLPromise<FIRInstallationsStoredAuthToken *> *)authTokenWithServerResponse:
179176
(FIRInstallationsURLSessionResponse *)response {
180-
return [self validateHTTPResponseStatusCode:response].then(^id(id result) {
177+
return [FBLPromise do:^id {
181178
FIRLogDebug(kFIRLoggerInstallations, kFIRInstallationsMessageCodeParsingAPIResponse,
182179
@"Parsing server response for %@.", response.HTTPResponse.URL);
183180
NSError *error;
@@ -196,7 +193,7 @@ - (NSURLRequest *)authTokenRequestWithInstallation:(FIRInstallationsItem *)insta
196193
kFIRInstallationsMessageCodeAPIResponseParsingAuthTokenSucceed,
197194
@"FIRInstallationsStoredAuthToken parsed successfully.");
198195
return token;
199-
});
196+
}];
200197
}
201198

202199
#pragma mark - Delete Installation
@@ -229,8 +226,8 @@ - (NSURLRequest *)requestWithURL:(NSURL *)requestURL
229226
return [request copy];
230227
}
231228

232-
- (FBLPromise<FIRInstallationsURLSessionResponse *> *)sendURLRequest:(NSURLRequest *)request {
233-
return [FBLPromise async:^(FBLPromiseFulfillBlock fulfill, FBLPromiseRejectBlock reject) {
229+
- (FBLPromise<FIRInstallationsURLSessionResponse *> *)URLRequestPromise:(NSURLRequest *)request {
230+
return [[FBLPromise async:^(FBLPromiseFulfillBlock fulfill, FBLPromiseRejectBlock reject) {
234231
FIRLogDebug(kFIRLoggerInstallations, kFIRInstallationsMessageCodeSendAPIRequest,
235232
@"Sending request: %@, body:%@, headers: %@.", request,
236233
[[NSString alloc] initWithData:request.HTTPBody encoding:NSUTF8StringEncoding],
@@ -253,6 +250,8 @@ - (NSURLRequest *)requestWithURL:(NSURL *)requestURL
253250
data:data]);
254251
}
255252
}] resume];
253+
}] then:^id _Nullable(FIRInstallationsURLSessionResponse *response) {
254+
return [self validateHTTPResponseStatusCode:response];
256255
}];
257256
}
258257

@@ -271,6 +270,17 @@ - (NSURLRequest *)requestWithURL:(NSURL *)requestURL
271270
}];
272271
}
273272

273+
- (FBLPromise<FIRInstallationsURLSessionResponse *> *)sendURLRequest:(NSURLRequest *)request {
274+
return [FBLPromise attempts:1
275+
delay:1
276+
condition:^BOOL(NSInteger remainingAttempts, NSError *_Nonnull error) {
277+
return [FIRInstallationsErrorUtil isAPIError:error withHTTPCode:500];
278+
}
279+
retry:^id _Nullable {
280+
return [self URLRequestPromise:request];
281+
}];
282+
}
283+
274284
- (NSString *)SDKVersion {
275285
return [NSString stringWithFormat:@"i:%s", FIRInstallationsVersionStr];
276286
}

FirebaseInstallations/Source/Library/InstallationsIDController/FIRInstallationsIDController.m

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,7 @@ - (FIRInstallationsStoredRegistrationParameters *)currentRegistrationParameters
348348
[registeredInstallation.authToken.expirationDate timeIntervalSinceDate:[NSDate date]] <
349349
kFIRInstallationsTokenExpirationThreshold;
350350
if (forceRefresh || isTokenExpiredOrExpiresSoon) {
351-
return [self refreshAuthTokenForInstallation:registeredInstallation];
351+
return [self.APIService refreshAuthTokenForInstallation:registeredInstallation];
352352
} else {
353353
return registeredInstallation;
354354
}
@@ -358,18 +358,6 @@ - (FIRInstallationsStoredRegistrationParameters *)currentRegistrationParameters
358358
});
359359
}
360360

361-
- (FBLPromise<FIRInstallationsItem *> *)refreshAuthTokenForInstallation:
362-
(FIRInstallationsItem *)installation {
363-
return [FBLPromise attempts:1
364-
delay:1
365-
condition:^BOOL(NSInteger remainingAttempts, NSError *_Nonnull error) {
366-
return [FIRInstallationsErrorUtil isAPIError:error withHTTPCode:500];
367-
}
368-
retry:^id _Nullable {
369-
return [self.APIService refreshAuthTokenForInstallation:installation];
370-
}];
371-
}
372-
373361
#pragma mark - Delete FID
374362

375363
- (FBLPromise<NSNull *> *)deleteInstallation {

FirebaseInstallations/Source/Tests/Unit/FIRInstallationsAPIServiceTests.m

Lines changed: 189 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
#import "FIRInstallationsAPIService.h"
2525
#import "FIRInstallationsErrorUtil.h"
26+
#import "FIRInstallationsHTTPError.h"
2627
#import "FIRInstallationsStoredAuthToken.h"
2728
#import "FIRInstallationsVersion.h"
2829

@@ -57,6 +58,9 @@ - (void)tearDown {
5758
self.mockURLSession = nil;
5859
self.projectID = nil;
5960
self.APIKey = nil;
61+
62+
// // Wait for any pending promises to complete.
63+
// XCTAssert(FBLWaitForPromisesWithTimeout(2));
6064
}
6165

6266
- (void)testRegisterInstallationSuccess {
@@ -139,7 +143,67 @@ - (void)testRegisterInstallationSuccess {
139143
isApproximatelyEqualCurrentPlusTimeInterval:604800];
140144
}
141145

142-
// TODO: More tests for Register Installation API
146+
- (void)testRegisterInstallation_WhenError500_ThenRetriesOnce {
147+
FIRInstallationsItem *installation = [[FIRInstallationsItem alloc] initWithAppID:@"app-id"
148+
firebaseAppName:@"name"];
149+
installation.firebaseInstallationID = [FIRInstallationsItem generateFID];
150+
151+
// 1. Stub URL session:
152+
153+
// 1.2. Capture completion to call it later.
154+
__block void (^taskCompletion)(NSData *, NSURLResponse *, NSError *);
155+
id completionArg = [OCMArg checkWithBlock:^BOOL(id obj) {
156+
taskCompletion = obj;
157+
return YES;
158+
}];
159+
160+
// 1.3. Create a data task mock.
161+
id mockDataTask1 = OCMClassMock([NSURLSessionDataTask class]);
162+
OCMExpect([(NSURLSessionDataTask *)mockDataTask1 resume]);
163+
164+
// 1.4. Expect `dataTaskWithRequest` to be called.
165+
OCMExpect([self.mockURLSession dataTaskWithRequest:[OCMArg any] completionHandler:completionArg])
166+
.andReturn(mockDataTask1);
167+
168+
// 2. Call
169+
FBLPromise<FIRInstallationsItem *> *promise = [self.service registerInstallation:installation];
170+
171+
// 3. Wait for `[NSURLSession dataTaskWithRequest...]` to be called
172+
OCMVerifyAllWithDelay(self.mockURLSession, 0.5);
173+
174+
// 4. Wait for the data task `resume` to be called.
175+
OCMVerifyAllWithDelay(mockDataTask1, 0.5);
176+
177+
// 5. Call the data task completion.
178+
NSData *successResponseData =
179+
[self loadFixtureNamed:@"APIRegisterInstallationResponseSuccess.json"];
180+
taskCompletion(successResponseData,
181+
[self responseWithStatusCode:kFIRInstallationsAPIInternalErrorHTTPCode], nil);
182+
183+
// 6.1. Expect network request to send again.
184+
id mockDataTask2 = OCMClassMock([NSURLSessionDataTask class]);
185+
OCMExpect([(NSURLSessionDataTask *)mockDataTask2 resume]);
186+
OCMExpect([self.mockURLSession dataTaskWithRequest:[OCMArg any] completionHandler:completionArg])
187+
.andReturn(mockDataTask2);
188+
189+
// 6.2. Wait for the second network request to complete.
190+
OCMVerifyAllWithDelay(self.mockURLSession, 1.5);
191+
OCMVerifyAllWithDelay(mockDataTask2, 1.5);
192+
193+
// 6.3. Send network response again.
194+
taskCompletion(successResponseData,
195+
[self responseWithStatusCode:kFIRInstallationsAPIInternalErrorHTTPCode], nil);
196+
197+
// 7. Check result.
198+
XCTAssert(FBLWaitForPromisesWithTimeout(0.5));
199+
200+
XCTAssertNil(promise.value);
201+
XCTAssertNotNil(promise.error);
202+
203+
XCTAssertTrue([promise.error isKindOfClass:[FIRInstallationsHTTPError class]]);
204+
FIRInstallationsHTTPError *HTTPError = (FIRInstallationsHTTPError *)promise.error;
205+
XCTAssertEqual(HTTPError.HTTPResponse.statusCode, kFIRInstallationsAPIInternalErrorHTTPCode);
206+
}
143207

144208
- (void)testRefreshAuthTokenSuccess {
145209
FIRInstallationsItem *installation = [FIRInstallationsItem createRegisteredInstallationItem];
@@ -248,6 +312,72 @@ - (void)testRefreshAuthTokenAPIError {
248312
XCTAssertNil(promise.value);
249313
}
250314

315+
- (void)testRefreshAuthToken_WhenAPIError500_ThenRetriesOnce {
316+
FIRInstallationsItem *installation = [FIRInstallationsItem createRegisteredInstallationItem];
317+
installation.firebaseInstallationID = @"qwertyuiopasdfghjklzxcvbnm";
318+
319+
// 1. Stub URL session:
320+
321+
// 1.1. URL request validation.
322+
id URLRequestValidation = [self refreshTokenRequestValidationArgWithInstallation:installation];
323+
324+
// 1.2. Capture completion to call it later.
325+
__block void (^taskCompletion)(NSData *, NSURLResponse *, NSError *);
326+
id completionArg = [OCMArg checkWithBlock:^BOOL(id obj) {
327+
taskCompletion = obj;
328+
return YES;
329+
}];
330+
331+
// 1.3. Create a data task mock.
332+
id mockDataTask1 = OCMClassMock([NSURLSessionDataTask class]);
333+
OCMExpect([(NSURLSessionDataTask *)mockDataTask1 resume]);
334+
335+
// 1.4. Expect `dataTaskWithRequest` to be called.
336+
OCMExpect([self.mockURLSession dataTaskWithRequest:URLRequestValidation
337+
completionHandler:completionArg])
338+
.andReturn(mockDataTask1);
339+
340+
// 1.5. Prepare server response data.
341+
NSData *errorResponseData =
342+
[self loadFixtureNamed:@"APIGenerateTokenResponseInvalidRefreshToken.json"];
343+
344+
// 2. Call
345+
FBLPromise<FIRInstallationsItem *> *promise =
346+
[self.service refreshAuthTokenForInstallation:installation];
347+
348+
// 3. Wait for `[NSURLSession dataTaskWithRequest...]` to be called
349+
OCMVerifyAllWithDelay(self.mockURLSession, 0.5);
350+
351+
// 4. Wait for the data task `resume` to be called.
352+
OCMVerifyAllWithDelay(mockDataTask1, 0.5);
353+
354+
// 5. Call the data task completion.
355+
taskCompletion(errorResponseData,
356+
[self responseWithStatusCode:kFIRInstallationsAPIInternalErrorHTTPCode], nil);
357+
358+
// 6. Retry:
359+
360+
// 6.1. Expect another API request to be sent.
361+
id mockDataTask2 = OCMClassMock([NSURLSessionDataTask class]);
362+
OCMExpect([(NSURLSessionDataTask *)mockDataTask2 resume]);
363+
OCMExpect([self.mockURLSession dataTaskWithRequest:URLRequestValidation
364+
completionHandler:completionArg])
365+
.andReturn(mockDataTask2);
366+
OCMVerifyAllWithDelay(self.mockURLSession, 1.5);
367+
OCMVerifyAllWithDelay(mockDataTask2, 1.5);
368+
369+
// 6.2. Send the API response again.
370+
taskCompletion(errorResponseData,
371+
[self responseWithStatusCode:kFIRInstallationsAPIInternalErrorHTTPCode], nil);
372+
373+
// 6. Check result.
374+
XCTAssert(FBLWaitForPromisesWithTimeout(0.5));
375+
376+
XCTAssertTrue([FIRInstallationsErrorUtil isAPIError:promise.error
377+
withHTTPCode:kFIRInstallationsAPIInternalErrorHTTPCode]);
378+
XCTAssertNil(promise.value);
379+
}
380+
251381
- (void)testRefreshAuthTokenDataNil {
252382
FIRInstallationsItem *installation = [FIRInstallationsItem createRegisteredInstallationItem];
253383
installation.firebaseInstallationID = @"qwertyuiopasdfghjklzxcvbnm";
@@ -384,6 +514,64 @@ - (void)testDeleteInstallationErrorNotFound {
384514
XCTAssertNil(promise.value);
385515
}
386516

517+
- (void)testDeleteInstallation_WhenAPIError500_ThenRetriesOnce {
518+
FIRInstallationsItem *installation = [FIRInstallationsItem createRegisteredInstallationItem];
519+
520+
// 1. Stub URL session:
521+
522+
// 1.1. URL request validation.
523+
id URLRequestValidation = [self deleteInstallationRequestValidationWithInstallation:installation];
524+
525+
// 1.2. Capture completion to call it later.
526+
__block void (^taskCompletion)(NSData *, NSURLResponse *, NSError *);
527+
id completionArg = [OCMArg checkWithBlock:^BOOL(id obj) {
528+
taskCompletion = obj;
529+
return YES;
530+
}];
531+
532+
// 1.3. Create a data task mock.
533+
id mockDataTask1 = OCMClassMock([NSURLSessionDataTask class]);
534+
OCMExpect([(NSURLSessionDataTask *)mockDataTask1 resume]);
535+
536+
// 1.4. Expect `dataTaskWithRequest` to be called.
537+
OCMExpect([self.mockURLSession dataTaskWithRequest:URLRequestValidation
538+
completionHandler:completionArg])
539+
.andReturn(mockDataTask1);
540+
541+
// 2. Call
542+
FBLPromise<FIRInstallationsItem *> *promise = [self.service deleteInstallation:installation];
543+
544+
// 3. Wait for `[NSURLSession dataTaskWithRequest...]` to be called
545+
OCMVerifyAllWithDelay(self.mockURLSession, 0.5);
546+
547+
// 4. Wait for the data task `resume` to be called.
548+
OCMVerifyAllWithDelay(mockDataTask1, 0.5);
549+
550+
// 5. Call the data task completion.
551+
// HTTP 200 but no data (a potential server failure).
552+
taskCompletion(nil, [self responseWithStatusCode:kFIRInstallationsAPIInternalErrorHTTPCode], nil);
553+
554+
// 6. Retry:
555+
// 6.1. Wait for the API request to be sent again.
556+
id mockDataTask2 = OCMClassMock([NSURLSessionDataTask class]);
557+
OCMExpect([(NSURLSessionDataTask *)mockDataTask2 resume]);
558+
OCMExpect([self.mockURLSession dataTaskWithRequest:URLRequestValidation
559+
completionHandler:completionArg])
560+
.andReturn(mockDataTask2);
561+
OCMVerifyAllWithDelay(self.mockURLSession, 1.5);
562+
OCMVerifyAllWithDelay(mockDataTask1, 1.5);
563+
564+
// 6.1. Send another response.
565+
taskCompletion(nil, [self responseWithStatusCode:kFIRInstallationsAPIInternalErrorHTTPCode], nil);
566+
567+
// 7. Check result.
568+
FBLWaitForPromisesWithTimeout(0.5);
569+
570+
XCTAssertTrue([FIRInstallationsErrorUtil isAPIError:promise.error
571+
withHTTPCode:kFIRInstallationsAPIInternalErrorHTTPCode]);
572+
XCTAssertNil(promise.value);
573+
}
574+
387575
#pragma mark - Helpers
388576

389577
- (NSData *)loadFixtureNamed:(NSString *)fileName {
@@ -464,8 +652,6 @@ - (id)deleteInstallationRequestValidationWithInstallation:(FIRInstallationsItem
464652
}];
465653
}
466654

467-
#pragma mark - Helpers
468-
469655
- (NSString *)SDKVersion {
470656
return [NSString stringWithFormat:@"i:%s", FIRInstallationsVersionStr];
471657
}

0 commit comments

Comments
 (0)