0

I created an NSURLSessionConfiguration with some default settings but when I see the request object made with that configuration in my custom NSURLProtocol it doesn't seem that all those settings are inherited and I'm a bit confused.

NSURLSessionConfiguration* config = [NSURLSessionConfiguration defaultSessionConfiguration];

NSMutableArray *protocolsArray = [NSMutableArray arrayWithArray:config.protocolClasses];

[protocolsArray insertObject:[CustomProtocol class] atIndex:0];

config.protocolClasses = protocolsArray;

// ex. set some random parameters

[config setHTTPAdditionalHeaders:@{@"Authorization":@"1234"}];
[config setAllowsCellularAccess:NO];
[config setRequestCachePolicy:NSURLRequestReturnCacheDataElseLoad];
[config setHTTPShouldSetCookies:NO];
[config setNetworkServiceType:NSURLNetworkServiceTypeVoice];
[config setTimeoutIntervalForRequest:4321];

// Create a request with this configuration and start a task

NSURLSession* session = [NSURLSession sessionWithConfiguration:config];

NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://google.com"]];

NSURLSessionDataTask* task = [session dataTaskWithRequest:request];

[task resume];

In my custom NSURLProtocol that is registered

- (void)startLoading {
    ...

    // po [self.request valueForHTTPHeaderField:@"Authorization"] returns 1234
    //
    // However, I'm very confused why
    //
    //  - allowsCellularAccess
    //  - cachePolicy
    //  - HTTPShouldHandleCookies
    //  - networkServiceType
    //  - timeoutInterval
    //
    // for the request return the default values unlike for the header

    ...

}

Is there some way to check that those parameters I've set are obeyed and inherited by the request?

wesshi
  • 231
  • 2
  • 11

1 Answers1

0

When dealing with http requests, it is helpful to start with the basics, such as is the request actually being made by the OS, and is a response being received? This will in part help to answer your question about checking that set parameters are infact being obeyed by the request.

I would challenge your use of the word "inherit" in the phrase

Is there some way to check that those parameters I've set are obeyed and inherited by the request?

Inheritance in Object Oriented programming has a very specific meaning. Did you in fact create a custom subclass (let's call it SubClassA) of NSURLRequest with specific properties, and then a further subclass (let's call it SubClassB), and are expecting the second subclass (SubClassB) to inherit properties from its parent (SubClassA)? If so, this is certainly not indicated in the code you provided.

There are several HTTP Proxy programs available which help confirm whether or not the HTTP request is being sent, if a response is received, and also which allow you to inspect the details of the request and the response. Charles HTTP Proxy is one such program. Using Charles, I was able to determine that your code as provided is not making any HTTP request. So you cannot confirm or deny any parameters if the request is not being made.

By commenting out the lines including the CustomProtocol as part of the NSURLSession configuration, and running your code either with or without these lines, I gained some potentially valuable information:

  1. by commenting out the lines including the CustomProtocol, a request was in fact made (and failed), as informed by Charles HTTP Proxy. I also added a completion block to your method dataTaskWithRequest. This completion block is hit when the CustomProtocol configuration lines are commented out. The CustomProtocol's startLoading method is not hit.
  2. when leaving in the original lines to configure the NSURLSession using the CustomProtocol, there was no request recorded by Charles HTTP Proxy, and the completion handler is not hit. However, the CustomProtocol's startLoading method is hit.

Please see code below (modifications made to the code posted in the original question).

NSURLSessionConfiguration* config = [NSURLSessionConfiguration defaultSessionConfiguration];

NSMutableArray *protocolsArray = [NSMutableArray arrayWithArray:config.protocolClasses];

//[protocolsArray insertObject:[CustomProtocol class] atIndex:0];

//config.protocolClasses = protocolsArray;

// ex. set some random parameters

[config setHTTPAdditionalHeaders:@{@"Authorization":@"1234"}];
[config setAllowsCellularAccess:NO];
[config setRequestCachePolicy:NSURLRequestReturnCacheDataElseLoad];
[config setHTTPShouldSetCookies:NO];
[config setNetworkServiceType:NSURLNetworkServiceTypeVoice];
[config setTimeoutIntervalForRequest:4321];

// Create a request with this configuration and start a task

NSURLSession* session = [NSURLSession sessionWithConfiguration:config];

NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://google.com"]];

NSURLSessionDataTask* task = [session dataTaskWithRequest:request
                                        completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
                                            NSString * auth = [request valueForHTTPHeaderField:@"Authorization"];
                                            NSLog(@"Authorization: %@", auth);
                                            BOOL allowsCellular = [request allowsCellularAccess];
                                            NSString * allowsCellularString = allowsCellular ? @"YES" : @"NO";
                                            NSLog(@"Allows cellular: %@", allowsCellularString);
}];

[task resume];

This gives you the information that the CustomProtocol is not properly handling the request. Yes, the breakpoint inside the startLoading method is hit when the CustomProtocol is configured as part of the NSURLSession, but that is not definitive proof that the CustomProtocol is handling the request properly. There are many steps necessary to using a CustomProtocol, as outlined by Apple (Protocol Support, NSURLProtocol Class Reference) that you should confirm you are following.

Some things to make sure are working:

  • if you are using a CustomProtocol, that means you are likely trying to handle a different protocol other than http, https, ftp, ftps, etc.
  • make sure that your end point (the server which is listening for the http requests and responding) can actually accept the request and reply.
  • if you are setting an HTTP Authorization Header, make sure that the server can respond appropriately, and that the credentials are valid if you are expecting a positive response
  • remember to register your CustomProtocol

for example:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    [NSURLProtocol registerClass:[CustomProtocol class]];
    return YES;
}

Below is a unit tests to verify that the NSURLSession is functioning as expected (without using our custom protocol explicitly). Note that this unit test does pass when added to Apple's own sample code for the project CustomHTTPProtocol, but does not pass using our very bare bones CustomProtocol

- (void)testNSURLSession {
    XCTestExpectation *expectation = [self expectationWithDescription:@"Testing standard NSURL Session"];

    [[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:@"https://www.apple.com/"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
#pragma unused(data)
        XCTAssertNil(error, @"NSURLSession test failed with error: %@", error);
        if (error == nil) {
            NSLog(@"success:%zd / %@", (ssize_t) [(NSHTTPURLResponse *) response statusCode], [response URL]);
            [expectation fulfill];
        }
    }] resume];

    [self waitForExpectationsWithTimeout:3.0 handler:^(NSError * _Nullable error) {
        if(nil != error) {
            XCTFail(@"NSURLSession test failed with error: %@", error);
        }
    }];
}

Below is a unit test which may be used to verify that the configurations made to a NSURLSession are as expected, when configuring using our own CustomProtocol class. Again, please note that this test fails using the empty implementation of CustomProtocol but this is expected if using Test Driven Development (create the test first, and then the code second which will allow the test to pass).

- (void)testCustomProtocol {
    XCTestExpectation *expectation = [self expectationWithDescription:@"Testing Custom Protocol"];

    NSURLSessionConfiguration* config = [NSURLSessionConfiguration defaultSessionConfiguration];
    NSMutableArray *protocolsArray = [NSMutableArray arrayWithArray:config.protocolClasses];

    [protocolsArray insertObject:[CustomProtocol class] atIndex:0];

    config.protocolClasses = protocolsArray;

    // ex. set some random parameters

    [config setHTTPAdditionalHeaders:@{@"Authorization":@"1234"}];
    [config setAllowsCellularAccess:NO];
    [config setRequestCachePolicy:NSURLRequestReturnCacheDataElseLoad];
    [config setHTTPShouldSetCookies:NO];
    [config setNetworkServiceType:NSURLNetworkServiceTypeVoice];
    [config setTimeoutIntervalForRequest:4321];

    // Create a request with this configuration and start a task
    NSURLSession* session = [NSURLSession sessionWithConfiguration:config];
    NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://www.apple.com"]];

    NSURLSessionDataTask* task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
#pragma unused(data)
        XCTAssertNil(error, @"test failed: %@", error.description);
        if (error == nil) {
            NSLog(@"success:%zd / %@", (ssize_t) [(NSHTTPURLResponse *) response statusCode], [response URL]);
            NSString * auth = [request valueForHTTPHeaderField:@"Authorization"];
            NSLog(@"Authorization: %@", auth);
            XCTAssertNotNil(auth);

            BOOL allowsCellular = [request allowsCellularAccess];
            XCTAssertTrue(allowsCellular);

            XCTAssertEqual([request cachePolicy], NSURLRequestReturnCacheDataElseLoad);

            BOOL shouldSetCookies = [request HTTPShouldHandleCookies];
            XCTAssertTrue(shouldSetCookies);

            XCTAssertEqual([request networkServiceType], NSURLNetworkServiceTypeVoice);

            NSTimeInterval timeOutInterval = [request timeoutInterval];
            XCTAssertEqualWithAccuracy(timeOutInterval, 4321, 0.01);
            [expectation fulfill];

        }

    }];

    [task resume];

    [self waitForExpectationsWithTimeout:3.0 handler:^(NSError * _Nullable error) {
        if(nil != error) {
            XCTFail(@"Custom Protocol test failed with error: %@", error);
        }
    }];
}
Jason Cross
  • 1,721
  • 16
  • 11
  • It seems even when using a completionHandler for a request that is successful, NSLog(@"Allows cellular: %@", allowsCellularString) will return YES even though setAllowsCellularAccess is NO in the session configuration. The same applies to the other parameters set. Really appreciate your answer but still confused if I can check that these parameters set on the configuration are used by the request without having to manually test it. – wesshi Dec 16 '15 at 17:08
  • Thanks for your feedback and clarification. I would like to try and help more but I'm unsure what type of a solution you have in mind. You have stated you would like to "check that the parameters are set ... **without** having to manually test it". This is a logical contradiction. It's like saying "I want to know how it tastes without tasting it", "I want to know what's inside the box without opening it", or "I want to know if this old car will run without starting it". Is there another means you are looking for (besides testing) to inform if the parameters are set? – Jason Cross Dec 16 '15 at 18:45
  • While I do manually check, my purpose would be to create unit tests to check that these parameters are what I'd expect them to be so that I don't have to manually check. – wesshi Dec 16 '15 at 22:06
  • O.K. I understand. I may be able to come up with some unit tests that verify the parameters are correct. Are you able to post your code related to the custom `NSURLProtocol` subclass, such as the implementation (.m file) for `CustomProtocol` ? – Jason Cross Dec 17 '15 at 01:41
  • Hey Jason, the code very closely resembles Apple's example for a custom protocol @ https://developer.apple.com/library/ios/samplecode/CustomHTTPProtocol/Listings/CustomHTTPProtocol_Core_Code_CustomHTTPProtocol_m.html#//apple_ref/doc/uid/DTS40013653-CustomHTTPProtocol_Core_Code_CustomHTTPProtocol_m-DontLinkElementID_10 Appreciate your time but please don't feel obligated! I'm still looking into it myself and if I come up with a good way to unit test / confirm these parameters then I'll make an answer too – wesshi Dec 17 '15 at 02:11
  • Hi @user3657391, I have updated my answer to include unit tests. As I posted, these tests fail using the empty implementation I created for `CustomProtocol` but the first test does indeed pass (and the second test fails but at least the response returns without error) when using Apples code and their `CustomHTTPProtocol` object. – Jason Cross Dec 23 '15 at 19:31
  • Ah I didn't realize you replied and since then I've decided not to go with that route of testing. The thing with checking the request in the completionHandler of the data task is that I'm can't be sure that the requests are actually the same. In the NSURLProtocol, we'll often create our own new requests or tasks. – wesshi Jan 09 '16 at 01:26
  • Hi, no problem. Even though you no longer need a solution, does the answer provided satisfy the original question? If so, please consider "accepting" the answer. Thanks. – Jason Cross Jan 18 '16 at 06:22
  • @JasonCross I too was interested in the same question. I wondered whether configuration settings _override_ corresponding settings in the request which has been created separately. Even in the case for standard protocols, the answer seems to be "No". That is, session configuration settings do _not_ override the settings in the request once the task has been created or resumed. I verified this with retrieving the request _from the session task_ via `task.currentRequest` and `task.originalRequest` - since the request will be copied in the call to `dataTaskWithRequest:`. – CouchDeveloper Feb 04 '16 at 12:39