1. Зачем нужна URLSessionConfiguration
Когда вы только начинаете работать с сетью, очень легко скатиться в стиль «на каждый запрос по десять строчек настроек». Работает? Работает. Но через неделю вы понимаете, что в одном месте таймаут 5 секунд, в другом 50, где-то забыли заголовок Accept, а где-то случайно включили кэш и ловите «призраков» старых ответов. URLSessionConfiguration нужна именно для того, чтобы один раз описать правила игры для группы запросов, а потом не копировать их руками.
URLSessionConfiguration — это объект, который хранит настройки (политику) для URLSession. Если представить сеть как доставку еды, то URLRequest — это конкретный заказ («пицца, адрес, комментарий»), а URLSessionConfiguration — правила доставки: «сколько ждём курьера», «какие пакеты используем», «кладём ли визитку (User-Agent)», «можем ли использовать кэш».
Технически нам важно запомнить одну мысль: URLSessionConfiguration настраивает сессию, а не запрос. Запрос может что-то переопределить, но «база» живёт на уровне сессии.
URLSession.shared и своя сессия
Почти все начинают с URLSession.shared, и это нормально: он как «общественный транспорт» — сел и поехал. Но у общественного транспорта нет кнопки «только для меня сделай кондиционер потеплее». URLSession.shared — это готовая сессия, и её конфигурацию вы не настраиваете (по крайней мере, не так, чтобы это было хорошей идеей).
Чтобы иметь свои правила (таймауты, заголовки, кэш), мы создаём собственную сессию:
import Foundation
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
// session готова, дальше будем использовать её для dataTask(...)
_ = session
В этот момент стоит на секунду остановиться и понять архитектуру:
flowchart LR
A[URLRequest
конкретный запрос] --> B[URLSession
исполнитель]
B --> C[URLSessionConfiguration
политика: таймауты/кэш/заголовки]
Сессия создаётся на базе конфигурации. А вот после того как сессия создана, менять конфигурацию «на лету» — плохая привычка: вы легко сделаете поведение непредсказуемым. В учебных примерах мы будем считать, что конфигурация задаётся до создания URLSession, а потом сессия используется как «стабильный инструмент».
Небольшой практический вывод для нашего учебного CLI: даже если пока запрос один, мы всё равно хотим создать «нашу» сессию, потому что дальше запросов станет больше, и копипаста начнёт кусаться.
2. Таймауты: почему их два
Когда люди слышат слово «таймаут», они часто представляют одну ручку: «если медленно — отрубить». В URLSessionConfiguration таймаутов два, и это не из вредности Apple, а потому что «медленно» бывает разным. Сейчас мы разберёмся без мистики, что именно мы контролируем.
Первый таймаут — timeoutIntervalForRequest. Он про то, как долго мы готовы ждать, пока запрос в целом движется (условно: устанавливаем соединение, ждём первые байты ответа, получаем данные). Второй — timeoutIntervalForResource. Он про то, сколько мы готовы ждать всю загрузку ресурса целиком (условно: «скачай мне вот этот файл/ответ полностью, но не вечно»).
Давайте сведём это в табличку (потому что таблица спасает мозг лучше, чем десять абзацев):
| Таймаут в URLSessionConfiguration | О чём по смыслу | Когда полезен |
|---|---|---|
|
«Сколько ждать в рамках одного запроса» | Чтобы запрос не висел «вечность» из-за сети |
|
«Сколько ждать ресурс целиком» | Чтобы длинная загрузка не тянулась бесконечно |
Пример настройки:
import Foundation
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 10 // секунд
config.timeoutIntervalForResource = 30 // секунд
let session = URLSession(configuration: config)
_ = session
Важно: таймаут — это не способ сделать сеть «быстрой». Это способ сделать поведение программы предсказуемым. Пользователь (или вы сами, когда дебажите) должен понимать: «через N секунд мы перестанем ждать и вернём ошибку/сообщение». А сеть пусть остаётся сетью: иногда она грустит.
Ещё одна важная тонкость (мы её не углубляем, но фиксируем как факт): конкретное поведение таймаутов зависит от системы и условий (DNS, TCP, прокси и т.д.). Поэтому мы используем таймауты как политику, а не как «секундомер с гарантией».
3. Заголовки и кэш
Заголовки по умолчанию: httpAdditionalHeaders
Когда вы делаете CLI, у вас очень быстро появится желание ставить одинаковые заголовки на каждый запрос. Например, Accept: application/json и User-Agent: LibraryCLI/0.1. Делать это вручную в каждом URLRequest можно, но это как каждый раз заново настраивать раскладку клавиатуры перед печатью.
Для дефолтных заголовков у конфигурации есть httpAdditionalHeaders. Смысл: «все запросы этой сессии по умолчанию будут иметь эти заголовки».
import Foundation
let config = URLSessionConfiguration.default
config.httpAdditionalHeaders = [
"Accept": "application/json",
"User-Agent": "LibraryCLI/0.1"
]
let session = URLSession(configuration: config)
_ = session
Теперь важный вопрос, который почти всегда всплывает: а если я в URLRequest поставлю другой Accept? Кто победит?
Практическое правило (и его достаточно на этом уровне): заголовки в конкретном URLRequest — это «последнее слово» для этого запроса. То есть, если вы поставили дефолт в сессии, а потом в запросе указали заголовок с тем же ключом, вы тем самым явно сказали: «для этого запроса сделай иначе».
Небольшая визуальная схема:
flowchart TD
A[URLSessionConfiguration.httpAdditionalHeaders
дефолты] --> B[URLRequest]
B --> C[Финальные заголовки запроса]
D["URLRequest.setValue(...)"] --> B
И маленький пример переопределения:
import Foundation
var request = URLRequest(url: URL(string: "https://example.com")!)
request.setValue("text/plain", forHTTPHeaderField: "Accept") // переопределили дефолт
Да, здесь URL(string:)! выглядит как нарушение наших правил. В реальном коде так делать не надо, но в демонстрации с константой example.com это допустимо как «контракт учебника»: строка точно валидная. (В боевом коде мы продолжаем любить guard let.)
Кэш: почему он существует и где настраивается
Кэширование в сети — тема, где новички чаще всего начинают подозревать, что компьютер живёт собственной жизнью. Вы сделали запрос, получили ответ. Потом сделали запрос снова — и внезапно получили старый ответ, хотя «на сервере уже всё поменялось». Это не мистика и не заговор, это кэш.
Базовая идея: если ресурс можно безопасно переиспользовать (по правилам HTTP и заголовкам ответа), система может сохранить его и отдать повторно, чтобы сэкономить время и трафик. URLSessionConfiguration участвует в этой истории через политику кэша и через ссылку на URLCache.
Самое главное, что нужно понять на этом уровне: кэш — это часть политики. Это не «опция на один запрос», а поведение, которое удобно задавать централизованно, чтобы потом не ловить сюрпризы.
Пример: мы можем сказать, что хотим игнорировать локальный кэш (это полезно для CLI‑утилит, где мы чаще хотим «самые свежие данные»):
import Foundation
let config = URLSessionConfiguration.default
config.requestCachePolicy = .reloadIgnoringLocalCacheData
let session = URLSession(configuration: config)
_ = session
И отдельно: у запроса тоже есть cachePolicy, и он может переопределить политику сессии, если конкретно этому запросу «можно иначе»:
import Foundation
let url = URL(string: "https://example.com/api")!
var request = URLRequest(url: url)
request.cachePolicy = .reloadIgnoringLocalCacheData
Заметьте, мы не строим стратегию кэширования «как в браузере», не обсуждаем TTL, не делаем «умный кеш‑слой». Сегодня наша цель скромнее: вы должны узнать, что кэш существует, где он настраивается и почему он может влиять на результаты. А глубокая оптимизация и архитектура кэша — это отдельный разговор (и точно не в этой лекции, иначе мы случайно построим мини‑Chrome).
4. Виды URLSessionConfiguration: default и ephemeral
Когда вы видите URLSessionConfiguration.default, может возникнуть ощущение, что это «единственный вариант, просто имя такое». На самом деле вариантов несколько, и выбор влияет на поведение: что сохраняется, что пишется на диск, как ведут себя cookies/кэш и т.д. Мы не будем сейчас уходить в платформенные детали, но минимальную карту местности нарисуем, чтобы вы не терялись в API.
На базовом уровне нам достаточно понимать два режима.
default — нормальный рабочий режим. Он использует стандартные механизмы кэша и хранения, где это уместно. Для большинства задач это «обычная жизнь».
ephemeral — режим «ничего лишнего». Идея в том, что сессия старается не оставлять следов (например, не писать кэш на диск). Это бывает полезно, когда вы хотите вести себя более «одноразово»: запрос сделали, результат получили, и никаких накопленных артефактов.
Пример:
import Foundation
let defaultConfig = URLSessionConfiguration.default
let ephemeralConfig = URLSessionConfiguration.ephemeral
let defaultSession = URLSession(configuration: defaultConfig)
let ephemeralSession = URLSession(configuration: ephemeralConfig)
_ = (defaultSession, ephemeralSession)
Почему .background(withIdentifier:) пока пропускаем
А вот .background(withIdentifier:) мы сознательно не трогаем. Это уже история про фоновые загрузки, жизненный цикл задач и поведение приложения вне активного процесса. Мы сейчас учимся ходить, а не прыгать с парашютом.
5. Практика: фабрика сессии и один GET
Мини‑фабрика сессии для CLI
Когда вы пишете учебные примеры, кажется, что можно обойтись парой строк. Но курс у нас про системное мышление: мы хотим, чтобы код разрастался в одно приложение, а не в набор разрозненных фрагментов. Поэтому сейчас добавим маленький строительный блок: функцию, которая создаёт настроенную сессию для всего приложения.
Представим, что у нас есть CLI‑приложение LibraryCLI. Пока оно может, например, дергать /ping эндпоинт или получать JSON (декодирование мы нормально оформим позже). Сегодня — только сессия.
import Foundation
struct NetworkSessionFactory {
static func makeSession() -> URLSession {
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 10
config.timeoutIntervalForResource = 30
config.httpAdditionalHeaders = ["Accept": "application/json"]
return URLSession(configuration: config)
}
}
Почему это полезно?
- Во‑первых, у нас появляется «одно место правды»: если завтра вы решите, что таймаут должен быть 15 секунд, вы меняете одну строчку.
- Во‑вторых, код запроса становится чище: запрос отвечает за URL/метод/тело, а сессия отвечает за «политику сети».
- В‑третьих, это психологически помогает не превращать сетевой код в «магический суп» из параметров, размазанных по всему проекту.
Маленькая историческая ремарка: многие Foundation‑типы пришли из Objective‑C и со временем «потеряли» префикс NS. В частности, NSURLSessionConfiguration стал URLSessionConfiguration. Это не жизненно важно для нашей лекции, но полезно знать, если вы читаете старые статьи и видите «NSURL…» в примерах.
Мини‑демо: один GET через настроенную URLSession
Сейчас мы соберём небольшой пример, который использует нашу сессию с конфигурацией. Мы не будем делать идеальный сетевой слой, но хотим увидеть, что настроенная сессия реально участвует в запросе.
import Foundation
let session = NetworkSessionFactory.makeSession()
let url = URL(string: "https://example.com")! // константа для демо
let request = URLRequest(url: url)
session.dataTask(with: request) { data, response, error in
print("error:", String(describing: error)) // error: nil (обычно)
print("bytes:", data?.count ?? 0) // bytes: 1256 (пример)
print("response:", String(describing: response)) // response: Optional(<NSHTTPURLResponse ...>)
}.resume()
Что здесь важно заметить, даже если вы пока не уверены в деталях HTTP:
URLSessionConfiguration не «торчит» в этом коде напрямую. Она уже спрятана внутри session. И это хорошо: у запроса не должно быть обязанности помнить про таймауты для всей программы.
Если вы захотите для конкретного запроса переопределить что-то точечно (например, кэш‑политику), вы сделаете это через URLRequest — и это останется локальным решением:
import Foundation
var request = URLRequest(url: URL(string: "https://example.com")!)
request.cachePolicy = .reloadIgnoringLocalCacheData
request.timeoutInterval = 5 // точечный таймаут только для этого запроса
Смысл такой: сессия задаёт «по умолчанию», запрос — «исключения».
6. Типичные ошибки при работе с URLSessionConfiguration
Ошибка №1: пытаться «настроить URLSession.shared».
Очень хочется сделать URLSession.shared.configuration.timeoutIntervalForRequest = ... и почувствовать себя властелином сети. Но правильная ментальная модель другая: shared — общая сессия без вашего персонального тюнинга. Хотите свои правила — создайте URLSession(configuration:) и используйте её явно.
Ошибка №2: считать, что таймаут — это гарантия, что запрос завершится за N секунд.
Таймаут — это ограничение ожидания и попытка сделать поведение предсказуемым, но сеть состоит из множества этапов и условий. Если выставить «слишком маленькое» число, вы получите не «быстрее», а «чаще падает». Новички иногда ставят 1 секунду и удивляются, почему интернет «плохой». Интернет не плохой — он просто интернет.
Ошибка №3: размазывать заголовки по всем URLRequest и потом забыть один.
Если у вас 20 запросов, и в 19 вы поставили Accept: application/json, а в одном забыли — это будет самый загадочный баг недели: «почему только один запрос возвращает не то?». Дефолтные заголовки на уровне сессии уменьшают вероятность таких сюрпризов.
Ошибка №4: не учитывать кэш и потом не верить своим глазам.
Когда вы тестируете API, особенно в учебном проекте, кэш иногда мешает обучению: вы думаете, что сделали новый запрос, а вам вернули старый ответ. Если поведение кажется «магическим», первым делом стоит вспомнить, что у нас есть requestCachePolicy в конфигурации и cachePolicy в запросе, и вы можете временно отключить кэширование, чтобы увидеть честную сеть.
Ошибка №5: смешивать «политику» и «содержание запроса» в одном месте.
Если функция, которая делает запрос, одновременно собирает URL, ставит заголовки, настраивает таймауты, управляет кэшем и ещё печатает результат — она быстро превращается в монстра. Гораздо спокойнее жить, когда URLRequest отвечает за «что спросить», а URLSessionConfiguration — за «как мы в целом ходим в сеть».
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ