
A Deep Dive into Request Interception on iOS
Intercepting network requests is a powerful technique for debugging, monitoring, and even mocking network traffic in iOS apps. Apple’s URLProtocol
system provides a flexible way to observe, modify, or block requests at runtime—without changing your app’s networking code.
In this article we will explore how to implement a custom URLProtocol
to intercept and modify network requests.
How Request Interception Works on iOS
At the core of request interception is a custom subclass of URLProtocol
. By registering your class with the protocolClasses
property of a URLSessionConfiguration
, you can transparently intercept all HTTP and HTTPS requests made by your app.
This will allow you to do the following:
- See all network requests and responses including body and headers
- Allows to modify the request or response before it is sent or received
- Provide enhanced status about the request and response
Injecting Your Interceptor with protocolClasses
To enable interception, add your custom protocol class to the session configuration:
let config = URLSessionConfiguration.shared
config.protocolClasses = [NetworkInterceptor.self]
let session = URLSession(configuration: config)
This ensures that every request made with this session will first be handled by NetworkInterceptor
. You can use this technique for debugging, analytics, or request mocking.
A common mistake here it related to the timing. You have to run this code as early as possible in your application lifecycle.
Any reuest occuring before this code has been executed will not be intercepted. Another limitation are requests made with URLSessionConfiguration.default
.
This is because URLSessionConfiguration.shared is a singleton.
Implementing a Custom URLProtocol
The following is a simplified example of a custom interceptor class:
public class NetworkInterceptor: URLProtocol, URLSessionDelegate {
// Determines if this protocol should handle the given request
public override class func canInit(with request: URLRequest) -> Bool
// Returns a canonical version of the request
public override class func canonicalRequest(for request: URLRequest) -> URLRequest
// Starts loading the request (called by the system)
public override func startLoading()
// Stops loading the request
public override func stopLoading()
// URLSessionDelegate methods for handling authentication, redirects, etc.
public func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
}
canInit(with:)
This method decides whether the protocol should handle a given request. Typically, you check the scheme (http/https), avoid recursion, and can filter requests based on custom logic (e.g., only intercepting certain hosts).
public override class func canInit(with request: URLRequest) -> Bool {
// Avoid handling requests twice
if URLProtocol.property(forKey: "Handled", in: request) != nil {
return false
}
// Only handle http/https
guard let scheme = request.url?.scheme?.lowercased(), ["http", "https"].contains(scheme) else {
return false
}
return true
}
startLoading()
This is where the magic happens. Mark the request as handled, create a new URLSession
(with yourself as delegate), and start the real network request. You can also inject mock responses here if needed.
public override func startLoading() {
// Mark as handled to avoid recursion
let newRequest = (request as NSURLRequest).mutableCopy() as! NSMutableURLRequest
URLProtocol.setProperty(true, forKey: "Handled", in: newRequest)
// Optionally: Check for mock response
if let mock = MockRegistry.match(request: newRequest as URLRequest) {
// Return mock response
self.client?.urlProtocol(self, didReceive: mock.response, cacheStoragePolicy: .notAllowed)
self.client?.urlProtocol(self, didLoad: mock.data)
self.client?.urlProtocolDidFinishLoading(self)
return
}
// Otherwise, perform the real request
let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
self.task = session.dataTask(with: newRequest as URLRequest)
self.task?.resume()
}
URLProtocol.setProperty can be useful to avoid infinite recursion. You can check against this property in the canInit method and then return early.
stopLoading()
This cancels the network task if it’s still running:
public override func stopLoading() {
self.task?.cancel()
}
Handling Authentication and Redirects
By implementing URLSessionDelegate
methods, you can handle authentication challenges, redirects, and SSL pinning transparently. This ensures intercepted requests behave just like normal ones.
public func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
// Forward the challenge to the client or handle as needed
completionHandler(.performDefaultHandling, nil)
}
Summary
Request interception on iOS is built on top of Apple’s URLProtocol
system, allowing for powerful debugging, monitoring, and mocking of network traffic in your app. By injecting your custom URLProtocol
into your session’s protocolClasses
, you can observe and control all network requests with minimal code changes.