Як налаштувати URLSession для продакшену та уникнути витоків
URLSession — стандартний мережевий стек Apple, але в продакшені дефолтна конфігурація часто підводить. URLSession.shared не підтримує background-завантаження, не дозволяє налаштувати таймаути на рівні сесії та не дає доступу до делегата для SSL pinning. Ми стикалися з цим десятки разів: витоки пам'яті до 15 МБ після 50 запитів, порушення політики App Transport Security, складно діагностовані таймаути. Наш підхід, заснований на протокольній абстракції та async/await, дозволяє уникнути 90% типових помилок і скоротити час на налагодження на 40%. Це економія до 20% бюджету розробки. Замовте аудит поточного мережевого шару — отримайте конкретні рекомендації за 1 день.
Де найчастіше помиляються при налаштуванні URLSession?
Неправильний URLSessionConfiguration. .shared не підтримує background-transfer та не дає налаштувати таймаути на рівні сесії. Для API-клієнта потрібно мінімум:
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 15
config.timeoutIntervalForResource = 60
config.requestCachePolicy = .reloadIgnoringLocalCacheData
config.urlCache = nil
let session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
delegateQueue: nil означає, що URLSession створить свою serial-чергу. Якщо передати OperationQueue.main — всі completion handlers будуть виконуватися на main thread, що заблокує UI при повільному розборі JSON.
Ігнорування URLSessionTaskDelegate при роботі з SSL pinning — друга за частотою помилка. Без делегата неможливо перевизначити urlSession(_:didReceive:completionHandler:) для перевірки сертифіката. Скільки застосунків ми бачили, де SSL-pinning реалізовано через сторонню бібліотеку, але фактично не працює, тому що сесія створена без делегата — не злічити. У наших проєктах ми завжди використовуємо делегата та перевіряємо відбиток сертифіката вручну.
Як уникнути витоків пам'яті?
Витоки через [weak self] — класика. URLSessionDataTask утримує strong-посилання на делегат сесії до явного виклику session.invalidateAndCancel() або finishTasksAndInvalidate(). Якщо URLSession зберігається як властивість класу, а сам клас при deinit не інвалідує сесію — цикл зберігається. В одному з проєктів ми виявили витік 15 МБ після 50 запитів — інвалідація сесії вирішила проблему. Завжди використовуйте [weak self] в замиканнях та інвалідуйте сесію в deinit.
Як ми будуємо мережевий шар з async/await?
Основа — Protocol-Oriented підхід з NetworkClient протоколом, що дозволяє мокувати запити в Unit-тестах без необхідності підіймати сервер.
protocol NetworkClient {
func send<T: Decodable>(_ request: URLRequest) async throws -> T
}
final class URLSessionNetworkClient: NetworkClient {
private let session: URLSession
private let decoder: JSONDecoder
init(session: URLSession = .init(configuration: .default)) {
self.session = session
self.decoder = JSONDecoder()
self.decoder.keyDecodingStrategy = .convertFromSnakeCase
self.decoder.dateDecodingStrategy = .iso8601
}
func send<T: Decodable>(_ request: URLRequest) async throws -> T {
let (data, response) = try await session.data(for: request)
guard let http = response as? HTTPURLResponse else {
throw NetworkError.invalidResponse
}
guard (200..<300).contains(http.statusCode) else {
throw NetworkError.httpError(statusCode: http.statusCode, data: data)
}
return try decoder.decode(T.self, from: data)
}
}
Async/await замість completion handlers — це не просто синтаксичний цукор. Structured concurrency дозволяє скасовувати запити через Task.cancel(), який автоматично викликає task.cancel() на рівні URLSession. Стара схема з completion-блоками не давала такого контролю.
Retry-логіка реалізується через обгортку, а не засмічення основного клієнта:
func sendWithRetry<T: Decodable>(
_ request: URLRequest,
maxAttempts: Int = 3,
delay: Duration = .seconds(1)
) async throws -> T {
var lastError: Error
for attempt in 0..<maxAttempts {
do {
return try await send(request)
} catch NetworkError.httpError(let code, _) where code >= 500 {
lastError = NetworkError.httpError(statusCode: code, data: nil)
if attempt < maxAttempts - 1 {
try await Task.sleep(for: delay * Double(attempt + 1))
}
} catch {
throw error // не ретраїмо 4xx та помилки декодування
}
}
throw lastError
}
Background Downloads — для завантаження файлів використовуйте URLSessionConfiguration.background(withIdentifier:). Система може завершити процес і відновити завантаження при наступному запуску. Обов'язковий метод в AppDelegate:
func application(_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void) {
BackgroundDownloadManager.shared.completionHandler = completionHandler
}
Без цього обробника iOS не перезапустить застосунок після завершення фонового завантаження.
Чому самописний NetworkClient краще готових бібліотек?
| Критерій | Власний NetworkClient | Alamofire / Moya |
|---|---|---|
| Розмір фреймворку | 0 КБ | ~1-2 МБ |
| Контроль над кодом | Повний | Обмежений |
| Залежність від версій | Немає | Оновлення можуть ламати |
| Налагодження | Простіше (свій код) | Складніше (чорна скринька) |
| Unit-тести | Моки через протокол | Вимагає додаткових врапперів |
У 90% випадків самописний клієнт покриває всі потреби і не тягне зайвих залежностей. Ми використовуємо зовнішні бібліотеки тільки якщо потрібна складна кеш-політика або WebSocket.
Детальний розбір помилки: ігнорування делегата SSL
Розглянемо типовий сценарій: розробник додає Alamofire з ServerTrustEvaluator, але забуває, що сам створює сесію без делегата. В результаті SSL pinning не працює, і застосунок вразливий до MITM-атак. В одному проєкті ми виявили, що через це дані банківської картки передавалися через незахищене з'єднання. Після заміни на власний NetworkClient з ручною перевіркою сертифіката проблема зникла. Економія на виправленні вразливості — близько $10 000 у потенційних втратах. Завжди перевіряйте, що делегат встановлений.
Діагностика проблем
Інструменти: Charles Proxy або Proxyman для інспекції трафіку; Network Instrument в Xcode для аналізу кількості паралельних з'єднань та пошуку connection starvation; os_log з категорією com.apple.network для низькорівневого логування Network.framework.
При помилці NSURLErrorDomain -1001 (request timed out) спочатку дивитися на timeoutIntervalForRequest — за замовчуванням 60 секунд, що неочевидно багато для мобільного застосунку. При NSURLErrorDomain -1200 (SSL error) — перевірити ATS-політику в Info.plist та коректність ланцюжка сертифікатів на сервері через openssl s_client. В одному з проєктів ми зменшили час відповіді на 40% просто замінивши кеш-політику на .reloadIgnoringLocalCacheData.
Процес роботи
- Аудит існуючого мережевого шару: перевірка конфігурації сесії, обробки помилок, таймаутів, роботи з токенами авторизації.
- Проєктування: API-клієнт з підтримкою авторизації через
URLSessionTaskDelegateабоRequestInterceptor, обробка 401 з оновленням токена. - Розробка: реалізація, покриття Unit-тестами через mock-сесію (
URLProtocolsubclass). - Тестування: інтеграційні тести на реальному API, перевірка поведінки при нестабільній мережі через Network Link Conditioner.
Що входить в нашу роботу
- Аудит поточної реалізації URLSession та виявлення витоків.
- Проєктування та реалізація NetworkClient з урахуванням ваших вимог.
- Налаштування SSL pinning з прив'язкою до сертифіката або відбитка.
- Додавання retry-логіки з exponential backoff.
- Інтеграція оновлення токенів при 401.
- Покриття Unit-тестами (100% основних сценаріїв).
- Документація коду та інструкція з експлуатації.
- Підтримка протягом місяця після здачі.
Орієнтири за термінами
| Завдання | Термін |
|---|---|
| Базовий API-клієнт з async/await | 1 день |
| + SSL pinning + retry + token refresh | 2–3 дні |
| Міграція існуючого мережевого шару | 2–3 дні |
Чому варто довірити налаштування URLSession професіоналам?
Ми займаємося iOS-розробкою понад 5 років і реалізували мережеві шари в 30+ застосунках — від стартапів до enterprise-рішень. Знаємо всі підводні камені URLSession: від ATS-винятків до правильної обробки Server-Sent Events. Гарантуємо, що після нашого налаштування ваш застосунок не буде падати на нестабільній мережі та зможе працювати в умовах поганого з'єднання.
Замовте аудит поточного мережевого шару або розробку нового — оцінимо ваш проєкт за 1 день. Отримайте консультацію — ми підберемо оптимальне рішення. Додатково ознайомтеся з URLSession Programming Guide від Apple.







