Custom NSURLProtocol slower after switching NSURLConnection to NSURLSession - ios

I have a custom NSURLProtocol ("UrlProtocol") written to intercept requests from a UIWebView when navigating to specific websites, and apply an extra HTTP header before being sent. I followed https://www.raywenderlich.com/59982/nsurlprotocol-tutorial for a working class. My problem comes with switching from the deprecated NSURLConnection to NSURLSession: I tested an extremely simple one-file html page, which loaded successfully. However, slightly more complex sites with resources like js files, images, etc. will timeout, whereas using NSURLConnection the entire site will load within a few seconds.
I'll paste the original UrlProtocol using NSURLConnection, then the new class using NSURLSession. The original:
#import "UrlProtocol.h"
#import "Globals.h"
#implementation UrlProtocol
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
if (![request.URL.absoluteString hasPrefix:#"http"]) return NO; //No need to intercept non-http requests
if ([NSURLProtocol propertyForKey:#"handled" inRequest:request]) {
return NO;
}
return YES;
}
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
NSString* key = #"custom-auth-header";
Globals* globals = [Globals getInstance];
NSString* token = [globals token];
NSMutableURLRequest *newRequest = [request mutableCopy]; //Create a mutable copy that can be modified
[newRequest setValue:token forHTTPHeaderField:key];
return [newRequest copy]; //return a non-mutable copy
}
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b {
return [super requestIsCacheEquivalent:a toRequest:b];
}
- (void)startLoading {
NSMutableURLRequest *newRequest = [self.request mutableCopy];
[NSURLProtocol setProperty:#YES forKey:#"handled" inRequest:newRequest];
self.connection = [NSURLConnection connectionWithRequest:newRequest delegate:self];
}
- (void)stopLoading {
NSLog(#"stopLoading");
[self.connection cancel];
self.connection = nil;
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[self.client URLProtocol:self didLoadData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
[self.client URLProtocolDidFinishLoading:self];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
[self.client URLProtocol:self didFailWithError:error];
}
#end
The new UrlProtocol using NSURLSessionDataTasks for every request:
#import "UrlProtocol.h"
#import "Globals.h"
#implementation UrlProtocol
+ (BOOL)canInitWithRequest:(NSURLRequest * _Nonnull) request {
if (![request.URL.absoluteString hasPrefix:#"http"]) return NO; //No need to intercept non-http requests
if ([NSURLProtocol propertyForKey:#"handled" inRequest:request]) {
return NO;
}
return YES;
}
+ (NSURLRequest * _Nonnull)canonicalRequestForRequest:(NSURLRequest * _Nonnull)request {
NSString* key = #"custom-auth-header";
Globals* globals = [Globals getInstance];
NSString* token = [globals token];
NSMutableURLRequest *newRequest = [request mutableCopy]; //Create a mutable copy that can be modified
[newRequest setValue:token forHTTPHeaderField:key];
return [newRequest copy]; //return a non-mutable copy
}
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest * _Nonnull)a toRequest:(NSURLRequest * _Nonnull)b {
return [super requestIsCacheEquivalent:a toRequest:b];
}
- (void)startLoading {
NSMutableURLRequest *newRequest = [self.request mutableCopy];
[NSURLProtocol setProperty:#YES forKey:#"handled" inRequest:newRequest];
[Globals setUrlSessionDelegate:self];
Globals* globals = [Globals getInstance];
self.dataTask = [globals.session dataTaskWithRequest:newRequest];
[self.dataTask resume];
}
- (void)URLSession:(NSURLSession * _Nonnull)session dataTask:(NSURLSessionDataTask * _Nullable)dataTask didReceiveData:(NSData * _Nullable)data{
[self.client URLProtocol:self didLoadData:data];
}
- (void)URLSession:(NSURLSession * _Nonnull)session dataTask:(NSURLSessionDataTask * _Nullable)dataTask didReceiveResponse:(NSURLResponse * _Nullable)response
completionHandler:(void (^ _Nullable)(NSURLSessionResponseDisposition))completionHandler{
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
completionHandler(NSURLSessionResponseAllow);
}
- (void)URLSession:(NSURLSession * _Nonnull)session task:(NSURLSessionTask * _Nonnull)task didCompleteWithError:(NSError * _Nullable)error{
if (error){
[self.client URLProtocol:self didFailWithError:error];
} else {
[self.client URLProtocolDidFinishLoading:self];
}
}
- (void)URLSession:(NSURLSession * _Nonnull)session task:(NSURLSessionTask * _Nonnull)task willPerformHTTPRedirection:(NSHTTPURLResponse * _Nonnull)response
newRequest:(NSURLRequest * _Nonnull)request completionHandler:(void (^ _Nonnull)(NSURLRequest * _Nullable))completionHandler {
completionHandler(request);
}
- (void)stopLoading {
[self.dataTask cancel];
self.dataTask = nil;
}
#end
"Globals" is a singleton where I have initialized one NSURLSession meant to be used throughout the runtime of the app. It also contains the token I set as a custom HTTP header for all requests:
#import "Globals.h"
#import "UrlProtocol.h"
#implementation Globals
#synthesize token;
#synthesize session;
static Globals *instance = nil;
+(Globals*) getInstance
{
#synchronized(self)
{
if (instance == nil)
{
instance = [Globals new];
}
}
return instance;
}
//UrlProtocol class has no init method, so the NSURLSession delegate is being set on url load. We will ensure only one NSURLSession is created.
+(void) setUrlSessionDelegate:(UrlProtocol*) urlProtocol{
Globals* globals = [Globals getInstance];
if (!globals.session){
globals.session = [NSURLSession sessionWithConfiguration:NSURLSessionConfiguration.defaultSessionConfiguration delegate:urlProtocol delegateQueue:nil];
}
}
#end

Solved my problem by creating a new default NSURLSession for each NSURLSessionDataTask. Something was wrong with the way I was trying to share one NSURLSession for all my tasks. URLProtocol's startLoading method is now as follows:
- (void)startLoading {
NSMutableURLRequest *newRequest = [self.request mutableCopy];
[NSURLProtocol setProperty:#YES forKey:#"handled" inRequest:newRequest];
NSURLSessionConfiguration* config = NSURLSessionConfiguration.defaultSessionConfiguration;
NSURLSession* session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
self.dataTask = [session dataTaskWithRequest:newRequest];
[self.dataTask resume];
}
The simple HTML page test before must have worked because only one task was needed to load the page

Related

Unable to load video stream in

I am new to iOS development,
I want to stream video from webapi, The files are streamed from our backend server which requires authentication. It is key-based authenticated set in the Authorization HTTP Header.
I tried with AVPlayer didn't got my output. After doing some more research, i found customProtocol will be more useful do it so i have used customProtocol and tried with this code but the customProtocol is not getting called customProtocol.h, customProtocol.m
[NSURLProtocol registerClass:[MyCustomProtocol class]];
NSString *theURLString = #"customProtocol://abcd.com/download";
player = [[MPMoviePlayerController alloc] initWithContentURL:[NSURL URLWithString:theURLString]];
[self.view addSubview:player.view];
player.view.frame = self.view.frame;
[player play];
Can any one help me here? where i a making mistake?
Thank you in advance!
This is my customProtocol code :
#implementation MyCustomProtocol
+ (BOOL) canInitWithRequest:(NSURLRequest *)request {
NSURL* theURL = request.URL;
NSString* scheme = theURL.scheme;
if([scheme isEqualToString:#"customProtocol"]) {
return YES;
}
return NO;
}
// You could modify the request here, but I'm doing my legwork in startLoading
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
return request;
}
// I'm not doing any custom cache work
+ (BOOL) requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b {
return [super requestIsCacheEquivalent:a toRequest:b];
}
// This is where I inject my header
// I take the handled request, add a header, and turn it back into http
// Then I fire it off
- (void) startLoading {
NSMutableURLRequest* mutableRequest = [self.request mutableCopy];
Constants *constants = [Constants sharedInstance];
[mutableRequest setValue:[NSString stringWithFormat:#"Bearer %#",constants.access_token] forHTTPHeaderField:#"Authorization"];
NSURL* newUrl = [[NSURL alloc] initWithScheme:#"http" host:[mutableRequest.URL host] path:[mutableRequest.URL path]];
[mutableRequest setURL:newUrl];
self.connection = [NSURLConnection connectionWithRequest:mutableRequest delegate:self];
}
- (void) stopLoading {
[self.connection cancel];
}
// Below are boilerplate delegate implementations
// They are responsible for letting our client (the MPMovePlayerController) what happened
- (void) connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
[self.client URLProtocol:self didFailWithError:error];
self.connection = nil;
}
- (void) connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[self.client URLProtocol:self didLoadData:data];
}
- (void) connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];
}
- (void) connectionDidFinishLoading:(NSURLConnection *)connection {
[self.client URLProtocolDidFinishLoading:self];
self.connection = nil;
}
if you have passed authentication header with url then you can use below code
NSMutableDictionary * headers = [NSMutableDictionary dictionary];
[headers setObject:#"Your UA" forKey:#"User-Agent"];
AVURLAsset * asset = [AVURLAsset URLAssetWithURL:URL options:#{#"AVURLAssetHTTPHeaderFieldsKey" : headers}];
AVPlayerItem * item = [AVPlayerItem playerItemWithAsset:asset];
self.player = [[AVPlayer alloc] initWithPlayerItem:item];
it may work...try it
MPMoviePlayerViewController * movieController = [[MPMoviePlayerViewController alloc] initWithContentURL:url];
movieController.moviePlayer.movieSourceType = MPMovieSourceTypeStreaming;
[self presentMoviePlayerViewControllerAnimated:movieController];
[movieController.moviePlayer play];

Using NSURLSession inside NSURLProtocol

I'm trying to create a transparent NSURLProtocol for http:// and https:// connections using NSURLSession. However at the moment, even though the completion handler is being run, URL requests with the app (UIWebView) are coming back blank. Does anybody have any ideas? Code is below:
#import "MyURLProtocol.h"
// AppDelegate
#import "AppDelegate.h"
static NSString * const MyURLProtocolHandledKey = #"MyURLProtocolHandledKey";
#interface MyURLProtocol () <NSURLConnectionDelegate,NSURLSessionDelegate>
#property (nonatomic, strong) NSURLConnection *connection;
#property (nonatomic, strong) NSMutableData *mutableData;
#property (nonatomic, strong) NSURLResponse *response;
#end
#implementation MyURLProtocol
+(BOOL)canInitWithRequest:(NSURLRequest*)request
{
if ([NSURLProtocol propertyForKey:MyURLProtocolHandledKey inRequest:request])
return NO;
NSString *scheme = request.URL.scheme.lowercaseString;
return [scheme isEqualToString:#"http"] || [scheme isEqualToString:#"https"];
}
+ (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request {
return request;
}
-(void)startLoading
{
NSMutableURLRequest *newRequest = [self.request mutableCopy];
[NSURLProtocol setProperty:#YES forKey:#"MyURLProtocolHandledKey" inRequest:newRequest];
NSURLRequest *request = newRequest;
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request
completionHandler:
^(NSData *data, NSURLResponse *response, NSError *error) {
if (error != nil) {
NSLog(#"There was an error");
}
NSLog(#"Completiio handler ran");
self.mutableData = [NSMutableData dataWithData:data];
self.response = response;
}];
[task resume];
}
- (void) stopLoading {
[self.connection cancel];
self.mutableData = nil;
}
// Delegate stuff
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[self.client URLProtocol:self didLoadData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
[self.client URLProtocolDidFinishLoading:self];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
[self.client URLProtocol:self didFailWithError:error];
}
#end
Your code is using NSURLConnection delegates to pass data back to the caller, e.g. connectionDidFinishLoading:.
To fix this:
Replace these with NSURLSession delegate methods.
Create and retain a custom session whose delegate is your protocol class instance; the shared session doesn't have a delegate, so it won't call your class's delegate methods.
Remove the callback block so that the delegate method for request completion will be called correctly.

NSURLSession delegate implementation returns only <head> of html

Let's go straight into code.
First in appDelegate, I register my connection protocol:
[NSURLProtocol registerClass:[MyURLConnection class]];
Here's what my connection class looks like:
+(BOOL)canInitWithRequest:(NSURLRequest *)request {
return YES;
}
-(void)startLoading {
if (self.cachedResponse) {
NSLog(#"there is a cached response %#", self.cachedResponse);
}
MyNetworkSession *task = [MyNetworkSession new];
task.request = self.request;
task.urlClient = self.client;
task.isDataTask = YES;
task.isBackgroundSession = YES;
[task start];
}
-(void)stopLoading {
// nothing here
}
Now, MyNetworkSession is the delegate for NSURLSession tasks, like so:
// ----------- MyNetworkSession.h
#interface MyNetworkSession : NSObject
<NSURLSessionDelegate,
NSURLSessionTaskDelegate,
NSURLSessionDataDelegate>
-(void) start;
#end
//------------ MyNetworkSession.m
#implementation MyNetworkSession {
NSURLSessionDataTask *_dataTask;
NSURLSessionDownloadTask *_downloadTask;
}
// create session
-(NSURLSession*) session {
static NSURLSession *session = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSURLSessionConfiguration *backgroundConfiguration = [
NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:
BACKGROUND_SESSION_IDENTIFIER];
backgroundConfiguration.requestCachePolicy = NSURLRequestUseProtocolCachePolicy;
session = [NSURLSession
sessionWithConfiguration:backgroundConfiguration
delegate:self
delegateQueue:nil];
});
return session;
}
// start download/data task
-(void)start {
if (self.isDataTask){
_dataTask = [self.session dataTaskWithRequest:self.request];
[_dataTask resume];
}
}
#pragma mark- delegate methods
-(void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error {
if (error) {
NSLog(#"Error in connection %# for task %#", error, task.response.URL);
}
NSLog(#"completed response %#", task.response.URL.absoluteString);
_dataTask = nil;
_downloadTask = nil;
[self.urlClient URLProtocolDidFinishLoading:self.urlClient];
}
-(void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
NSLog(#"received response %#", response.URL.absoluteString);
[self.urlClient URLProtocol:self.urlClient
didReceiveResponse:responsecacheStoragePolicy:NSURLCacheStorageAllowedInMemoryOnly];
completionHandler(NSURLSessionResponseAllow);
}
-(void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data {
NSLog(#"received data %#", dataTask.response.URL.absoluteString);
if (!self.data) {
self.data = [data mutableCopy];
} else {
[self.data appendData:data];
}
[self.urlClient URLProtocol:self.urlClient didLoadData:self.data];
}
//... some more methods
#end
That's all the relevant code I guess.
The problem is that when I see the response in a webview, I only get the <head> tag of the document.
I am not getting the rest of the html. And also, the js/css files listed in the <head> tag are not downloaded. Do you see any flaw in the implementation?

can NSURLProtocol work with NSURLSession {upload, download}dataTask

I have a custom NSURLProtocol
#import <Foundation/Foundation.h>
#interface XXXURLProtocol : NSURLProtocol <NSURLSessionDataDelegate, NSURLSessionTaskDelegate>
#property (nonatomic, strong) NSURLSession *session;
#property (nonatomic, strong) NSURLSessionTask *task;
#end
#import "XXXURLProtocol.h"
#implementation DXYURLProtocol
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
if ([NSURLProtocol propertyForKey:YXURLProtocolHandled inRequest:request]) {
return NO;
}
NSString *scheme = [[request URL] scheme];
NSDictionary *dict = [request allHTTPHeaderFields];
return [dict objectForKey:#"custom_header"] == nil &&
([scheme caseInsensitiveCompare:#"http"] == NSOrderedSame ||
[scheme caseInsensitiveCompare:#"https"] == NSOrderedSame);
}
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
return request;
}
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a
toRequest:(NSURLRequest *)b {
return [super requestIsCacheEquivalent:a toRequest:b];
}
- (void)startLoading
{
NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy];
//add custom headers
[XXXURLProtocol addCustomHeaders:mutableReqeust];
[NSURLProtocol setProperty:#(YES)
forKey:YXURLProtocolHandled
inRequest:mutableReqeust];
NSURLSessionConfiguration *config;
config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.protocolClasses = #[ self ];
self.session = [NSURLSession sessionWithConfiguration:config];
self.task = [self.session dataTaskWithRequest:mutableReqeust];
[self.task resume];
}
- (void)stopLoading
{
[self.task cancel];
self.task = nil;
}
//and all other NSURLProtocolClient protocol method
#end
how to make this custom NSURLProtocol to support {upload, download}dataTask?
You have the right idea, but you need to implement some key URLSessionDelegate methods to pass the response and data up to the NSURLProtocol client:
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
if( error ) {
[self.client URLProtocol:self didFailWithError:error];
} else {
[self.client URLProtocolDidFinishLoading:self];
}
}
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
completionHandler(NSURLSessionResponseAllow);
}
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
[self.client URLProtocol:self didLoadData:data];
}

IOS 7 how to print the url response data

I am trying to call a web service. I tried this
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:#"http://192.168.75.1:8082/projectname/public/tests"]];
NSURLSessionConfiguration *configuration = [ NSURLSessionConfiguration ephemeralSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration];
NSURLSessionDownloadTask *task = [session downloadTaskWithRequest:request completionHandler:^(NSURL *localFile, NSURLResponse *response, NSError *error) {
if(!error){
NSLog(#"no error");
}else{
NSLog(#"error");
}
}];
[task resume];
}
as you see there are two nslog statements. I got the no error one.
when I call that web service from my safari, I got a simple string which is index printed in the browser, how can I see that string in my xcode please?
Thanks
you can implement the delegate method
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location;
EDIT
Try This
NSHTTPURLResponse *response = nil;
NSError *error = nil;
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:YOUR URL]];
NSData *respData = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
NSLog(#"~~~~~ Status code: %d", [response statusCode]);
//Print your recived data here..
NSString *str = [[NSString alloc] initWithData:respData encoding:NSUTF8StringEncoding];
NSLog(#"str: %#", str);
You can use the delegate methods. When a NSURLSessionDownlaodTask is completed, it's delegates will be called if your class confirmed to it.
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
you can get your response by parsing the data in that delegate method. It will tell you the location that the URLSession stores the downloaded result.
what I would do If I were you is:
NOTE: it is based on you said you receive a simple string only from your back-end. if that is not a simple string, you may need to revise the –connectionDidFinishLoading: method's body.
.h
#interface UIRandomViewController : UIViewController {
NSURLConnection *_urlConnection;
NSMutableData *_receivedData;
// ...
}
// ...
#end
.m
#implementation UIRandomViewController {
// ...
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSURLRequest *_request = [NSURLRequest requestWithURL:[NSURL URLWithString:#"http://192.168.75.1:8082/projectname/public/tests"]];
_urlConnection = [[NSURLConnection alloc] initWithRequest:_request delegate:self startImmediately:TRUE];
// ...
}
// ...
#pragma mark - <NSURLConnectionDelegate>
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
_receivedData = nil;
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
if (_receivedData == nil) _receivedData = [NSMutableData dataWithData:data];
else [_receivedData appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
NSString *_receivedString = [[NSString alloc] initWithData:_receivedData encoding:NSUTF8StringEncoding];
// hello beautiful...
NSLog(#"received data : %#", _receivedString);
}
}
#end

Resources