HTTP Client in Swift: Making Requests Properly — Advanced Level

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: From Basic GET to Industrial HTTP Client

A standard HTTP request in Swift is three lines of code with URLSession.shared.dataTask. But in real projects (file downloads, token-based authorization, caching, retries), this is not enough. In this article, we will explore advanced techniques for creating an HTTP client in Swift for iOS and macOS: custom protocols, session management, multithreading via async/await and Combine, as well as robust error handling. The material is intended for developers already familiar with the basics of URLSession.



Architecture of an Advanced HTTP Client

An industrial client is not just a request function, but a layer of abstraction. Let's divide it into three levels:

  • Transport Layer — low-level work with URLSession, configuration management.
  • Service Layer — serialization/deserialization, status code handling, retries.
  • Application Layer — business logic, caching, authorization.

Let's start with customizing the Transport Layer via URLProtocol.



1. Custom URLProtocol for Intercepting and Modifying Requests

URLProtocol is a powerful mechanism for intercepting all network requests in an application. It can be used to add logging, request signing, or mock data for testing.

import Foundation

class LoggingProtocol: URLProtocol { override class func canInit(with request: URLRequest) -> Bool { // Intercept all HTTP/HTTPS requests return request.url?.scheme == "http" || request.url?.scheme == "https" }

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

override func startLoading() { print("📤 Request: \\(request.httpMethod ?? "GET") \\(request.url?.absoluteString ?? "")") print("📦 Headers: \\(request.allHTTPHeaderFields ?? [:])")

let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in guard let self = self else { return } if let error = error { print("❌ Error: \\(error.localizedDescription)") self.client?.urlProtocol(self, didFailWithError: error) } else if let data = data, let response = response { print("✅ Response: \\(response.expectedContentLength) bytes") self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .allowed) self.client?.urlProtocol(self, didLoad: data) } self.client?.urlProtocolDidFinishLoading(self) } task.resume() }

override func stopLoading() { // Do nothing, the task completes on its own }}

// Register the protocolURLProtocol.registerClass(LoggingProtocol.self)let config = URLSessionConfiguration.defaultconfig.protocolClasses = [LoggingProtocol.self]let session = URLSession(configuration: config)

Now all requests through this session will be logged. Similarly, you can add automatic Bearer token injection or 401 status handling.



2. Session Management: Custom Configuration and Multithreading

Use different URLSessionConfiguration for different purposes: .default for regular requests, .ephemeral for private data (no cache or cookies), .background for background downloads.

let backgroundConfig = URLSessionConfiguration.background(withIdentifier: "com.pythonlib.background")backgroundConfig.isDiscretionary = truebackgroundConfig.sessionSendsLaunchEvents = truelet backgroundSession = URLSession(configuration: backgroundConfig, delegate: self, delegateQueue: nil)

For multithreading, use async/await (Swift 5.5+):

func performRequest(url: URL) async throws -> Data {    let (data, response)                

Blogs

Book Recommendations