Skip to content

Commit

Permalink
Refactor HTTP code and add proxy support. (#578)
Browse files Browse the repository at this point in the history
This refactors the HTTP interaction in the library + Segment
integration. Internally, it moves away from `NSURLConnection` to
`NSURLSession`. `SEGAnalyticsRequest` is replaced by `NSURLSessionTask`,
and a thin wrapper class `SEGHTTPClient` is introduced to simplify the
interaction between the library and the Segment API.

This also adds tests for the networking code by stubbing  the Segment
API using https://github.com/luisobo/Nocilla so the behaviour of
`SEGHTTPClient` can be verified under different responses from the
server.

Publicly, it also adds a `SEGRequestFactory` abstraction that given a
URL returns a `NSMutableURLRequest `. The default implementation simply
calls `[NSMutableURLRequest requestWithURL:url]`.  It is configurable,
and this lets users customize the `NSMutableURLRequest` made by the
library. e.g. users could change it to point the SDK to a custom proxy
instance.
  • Loading branch information
f2prateek committed Aug 5, 2016
1 parent 900eb95 commit d5db28a
Show file tree
Hide file tree
Showing 90 changed files with 3,299 additions and 999 deletions.
18 changes: 0 additions & 18 deletions Analytics/Classes/Internal/SEGAnalyticsRequest.h

This file was deleted.

120 changes: 0 additions & 120 deletions Analytics/Classes/Internal/SEGAnalyticsRequest.m

This file was deleted.

23 changes: 23 additions & 0 deletions Analytics/Classes/Internal/SEGHTTPClient.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#import <Foundation/Foundation.h>
#import "SEGAnalytics.h"


@interface SEGHTTPClient : NSObject

@property (nonatomic, strong) SEGRequestFactory requestFactory;

+ (SEGRequestFactory)defaultRequestFactory;

- (instancetype)initWithRequestFactory:(SEGRequestFactory)requestFactory;

/**
* Upload dictionary formatted as per https://segment.com/docs/sources/server/http/#batch.
* This method will convert the dictionary to json, gzip it and upload the data.
* It will respond with retry = YES if the batch should be reuploaded at a later time.
* It will ask to retry for json errors and 3xx/5xx codes, and not retry for 2xx/4xx response codes.
*/
- (NSURLSessionUploadTask *)upload:(NSDictionary *)batch forWriteKey:(NSString *)writeKey completionHandler:(void (^)(BOOL retry))completionHandler;

- (NSURLSessionDataTask *)settingsForWriteKey:(NSString *)writeKey completionHandler:(void (^)(BOOL success, NSDictionary *settings))completionHandler;

@end
141 changes: 141 additions & 0 deletions Analytics/Classes/Internal/SEGHTTPClient.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
#import "SEGHTTPClient.h"
#import "NSData+GZIP.h"
#import "SEGAnalyticsUtils.h"


@implementation SEGHTTPClient

+ (NSMutableURLRequest * (^)(NSURL *))defaultRequestFactory
{
return ^(NSURL *url) {
return [NSMutableURLRequest requestWithURL:url];
};
}

- (instancetype)initWithRequestFactory:(SEGRequestFactory)requestFactory
{
if (self = [self init]) {
if (requestFactory == nil) {
self.requestFactory = [SEGHTTPClient defaultRequestFactory];
} else {
self.requestFactory = requestFactory;
}
}
return self;
}

- (NSString *)authorizationHeader:(NSString *)writeKey
{
NSString *rawHeader = [writeKey stringByAppendingString:@":"];
NSData *userPasswordData = [rawHeader dataUsingEncoding:NSUTF8StringEncoding];
return [userPasswordData base64EncodedStringWithOptions:0];
}

- (NSURLSessionUploadTask *)upload:(NSDictionary *)batch forWriteKey:(NSString *)writeKey completionHandler:(void (^)(BOOL retry))completionHandler
{
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.HTTPAdditionalHeaders = @{
@"Accept-Encoding" : @"gzip",
@"Content-Encoding" : @"gzip",
@"Content-Type" : @"application/json",
@"Authorization" : [@"Basic " stringByAppendingString:[self authorizationHeader:writeKey]],
};
NSURLSession *session = [NSURLSession sessionWithConfiguration:config];

NSURL *url = [NSURL URLWithString:@"https://api.segment.io/v1/batch"];
NSMutableURLRequest *request = self.requestFactory(url);
[request setHTTPMethod:@"POST"];

NSError *error = nil;
NSException *exception = nil;
NSData *payload = nil;
@try {
payload = [NSJSONSerialization dataWithJSONObject:batch options:0 error:&error];
}
@catch (NSException *exc) {
exception = exc;
}
if (error || exception) {
SEGLog(@"%@ Error serializing JSON for batch upload %@", self, error);
completionHandler(NO); // Don't retry this batch.
return nil;
}
NSData *gzippedPayload = [payload seg_gzippedData];

NSURLSessionUploadTask *task = [session uploadTaskWithRequest:request fromData:gzippedPayload completionHandler:^(NSData *_Nullable data, NSURLResponse *_Nullable response, NSError *_Nullable error) {
if (error) {
SEGLog(@"Error uploading request %@.", error);
completionHandler(YES);
return;
}

NSInteger code = ((NSHTTPURLResponse *)response).statusCode;
if (code < 300) {
// 2xx response codes.
completionHandler(NO);
return;
}
if (code < 400) {
// 3xx response codes.
SEGLog(@"Server responded with unexpected HTTP code %d.", code);
completionHandler(YES);
return;
}
if (code < 500) {
// 4xx response codes.
SEGLog(@"Server rejected payload with HTTP code %d.", code);
completionHandler(NO);
return;
}

// 5xx response codes.
SEGLog(@"Server error with HTTP code %d.", code);
completionHandler(YES);
}];
[task resume];
return task;
}

- (NSURLSessionDataTask *)settingsForWriteKey:(NSString *)writeKey completionHandler:(void (^)(BOOL success, NSDictionary *settings))completionHandler
{
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.HTTPAdditionalHeaders = @{
@"Accept-Encoding" : @"gzip"
};
NSURLSession *session = [NSURLSession sessionWithConfiguration:config];

NSString *rawURL = [NSString stringWithFormat:@"https://cdn.segment.com/v1/projects/%@/settings", writeKey];
NSURL *url = [NSURL URLWithString:rawURL];
NSMutableURLRequest *request = self.requestFactory(url);
[request setHTTPMethod:@"GET"];

NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData *_Nullable data, NSURLResponse *_Nullable response, NSError *_Nullable error) {
if (error != nil) {
SEGLog(@"Error fetching settings %@.", error);
completionHandler(NO, nil);
return;
}

NSInteger code = ((NSHTTPURLResponse *)response).statusCode;
if (code > 300) {
SEGLog(@"Server responded with unexpected HTTP code %d.", code);
completionHandler(NO, nil);
return;
}

NSError *jsonError = nil;
id responseJson = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError];
if (jsonError != nil) {
SEGLog(@"Error deserializing response body %@.", jsonError);
completionHandler(NO, nil);
return;
}

// 2xx response codes.
completionHandler(YES, responseJson);
}];
[task resume];
return task;
}

@end
3 changes: 0 additions & 3 deletions Analytics/Classes/Internal/SEGSegmentIntegration.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ extern NSString *const SEGSegmentRequestDidFailNotification;

@interface SEGSegmentIntegration : NSObject <SEGIntegration>

@property (nonatomic, copy) NSString *userId;
@property (nonatomic, strong) NSURL *apiURL;

- (id)initWithAnalytics:(SEGAnalytics *)analytics;

@end
Loading

0 comments on commit d5db28a

Please sign in to comment.