Log request and response data from URLSession

179 views Asked by At

I have an iOS app that uses URLSession.shared.data in multiple places. I would like to be able to create the functionality to log the request and response data to the console. I have created functions to do this logging, but I currently would have to add this function to all places in the app that use URLSession.shared.data. Is there a way to create an extension on URLSession to be able to log data once the response is received?

1

There are 1 answers

0
VictorCMS On

I would suggestion you make your own network layer instead of using URLSession.shared.data directly. Mainly because with that it hard to implement unit tests.

But you can make something like this:

import Foundation
import OSLog

private extension Logger {
   static let network = Logger(subsystem: "Your App's name", category: "network")
    }

    final class URLProtocolLogger: URLProtocol {
        private static let ignoredURL = ["any url that you want"]
    let logger = Logger()
    private var response: URLResponse?
    private var responseData: NSMutableData?

    private lazy var session: URLSession = { [unowned self] in
        return URLSession(configuration: .default, delegate: self, delegateQueue: nil)
    }()

    override class func canInit(with task: URLSessionTask) -> Bool {
        guard let request = task.currentRequest else { return false }
        return canServeRequest(request)
    }

    override class func canInit(with request: URLRequest) -> Bool {
        canServeRequest(request)
    }

    private class func canServeRequest(_ request: URLRequest) -> Bool {
        guard let url = request.url,
            (url.absoluteString.hasPrefix("http") || url.absoluteString.hasPrefix("https")) else {
            return false
        }

        let absoluteString = url.absoluteString
        guard !ignoredURL.contains(where: { absoluteString.hasPrefix($0) }) else { return false }

        return true
    }

    override class func canonicalRequest(for request: URLRequest) -> URLRequest {
        request
    }

    override func startLoading() {
        session.dataTask(with: request).resume()
    }

    override func stopLoading() {
        session.getTasksWithCompletionHandler { dataTasks, _, _ in
            dataTasks.forEach { $0.cancel() }
            self.session.invalidateAndCancel()
        }
    }
}

extension URLProtocolLogger: URLSessionDataDelegate {
    public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
        responseData?.append(data)

        client?.urlProtocol(self, didLoad: data)
    }

    public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
        self.response = response
        responseData = NSMutableData()

        client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .allowed)
        completionHandler(.allow)
    }

    public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        defer {
            if let error = error {
                client?.urlProtocol(self, didFailWithError: error)
            } else {
                client?.urlProtocolDidFinishLoading(self)
            }
        }

        guard let request = task.originalRequest, let url = request.url else {
            return
        }

        if error != nil {
            Logger.network.warning("Request: \(url.pathComponents) failed with: \(error)")
        } else if response != nil {
            let data = (responseData ?? NSMutableData()) as Data
            Logger.network.info("Request: \(url.absoluteString) success with: \(data)")
        }
    }

    public func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) {

        client?.urlProtocol(self, wasRedirectedTo: request, redirectResponse: response)
        completionHandler(request)
    }
}

Then you need to make your own URLSession like this:

static let appSession: URLSession = {
    let configuration = URLSessionConfiguration.default
    configuration.protocolClasses = [URLProtocolLogger.self]
    return URLSession(configuration: configuration)
}()