Как настроить 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.







