Skip to content

Commit 937b44f

Browse files
authored
Request reattempt fix (#418)
* Fix Request Re-Attempt Issue • Our SDK recently introduced HTTP reattempts for failed (500+) type requests. • However, our SDK also had a timeout where it would give up on an HTTP request if it took too long • This became apparent when calls to sendTags() when users had no network connection would, after 60 seconds, call the successBlock • The success block was being called because the final re-attempt had not yet occurred, so no errors had been populated in the errors dictionary • Fixed by (A) fixing the timeout to give enough time for all re-attempts to finish and (B) ensuring that there will be an error added to the errors dictionary if a request times out. * Add Testing • Adds testing to ensure that the reattempt logic works correctly
1 parent 16c5933 commit 937b44f

File tree

8 files changed

+135
-11
lines changed

8 files changed

+135
-11
lines changed

iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1147,6 +1147,11 @@
11471147
CLANG_WARN_SUSPICIOUS_MOVES = YES;
11481148
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
11491149
DEVELOPMENT_TEAM = 99SW8E36CT;
1150+
GCC_PREPROCESSOR_DEFINITIONS = (
1151+
"DEBUG=1",
1152+
"$(inherited)",
1153+
"OS_TEST=1",
1154+
);
11501155
INFOPLIST_FILE = UnitTests/Info.plist;
11511156
IPHONEOS_DEPLOYMENT_TARGET = 10.1;
11521157
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";

iOS_SDK/OneSignalSDK/Source/OneSignalClient.m

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,7 @@
2929
#import "UIApplicationDelegate+OneSignal.h"
3030
#import "ReattemptRequest.h"
3131
#import "OneSignal.h"
32-
33-
#define REATTEMPT_DELAY 30.0
34-
#define REQUEST_TIMEOUT_REQUEST 60.0 //for most HTTP requests
35-
#define REQUEST_TIMEOUT_RESOURCE 100.0 //for loading a resource like an image
36-
#define MAX_ATTEMPT_COUNT 3
32+
#import "OneSignalCommonDefines.h"
3733

3834
@interface OneSignal (OneSignalClientExtra)
3935
+ (BOOL)shouldLogMissingPrivacyConsentErrorWithMethodName:(NSString *)methodName;
@@ -66,6 +62,14 @@ -(instancetype)init {
6662
return self;
6763
}
6864

65+
- (NSError *)privacyConsentErrorWithRequestType:(NSString *)type {
66+
return [NSError errorWithDomain:@"OneSignal Error" code:0 userInfo:@{@"error" : [NSString stringWithFormat: @"Attempted to perform an HTTP request (%@) before the user provided privacy consent.", type]}];
67+
}
68+
69+
- (NSError *)genericTimedOutError {
70+
return [NSError errorWithDomain:@"OneSignal Error" code:0 userInfo:@{@"error" : @"The request timed out"}];
71+
}
72+
6973
- (void)executeSimultaneousRequests:(NSDictionary<NSString *, OneSignalRequest *> *)requests withSuccess:(OSMultipleSuccessBlock)successBlock onFailure:(OSMultipleFailureBlock)failureBlock {
7074
if (requests.allKeys.count == 0)
7175
return;
@@ -92,7 +96,15 @@ - (void)executeSimultaneousRequests:(NSDictionary<NSString *, OneSignalRequest *
9296
}];
9397
}
9498

95-
dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, REQUEST_TIMEOUT_REQUEST * NSEC_PER_SEC));
99+
// Will wait for up to (maxTimeout) seconds and will then give up and call
100+
// the failure block if the request times out.
101+
let timedOut = (bool)(0 != dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, MAX_TIMEOUT)));
102+
103+
// add a generic 'timed out' error if the request timed out
104+
// and there are no other errors present.
105+
if (timedOut && errors.allKeys.count == 0)
106+
for (NSString *key in requests.allKeys)
107+
errors[key] = [self genericTimedOutError];
96108

97109
//requests should all be completed at this point
98110
dispatch_async(dispatch_get_main_queue(), ^{
@@ -109,9 +121,7 @@ - (void)executeRequest:(OneSignalRequest *)request onSuccess:(OSResultSuccessBlo
109121

110122
if (request.method != GET && [OneSignal shouldLogMissingPrivacyConsentErrorWithMethodName:nil]) {
111123
if (failureBlock) {
112-
failureBlock([NSError errorWithDomain:@"OneSignal Error" code:0
113-
userInfo:@{@"error" : [NSString stringWithFormat:
114-
@"Attempted to perform an HTTP request (%@) before the user provided privacy consent.", NSStringFromClass(request.class)]}]);
124+
failureBlock([self privacyConsentErrorWithRequestType:NSStringFromClass(request.class)]);
115125
}
116126

117127
return;
@@ -198,7 +208,8 @@ - (BOOL)willReattemptRequest:(int)statusCode withRequest:(OneSignalRequest *)req
198208

199209
if (async) {
200210
//retry again in 15 seconds
201-
[OneSignal onesignal_Log:ONE_S_LL_DEBUG message:[NSString stringWithFormat:@"Re-scheduling request (%@) to be re-attempted in %i seconds due to failed HTTP request with status code %i", NSStringFromClass([request class]), (int)REATTEMPT_DELAY, (int)statusCode]];
211+
[OneSignal onesignal_Log:ONE_S_LL_DEBUG message:[NSString stringWithFormat:@"Re-scheduling request (%@) to be re-attempted in %.3f seconds due to failed HTTP request with status code %i", NSStringFromClass([request class]), REATTEMPT_DELAY, (int)statusCode]];
212+
NSLog(@"Delay: %f", REATTEMPT_DELAY);
202213

203214
[OneSignalHelper performSelector:@selector(reattemptRequest:) onMainThreadOnObject:self withObject:reattempt afterDelay:REATTEMPT_DELAY];
204215
} else {

iOS_SDK/OneSignalSDK/Source/OneSignalCommonDefines.h

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,27 @@
8686
// before registering the user anyways
8787
#define APNS_TIMEOUT 25.0
8888

89+
#ifndef OS_TEST
90+
// OneSignal API Client Defines
91+
#define REATTEMPT_DELAY 30.0
92+
#define REQUEST_TIMEOUT_REQUEST 120.0 //for most HTTP requests
93+
#define REQUEST_TIMEOUT_RESOURCE 120.0 //for loading a resource like an image
94+
#define MAX_ATTEMPT_COUNT 3
95+
96+
// Send tags batch delay
97+
#define SEND_TAGS_DELAY 5.0
98+
#else
99+
// Test defines for API Client
100+
#define REATTEMPT_DELAY 0.04
101+
#define REQUEST_TIMEOUT_REQUEST 0.2 //for most HTTP requests
102+
#define REQUEST_TIMEOUT_RESOURCE 0.2 //for loading a resource like an image
103+
#define MAX_ATTEMPT_COUNT 3
104+
105+
// Send tags batch delay
106+
#define SEND_TAGS_DELAY 0.05
107+
#endif
108+
109+
// A max timeout for a request, which might include multiple reattempts
110+
#define MAX_TIMEOUT ((REQUEST_TIMEOUT_REQUEST * MAX_ATTEMPT_COUNT) + (REATTEMPT_DELAY * MAX_ATTEMPT_COUNT)) * NSEC_PER_SEC
111+
89112
#endif /* OneSignalCommonDefines_h */

iOS_SDK/OneSignalSDK/UnitTests/Shadows/NSURLSessionOverrider.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,8 @@
3030
@interface NSURLSessionOverrider : NSObject
3131

3232
@end
33+
34+
35+
@interface MockNSURLSessionDataTask : NSURLSessionDataTask
36+
-(void)resume;
37+
@end

iOS_SDK/OneSignalSDK/UnitTests/Shadows/NSURLSessionOverrider.m

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,18 @@
2626
*/
2727

2828
#import "NSURLSessionOverrider.h"
29-
29+
#import "OneSignalSelectorHelpers.h"
3030
#import "TestHelperFunctions.h"
31+
#import "OneSignalHelper.h"
32+
33+
3134

3235
@implementation NSURLSessionOverrider
3336

3437
+ (void)load {
3538
// Swizzle an injected method defined in OneSignalHelper
3639
injectStaticSelector([NSURLSessionOverrider class], @selector(overrideDownloadItemAtURL:toFile:error:), [NSURLSession class], @selector(downloadItemAtURL:toFile:error:));
40+
injectToProperClass(@selector(overrideDataTaskWithRequest:completionHandler:), @selector(dataTaskWithRequest:completionHandler:), @[], [NSURLSessionOverrider class], [NSURLSession class]);
3741
}
3842

3943
// Override downloading of media attachment
@@ -52,4 +56,25 @@ + (NSString *)overrideDownloadItemAtURL:(NSURL*)url toFile:(NSString*)localPath
5256
return @"image/jpg";
5357
}
5458

59+
- (NSURLSessionDataTask *)overrideDataTaskWithRequest:(NSURLRequest *)request completionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler {
60+
61+
// mimics no active network connection
62+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.001 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
63+
NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:request.URL statusCode:0 HTTPVersion:@"1.1" headerFields:@{}];
64+
NSError *error = [NSError errorWithDomain:@"OneSignal Error" code:0 userInfo:@{@"error" : @"The user is not currently connected to the network."}];
65+
NSLog(@"Calling completion handler");
66+
completionHandler(nil, response, error);
67+
});
68+
69+
return [MockNSURLSessionDataTask new];
70+
}
71+
72+
@end
73+
74+
@implementation MockNSURLSessionDataTask
75+
76+
-(void)resume {
77+
//unimplemented
78+
}
79+
5580
@end

iOS_SDK/OneSignalSDK/UnitTests/Shadows/OneSignalClientOverrider.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,6 @@
4242
+(NSString *)lastHTTPRequestType;
4343
+(void)setRequiresEmailAuth:(BOOL)required;
4444
+(BOOL)hasExecutedRequestOfType:(Class)type;
45+
+ (void)disableExecuteRequestOverride:(BOOL)disable;
4546
@end
4647

iOS_SDK/OneSignalSDK/UnitTests/Shadows/OneSignalClientOverrider.m

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ @implementation OneSignalClientOverrider
4949
static NSString *lastHTTPRequestType;
5050
static BOOL requiresEmailAuth = false;
5151
static NSMutableArray<NSString *> *executedRequestTypes;
52+
static BOOL disableOverride = false;
5253

5354
+ (void)load {
5455
serialMockMainLooper = dispatch_queue_create("com.onesignal.unittest", DISPATCH_QUEUE_SERIAL);
@@ -63,7 +64,14 @@ + (void)load {
6364
executedRequestTypes = [[NSMutableArray alloc] init];
6465
}
6566

67+
// Calling this function twice results in reversing the swizzle
68+
+ (void)disableExecuteRequestOverride:(BOOL)disable {
69+
disableOverride = disable;
70+
}
71+
6672
- (void)overrideExecuteSimultaneousRequests:(NSDictionary<NSString *, OneSignalRequest *> *)requests withSuccess:(OSMultipleSuccessBlock)successBlock onFailure:(OSMultipleFailureBlock)failureBlock {
73+
if (disableOverride)
74+
return [self overrideExecuteSimultaneousRequests:requests withSuccess:successBlock onFailure:failureBlock];
6775

6876
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
6977

@@ -94,6 +102,10 @@ - (void)overrideExecuteSimultaneousRequests:(NSDictionary<NSString *, OneSignalR
94102
}
95103

96104
- (void)overrideExecuteRequest:(OneSignalRequest *)request onSuccess:(OSResultSuccessBlock)successBlock onFailure:(OSFailureBlock)failureBlock {
105+
106+
if (disableOverride)
107+
return [self overrideExecuteRequest:request onSuccess:successBlock onFailure:failureBlock];
108+
97109
[executedRequestTypes addObject:NSStringFromClass([request class])];
98110

99111
if (executeInstantaneously) {

iOS_SDK/OneSignalSDK/UnitTests/UnitTests.m

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2051,4 +2051,46 @@ - (void)testRegistersAfterNoApnsResponse {
20512051
XCTAssertTrue(observer->last.to.userId != nil);
20522052
}
20532053

2054+
/*
2055+
To prevent tests from generating actual HTTP requests, we swizzle
2056+
a method called executeRequest() in the OneSignalClient class
2057+
2058+
However, this test ensures that HTTP retry logic occurs correctly.
2059+
We have additionally swizzled NSURLSession to prevent real HTTP
2060+
requests from being generated.
2061+
2062+
TODO: Remove the OneSignalClientOverrider mock entirely and
2063+
instead swizzle NSURLSession
2064+
*/
2065+
- (void)testHTTPClientTimeout {
2066+
[OneSignal initWithLaunchOptions:nil appId:@"b2f7f966-d8cc-11e4-bed1-df8f05be55ba"
2067+
handleNotificationAction:nil
2068+
settings:nil];
2069+
2070+
[UnitTestCommonMethods runBackgroundThreads];
2071+
2072+
// Switches from overriding OneSignalClient to using a
2073+
// swizzled NSURLSession instead. This results in NSURLSession
2074+
// mimicking a no-network connection state
2075+
[OneSignalClientOverrider disableExecuteRequestOverride:true];
2076+
2077+
let expectation = [self expectationWithDescription:@"timeout_test"];
2078+
expectation.expectedFulfillmentCount = 1;
2079+
2080+
[OneSignal sendTags:@{@"test_tag_key" : @"test_tag_value"} onSuccess:^(NSDictionary *result) {
2081+
XCTFail(@"Success should not be called");
2082+
} onFailure:^(NSError *error) {
2083+
[expectation fulfill];
2084+
}];
2085+
2086+
[NSObjectOverrider runPendingSelectors];
2087+
[UnitTestCommonMethods runBackgroundThreads];
2088+
[NSObjectOverrider runPendingSelectors];
2089+
2090+
[self waitForExpectations:@[expectation] timeout:10.0];
2091+
2092+
// revert the swizzle back to the standard state for tests
2093+
[OneSignalClientOverrider disableExecuteRequestOverride:false];
2094+
}
2095+
20542096
@end

0 commit comments

Comments
 (0)