0

I am subclassing NSURLProtocol to intercept HTTP request.

Here is the custom NSURLProtocol class.

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
    if (NSOrderedSame != [request.URL.scheme caseInsensitiveCompare:@"http"] &&
        NSOrderedSame != [request.URL.scheme caseInsensitiveCompare:@"https"]) {
        return NO;
    }

    if ([NSURLProtocol propertyForKey:kURLProtocolRequestHandledKey inRequest:request] ) {
        return NO;
    }

    return YES;
}

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
    NSMutableURLRequest *mutableReqeust = [request mutableCopy];
    [NSURLProtocol setProperty:@YES forKey:kURLProtocolRequestHandledKey inRequest:mutableReqeust];
    return [mutableReqeust copy];
}

- (void)startLoading {
    self.startDate = [NSDate date];
    self.data = [NSMutableData data];
    self.error = nil;

    self.connection = [[NSURLConnection alloc] initWithRequest:[[self class] canonicalRequestForRequest:self.request] delegate:self startImmediately:YES];
}

- (void)stopLoading {
    [self.connection cancel];
}

#pragma mark - NSURLConnectionDelegate

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    [[self client] URLProtocol:self didFailWithError:error];
    self.error = error;
}

- (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection *)connection {
    return YES;
}

- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
    [[self client] URLProtocol:self didReceiveAuthenticationChallenge:challenge];
}

- (void)connection:(NSURLConnection *)connection didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
    [[self client] URLProtocol:self didCancelAuthenticationChallenge:challenge];
}

#pragma mark - NSURLConnectionDataDelegate

- (NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)response {
    if (response != nil){
        _response = response;
        NSMutableURLRequest *redirect = [request mutableCopy];
        redirect.URL = request.URL;
        [NSURLProtocol setProperty:@NO forKey:kURLProtocolRequestHandledKey inRequest:redirect];
        [[self client] URLProtocol:self wasRedirectedToRequest:redirect redirectResponse:response];

        return redirect;
    }
    return request;
}

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];
    _response = response;
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    [[self client] URLProtocol:self didLoadData:data];
    [self.data appendData:data];
}

- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse {
    return cachedResponse;
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    [[self client] URLProtocolDidFinishLoading:self];
}

- (void)connection:(NSURLConnection *)connection didSendBodyData:(NSInteger)bytesWritten totalBytesWritten:(NSInteger)totalBytesWritten totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite {

}

I add a UIWebView as a subView and then loads a URL http://ln.clientaccess.10086.cn/shop/optical/Appointment?channel=007&PHONE_NUM=18240235054&AREA_CODE=240&key=4A35774433BA79EB950EDE4B5C4D7121 , after the controller is dismissed , the APP is frozen even though I call - stopLoading on webView before it's dismissed.

Here is the thread stack:

enter image description here

Weizhi
  • 174
  • 4
  • 22

2 Answers2

0

I don't think you want to modify the request in canonicalRequest -- leave that alone. You do want to modify it in startLoading, and use the modified request in the new call to NSURLConnection, so that your protocol does not process it again during that call.

The second thing is the redirect implementation is likely wrong -- that method gets called in two situations; once when the request is being sent (and the redirect is nil); you want to return the request in that situation (which you do). The second is when you actually get a redirect; you want to call the client (which you are) but then you want to return nil, in order to let the client actually handle the redirect (otherwise the non-nil return can indicate that the redirect has been handled).

I'm not sure that either of those would cause the problem, but they are differences.

The only other thing I see different from ones I've implemented is the startImmediately:YES . That is the default though, so that shouldn't be an issue. Maybe try avoid caching and see if that helps, if neither of the above do. Or make sure to call -cancel on the connection in dealloc.

Carl Lindberg
  • 2,902
  • 18
  • 22
  • Thanks for your reply. The redirect implementation is ok. I have read some `NSURLProtocol` tutorials also call `startImmediately:YES`, I think this's not the problem. – Weizhi Feb 15 '16 at 08:31
  • I've spent more time on NSURLProtocol classes than I care to admit... I believe if you don't return nil with the redirect, it is not passed back down to the original NSURLConnection delegate. I've tested with a stack of protocols, quite carefully. I don't see many tutorials get it right. On the other hand, if you are not actually getting redirects, that wouldn't be an issue. If you expand out the backtrace more, it might help -- you are getting a deadlock, but it's not clear if the main thread is deadlocking itself or another thread. – Carl Lindberg Feb 15 '16 at 11:44
  • WebView starts the request in `viewDidLoad`: `- (void)viewDidLoad { [super viewDidLoad]; NSString *URLString = @"http://ln.clientaccess.10086.cn/shop/optical/Appointment?channel=007&PHONE_NUM=18240235054&AREA_CODE=240&key=4A35774433BA79EB950EDE4B5C4D7121"; NSURL *URL = [NSURL URLWithString:URLString]; [[self webView] loadRequest:[NSURLRequest requestWithURL:URL]]; }` The URL has 66 requests and does not have redirect URL, I think redirect is not a problem. – Weizhi Feb 16 '16 at 00:35
  • If that is your code, you are not specifying a scheme on the URL. And while you are seemingly comparing the scheme in your protocol, if the URL has a nil scheme, the result of the comparison call (message to a nil object) will be 0 -- so it will compare and invoke the protocol. You should guard against a nil scheme value in the URL in canInitWithRequest. Oh, I see you just edited the comment ;-) – Carl Lindberg Feb 16 '16 at 00:44
0

Use startImmediately:NO instead of startImmediately:YES will solve this problem.

    if (currentRunLoop && [currentRunLoop currentMode]) {
        self.connection = [[NSURLConnection alloc] initWithRequest:[[self class] canonicalRequestForRequest:self.request] delegate:self startImmediately:NO];
        [self.connection scheduleInRunLoop:currentRunLoop forMode:[[NSRunLoop currentRunLoop] currentMode]];
        [self.connection start];

    } else {
        self.connection = [[NSURLConnection alloc] initWithRequest:[[self class] canonicalRequestForRequest:self.request] delegate:self startImmediately:YES];
    }

It's wired. Will someone please tell me why?

Weizhi
  • 174
  • 4
  • 22
  • If you schedule it with startImmediately:YES, the connection is scheduled in the default run loop with the default mode. If you want to run it on anything other than the main run loop, you have to defer starting it until you've scheduled it on the right run loop. Not sure if that answers your question or not. – dgatwood May 08 '16 at 02:30