JavaRush /Курсы /Swift SELF /URLSessionConfiguration: таймауты, кэш, заголовки

URLSessionConfiguration: таймауты, кэш, заголовки

Swift SELF
63 уровень , 2 лекция
Открыта

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 О чём по смыслу Когда полезен
timeoutIntervalForRequest
«Сколько ждать в рамках одного запроса» Чтобы запрос не висел «вечность» из-за сети
timeoutIntervalForResource
«Сколько ждать ресурс целиком» Чтобы длинная загрузка не тянулась бесконечно

Пример настройки:

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 — за «как мы в целом ходим в сеть».

1
Задача
Swift SELF, 63 уровень, 2 лекция
Недоступна
Таймауты сессии
Таймауты сессии
1
Задача
Swift SELF, 63 уровень, 2 лекция
Недоступна
Заголовки по умолчанию
Заголовки по умолчанию
1
Задача
Swift SELF, 63 уровень, 2 лекция
Недоступна
Политика кэша
Политика кэша
1
Задача
Swift SELF, 63 уровень, 2 лекция
Недоступна
Фабрика сессий
Фабрика сессий
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ