NSURLProtocol isn't asked to load after YES response to canInitWithRequest
Asked Answered
G

2

1

I'm registering an implementation of NSURLProtocol to do some custom handling of certain URL requests (I tag these requests by setting properties on them). These URL requests are coming from a UIWebView's load method.

  1. Create a UI web view (first load after launch)
  2. Load content
  3. Destroy web view
  4. Create a new web view (subsequent loads)
  5. Load content

I'm seeing significantly different behavior between steps 2 and 5. My NSURLProtocol implementation manages cached data and is designed to handle the requests. If I don't detect my property in a request, I do a second check for a specific URL (for debugging). In either case canInitWithRequest will return YES:

First load after launch:

2014-10-15 11:37:11.403 MYApp[5813:60b] MYURLProtocol canInitWithRequest: Matched property in request: <0x15ebbb40> https://example.com/ 
2014-10-15 11:37:11.404 MYApp[5813:60b] MYURLProtocol canInitWithRequest: Matched property in request: <0x15dc8da0> https://example.com/ 
2014-10-15 11:37:11.409 MYApp[5813:60b] MYURLProtocol canInitWithRequest: Matched property in request: <0x15ee5ef0> https://example.com/ 
2014-10-15 11:37:11.409 MYApp[5813:60b] MYURLProtocol canInitWithRequest: Matched property in request: <0x15ee6240> https://example.com/ 
2014-10-15 11:37:11.410 MYApp[5813:60b] MYURLProtocol initWithRequest:cachedResponse:client: Request: https://example.com/
2014-10-15 11:37:11.411 MYApp[5813:60b] MYURLProtocol canInitWithRequest: Matched property in request: <0x15ee69d0> https://example.com/ 
2014-10-15 11:37:11.415 MYApp[5813:9c07] MYURLProtocol startLoading Loading <0x15ee6240> https://example.com/ 
... A bunch of loading of assets occurs (cached responses) ...
2014-10-15 11:37:12.497 MYApp[5813:60b] MyWebViewController webViewDidFinishLoad: Finished loading

Others have pointed out that there are multiple calls to the protocol about the same asset, and this is not a concern, though it's interesting to note that each time it is called with a new object, and it is the 4th (of 4) object that gets passed to startLoading. Still, no real concerns here.

Subsequent loads:

2014-10-15 11:11:27.466 MYApp[5782:60b] MYURLProtocol canInitWithRequest: Matched property in request: <0x1727c310> https://example.com/ 
2014-10-15 11:11:27.467 MYApp[5782:60b] MYURLProtocol canInitWithRequest: Matched property in request: <0x145b1d90> https://example.com/ 
2014-10-15 11:11:27.488 MYApp[5782:560f] MYURLProtocol canInitWithRequest: Matched URL in request: <0x17266060> https://example.com/
2014-10-15 11:11:27.669 MYApp[5782:60b] MYWebViewController webViewDidFinishLoad: Finished loading

This is where the behavior, in my view, is unexpected. It appears that property has been stripped off of the request by the third time it is passed to canInitWithRequest, and then, even though we respond YES we never actually get inited -- the page is simply returned to the UIWebView in its entirety, with no subsequent requests for assets. Here is what the request looks like when it is created:

NSURLRequest *request = [NSURLRequest requestWithURL:myURL cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:60.0];
[self.webView loadRequest:request];

Why, when I say that my protocol can handle the request, is it not being given the opportunity to do so? My guess is that the answer is in the implementation of UIWebView itself. Any thoughts on how to work around this if I really want my protocol to be the responsible entity for loading?

Gaullist answered 15/10, 2014 at 18:56 Comment(2)
This is probably relevant, though there is not equivalent for UIWebView. I cannot move entirely to WKWebView yet. https://mcmap.net/q/219930/-how-do-you-clear-the-webview-cache-in-cocoaGaullist
I'm going to try to cache-bust whatever UIWebView's caching mechanism is by appending a random query param onto the request which I'll strip in the internals of my protocol. I'll update this question with my findings.Gaullist
G
0

I was able to workaround this issue by cache-busting the UIWebView cache, while not busting the NSURLCache.

  1. Add a unique param to the query params of the original request. I chose 'key=000000' where the value is zero-led six digit random number.
  2. In the protocol, strip the key in canonicalRequestForRequest: and in initWithRequest:cachedResponse:client

My stripping code looks like this (there might be a cleaner way to strip the param, but this works):

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
    NSURLRequest *canonicalRequest = request;
    BOOL myProtocolRequest = [[NSURLProtocol propertyForKey:kMYProtocolRequest inRequest:request] boolValue];
    if (myProtocolRequest)
    {
        NSMutableURLRequest *mutableRequest = [request mutableCopyWorkaround];
        NSString *originalURLString = mutableRequest.URL.absoluteString;
        NSString *regexString = [NSString stringWithFormat:@"(?:[?&])(key=[[:digit:]]{%d}&*)", kMYKeyLength];

        NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:regexString options:0 error:0];
        NSTextCheckingResult *result = [regex firstMatchInString:originalURLString options:0 range:NSMakeRange(0, originalURLString.length)];
        if (result.numberOfRanges > 1)
        {
            NSRange keyRange = [result rangeAtIndex:1];
            NSLog(@"Removing '%@' from request", [originalURLString substringWithRange:keyRange]);
            NSString *replacementURLString = [originalURLString stringByReplacingCharactersInRange:keyRange withString:@""];
            mutableRequest.URL = [NSURL URLWithString:replacementURLString];
            canonicalRequest = mutableRequest;
        }
    }

    return canonicalRequest;
}

My init code looks like this:

- (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id<NSURLProtocolClient>)client
{
    self = [super initWithRequest:[MYURLProtocol canonicalRequestForRequest:request] cachedResponse:cachedResponse client:client];
    return self;
}

I don't like that I have to do this, but I'm finally getting exactly the behavior I want. Hopefully it helps someone out there.

Gaullist answered 16/10, 2014 at 15:36 Comment(2)
Can you please tell me where you put the init method. If possible can you tell me how you load the request in webview.Allocation
@Allocation The init method is in my subclass of NSURLProtocol. Before loading my web view I register it: [NSURLProtocol registerClass:[MYURLProtocol class]]; I think use UIWebView as normal.Gaullist
T
1
*** Way to clear the cache of webview **********
Follow this code:

NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
    for (NSHTTPCookie *cookie in [storage cookies]) {
        [storage deleteCookie:cookie];
    }
    [[NSUserDefaults standardUserDefaults] synchronize];
Tanah answered 27/12, 2016 at 8:44 Comment(2)
Welcome to StackOverflow! Definitely try to recreate the complete case of the question to make sure your answer works before posting. Your code addresses clearing cookies and contains an extraneous call to NSUserDefaults. What I was attempting is clearing all cached content.Gaullist
That was really helpful, I didn't think about cache.Erbe
G
0

I was able to workaround this issue by cache-busting the UIWebView cache, while not busting the NSURLCache.

  1. Add a unique param to the query params of the original request. I chose 'key=000000' where the value is zero-led six digit random number.
  2. In the protocol, strip the key in canonicalRequestForRequest: and in initWithRequest:cachedResponse:client

My stripping code looks like this (there might be a cleaner way to strip the param, but this works):

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
    NSURLRequest *canonicalRequest = request;
    BOOL myProtocolRequest = [[NSURLProtocol propertyForKey:kMYProtocolRequest inRequest:request] boolValue];
    if (myProtocolRequest)
    {
        NSMutableURLRequest *mutableRequest = [request mutableCopyWorkaround];
        NSString *originalURLString = mutableRequest.URL.absoluteString;
        NSString *regexString = [NSString stringWithFormat:@"(?:[?&])(key=[[:digit:]]{%d}&*)", kMYKeyLength];

        NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:regexString options:0 error:0];
        NSTextCheckingResult *result = [regex firstMatchInString:originalURLString options:0 range:NSMakeRange(0, originalURLString.length)];
        if (result.numberOfRanges > 1)
        {
            NSRange keyRange = [result rangeAtIndex:1];
            NSLog(@"Removing '%@' from request", [originalURLString substringWithRange:keyRange]);
            NSString *replacementURLString = [originalURLString stringByReplacingCharactersInRange:keyRange withString:@""];
            mutableRequest.URL = [NSURL URLWithString:replacementURLString];
            canonicalRequest = mutableRequest;
        }
    }

    return canonicalRequest;
}

My init code looks like this:

- (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id<NSURLProtocolClient>)client
{
    self = [super initWithRequest:[MYURLProtocol canonicalRequestForRequest:request] cachedResponse:cachedResponse client:client];
    return self;
}

I don't like that I have to do this, but I'm finally getting exactly the behavior I want. Hopefully it helps someone out there.

Gaullist answered 16/10, 2014 at 15:36 Comment(2)
Can you please tell me where you put the init method. If possible can you tell me how you load the request in webview.Allocation
@Allocation The init method is in my subclass of NSURLProtocol. Before loading my web view I register it: [NSURLProtocol registerClass:[MYURLProtocol class]]; I think use UIWebView as normal.Gaullist

© 2022 - 2024 — McMap. All rights reserved.