If you want to intervene in network requests, a good choice is to use NSURLProtocol, a feature of the iOS URL Loading System that provides a convenient interface to allow developers to redefine the behavior of network requests, including modifying the request initiation and response actions.

In iOS web development, if there is a need for something like.

  • adding specific header or parameters to global requests.
  • request redirection, MOCK request or use of local cache for certain resources.
  • processing the normal response data of a request, such as filtering keywords, etc.

A good solution is to use NSURLProtocol, a feature of the iOS URL Loading System, which provides a convenient interface to allow developers to redefine the behavior of network requests, including modifying the request initiation and response actions.

Create a new NSURLProtocol

NSURLProtocol is an abstract class that must be subclassed when used.

1
2
@interface EaseapiURLProtocol: NSURLProtocol
@end

Instead of the developer instantiating the NSURLProtocol subclasses themselves, the NSURLProtocol subclasses are registered with the system. There are two ways to register NSURLProtocol subclasses.

  • Using NSURLProtocol’s registerClass interface

    1
    2
    3
    4
    
    //注册
    [NSURLProtocol registerClass:[EaseapiURLProtocol class]]
    //卸载
    [NSURLProtocol unregisterClass:[EaseapiURLProtocol class]];
    

    Applies to network requests created using [NSURLSession sharedSession].

  • Use the protocolClasses interface of NSURLSessionConfiguration

    1
    2
    
    NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
    configuration.protocolClasses = @[[EaseapiURLProtocol class]];
    

    A request that applies to the Configuration of a custom NSURLSession.

After successful registration, the URL loading system checks all registered NSURLProtocol subclasses until it finds one that can handle the request and passes it on to that class before making a request using, for example, NSURLSessionTask.

Multiple NSURLProtocol subclasses can be registered with the same NSURLSessionConfiguration, and the URL loading system traverses them in the reverse order of registration. Once an NSURLProtocol subclass is able to process, it takes over the request and subsequent NSURLProtocol subclasses are not executed. That is, it is not guaranteed that all registered NSURLProtocol subclasses will be executed.

Internal logic of NSURLProtocol

The main work of NSURLProtocol development is to implement the interface methods of NSURLProtocol, including the following core interfaces.

Determines whether a request needs to be taken over

1
+ (BOOL)canInitWithRequest:(NSURLRequest *)request;

Return YES if you need to take over the request, otherwise return NO. After returning NO, the URL loading system will continue to query the next NSURLProtocol or initiate the request in the same way as before.

Edit request

1
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request;

After taking over the request, the original request can be modified, such as adding parameters, adding header, etc. The canonicalRequestForRequest method allows the original request to be modified and edited, and returns the modified NSURLRequest object. Using this method, you can easily implement operations such as request redirection, mapping local resources, etc.

start/stop request

1
2
- (void)startLoading;
- (void)stopLoading;

Since the original request has been taken over, it is also responsible for the execution of the new request after the editing of the new request has been completed. In the startLoading method, the request can be initiated using NSURLSession, etc. Example.

1
2
3
4
5
6
- (void)startLoading {
 NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
 NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration];
 self.task = [session dataTaskWithRequest:self.request];
 [self.task resume];
}

Any network request method can be used here, including AFNetworking, Alamofire, etc.

NSURLProtocol and URL loading system communication

After NSURLProtocol takes over the original request, it needs a way to seamlessly integrate with the original request in order to achieve a non-sensitive effect on the upper layer application.

1
@property (nullable, readonly, retain) id <NSURLProtocolClient> client;

The client property of NSURLProtocol is the object that communicates with the URL loading system. NSURLProtocolClient protocol is declared as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@protocol NSURLProtocolClient <NSObject>

- (void)URLProtocol:(NSURLProtocol *)protocol wasRedirectedToRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse;

- (void)URLProtocol:(NSURLProtocol *)protocol cachedResponseIsValid:(NSCachedURLResponse *)cachedResponse;

- (void)URLProtocol:(NSURLProtocol *)protocol didReceiveResponse:(NSURLResponse *)response cacheStoragePolicy:(NSURLCacheStoragePolicy)policy;

- (void)URLProtocol:(NSURLProtocol *)protocol didLoadData:(NSData *)data;

- (void)URLProtocolDidFinishLoading:(NSURLProtocol *)protocol;

- (void)URLProtocol:(NSURLProtocol *)protocol didFailWithError:(NSError *)error;

- (void)URLProtocol:(NSURLProtocol *)protocol didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;

- (void)URLProtocol:(NSURLProtocol *)protocol didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;

@end

The NSURLProtocolClient protocol defines receiving data, request success/failure, and Authentication Challeng handling, which needs to be called at the appropriate time. As an example, using the NSURLSessionDataTask request, the self.client method is called in the callback method of the response.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
    if (error != nil) {
        [self.client URLProtocol:self didFailWithError:error];
    } else {
        [self.client URLProtocolDidFinishLoading:self];
    }
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
    [self.client URLProtocol:self didLoadData:data];
}

How to avoid dead loops

You may already be aware of a problem: since NSURLSession requests can also be used in startLoading, this intercepted request may also be affected by NSURLProtocol. This may cause a dead loop: requests initiated in NSURLProtocol are intercepted again.

To avoid dead loops, add a processed flag to the already intercepted request.

1
[NSURLProtocol setProperty:@YES forKey:URLEaseapiHandledKey inRequest:self.request];

If this token is detected in the canInitWithRequest method, it is not intercepted.

1
2
3
4
5
6
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
    if ([NSURLProtocol propertyForKey:URLEaseapiHandledKey inRequest:request]) {
        return NO;
    }
    return YES;
}

Use traps

POST request body is empty

When intercepting a POST request, the HTTPBody property of the entry request of the canonicalRequestForRequest method is empty. If the body content is needed, use the HTTPBodyStream property to get it.

Impact of on the original request

NSURLProtocol in principle intercepts the original request and feeds back the response, so the caching policy, timeout and other settings of the original request may not work.

cannot guarantee that its NSURLProtocol will be executed

When there are multiple NSURLProtocol subclasses, the ones registered to the system later will be executed first, so there is no guarantee that your own NSURLProtocol subclasses will be executed. If the requirement is to implement a network interceptor, NSURLProtocol is still lacking in functionality. This is especially true when multiple interceptors are required to handle different services.