1. Зачем нам вообще HTTP в нашем CLI-проекте
Когда мы пишем LibraryCLI, очень легко поверить, что мир состоит из локальных файлов и наших гениальных функций. Но в реальной жизни данные часто живут где-то «снаружи»: на сервере, в облаке, в API. И чтобы наш CLI однажды смог сказать «дай мне книгу по id 42» не только из локального JSON, но и из внешнего источника, нам нужна базовая грамотность по HTTP.
HTTP — это не «магия интернета» и не «то, что делает браузер». Это довольно строгий протокол: мы отправляем запрос (request), получаем ответ (response), и обязаны корректно понять, что именно нам вернули. Иначе будет классическая ситуация: «сервер ответил, но программа всё равно считает, что всё хорошо». Это примерно как получить письмо “Ваш заказ отменён”, увидеть конверт, радостно кричать “Ура, доставка!” и уйти праздновать.
Модель HTTP: запрос → ответ
Если говорить максимально приземлённо, HTTP — это договор между клиентом и сервером. Клиент говорит: «вот что я хочу» (request), сервер отвечает: «вот что получилось» (response). Важно, что наличие ответа не означает успех — сервер может честно ответить «нет», «ошибка», «ты кто вообще такой?», и это всё равно будет корректный HTTP-ответ.
У запроса есть несколько ключевых частей: метод, URL (адрес ресурса), заголовки и иногда тело. В статье про Swift HTTP Types это описывается через базовые компоненты запроса (метод, схема, authority/host, path), и это хороший ориентир, даже если мы пока не пишем низкоуровневые HTTP-клиенты.
У ответа тоже есть ключевые части: статус-код, заголовки и тело. Статус-код — это «короткий вердикт», заголовки — «метаданные», а тело — «полезная нагрузка» (payload), чаще всего в виде Data.
Небольшая схема, чтобы в голове закрепилось:
flowchart LR
A[CLI-приложение: LibraryCLI] -->|HTTP Request| B[Сервер / API]
B -->|HTTP Response| A
A -->|Decode: Data -> String/JSON| C[Модель данных / DTO]
2. HTTP-методы: GET/POST/PUT/DELETE
Метод HTTP — это короткое слово, которое задаёт намерение запроса. Можно думать о методе как о глаголе в предложении: «получи», «создай», «замени», «удали». Адрес (URL) при этом обычно указывает, над чем выполняется действие — над ресурсом (книга, пользователь, заказ).
Важно: метод — это не «для красоты». Сервер может по-разному реагировать на один и тот же URL в зависимости от метода. Запрос GET /books/42 — это обычно «дай книгу», а DELETE /books/42 — «удали книгу». Если перепутать, можно устроить себе очень бодрый день.
GET — «дай посмотреть»
GET используют, когда мы хотим получить данные. Обычно GET-запрос не имеет тела, хотя формально бывают исключения (но мы туда не лезем — нам бы выжить).
Пример «человеческого» запроса (не настоящий сетевой код, просто чтобы увидеть структуру):
import Foundation
let rawRequest = """
GET /books/42 HTTP/1.1
Host: api.example.com
Accept: application/json
"""
print(rawRequest) // Печатает «скелет» HTTP-запроса
POST — «создай»
POST обычно означает создание нового ресурса или запуск операции. Часто у POST есть тело: например JSON, который описывает новую книгу.
Нам пока важна идея: если есть тело, мы обязаны знать формат (через заголовки), а сервер обязан нам сказать, что он понял/создал.
PUT — «замени целиком»
PUT часто используют для полного обновления ресурса: «вот новая версия книги целиком». В реальных API границы между PUT и PATCH могут быть тонкими, но мы не будем усложнять: PUT — это про «полную замену».
DELETE — «удали»
DELETE говорит сам за себя. Иногда сервер возвращает тело, иногда нет. Но статус-код всё равно обязателен: именно он сообщает, удалось ли удаление.
Таблица для памяти
| Метод | Интуитивный смысл | Типичный пример URL | Что чаще всего в ответе |
|---|---|---|---|
|
получить данные | |
+ JSON тела |
|
создать/запустить | |
+ JSON созданного ресурса |
|
заменить целиком | |
|
|
удалить | |
|
4. HTTP status codes: «ответ пришёл» ≠ «всё хорошо»
Статус-код — это центральная вещь в ответе сервера. Он отвечает на вопрос: «какой итог операции?». И да, сервер может вернуть вам красивое JSON-тело, но если статус 404 или 500 — это не успех, а «у нас проблемы».
В нашем курсе мы будем придерживаться очень практичного правила: успех — это 2xx (то есть 200...299). Именно так обычно и делают в клиентском коде.
Семейства статус-кодов
Чтобы не запоминать 60 чисел, запоминают «семейства»:
| Диапазон | Смысл | Простая трактовка |
|---|---|---|
|
informational | «я в процессе» (редко нужно в клиентском коде на старте) |
|
success | «получилось» |
|
redirection | «иди в другое место» |
|
client error | «ты (клиент) что-то сделал не так» |
|
server error | «сервер сломался / не справился» |
Сделаем маленькую функцию — это прям учебная «заготовка для мозга». Она будет использоваться позже, когда мы дойдём до реальных запросов:
import Foundation
func isSuccessStatus(_ code: Int) -> Bool {
(200...299).contains(code)
}
print(isSuccessStatus(200)) // true
print(isSuccessStatus(404)) // false
Статусы, которые встретятся чаще всего
Сейчас не нужно превращаться в ходячую Википедию. Но есть статус-коды, которые почти неизбежны:
— «всё нормально, вот данные».200 OK
— «создали ресурс».201 Created
— «успешно, но тело пустое».204 No Content
— «запрос неправильный (формат/поля)».400 Bad Request
— «нужна авторизация».401 Unauthorized
— «я тебя понял, но не дам».403 Forbidden
— «такого ресурса нет».404 Not Found
— «серверу плохо».500 Internal Server Error
В реальной серверной документации (например, в примерах OpenAPI) часто прямо показывают ветвление логики по статусам, включая «недокументированные статусы» — идея в том, что статусы бывают разные, и клиент обязан их учитывать.
5. Заголовки: метаданные, без которых всё развалится «вежливо»
Заголовки — это пары «имя: значение», которые описывают запрос или ответ. Можно представить их как наклейки на посылке: «хрупкое», «стекло», «адрес получателя», «температурный режим». Без наклеек посылка может доехать… но часто в виде конструктора «собери вазу сам».
Заголовки бывают и в запросе, и в ответе. Клиент отправляет, что он умеет принимать, какой формат отправляет, какие у него предпочтения. Сервер отвечает, что именно он отдал, каким типом данных, сколько байт, когда это было сформировано и так далее.
Accept и Content-Type: два заголовка, которые путают все
Очень частая путаница: Accept и Content-Type. Запомнить можно по логике:
- Accept — что я хочу получить (я “accept”, принимаю).
- Content-Type — что я отправляю (тип контента моего тела запроса).
Если вы отправляете JSON в теле запроса, обычно ставят Content-Type: application/json. Если вы ожидаете JSON в ответе, часто ставят Accept: application/json.
Покажем это на «игрушечном» примере через словарь. Мы ещё не собираем URLRequest (это будет отдельная лекция), но концепцию легко увидеть так:
import Foundation
let headers: [String: String] = [
"Accept": "application/json",
"Content-Type": "application/json"
]
print(headers["Accept"] ?? "<no Accept>") // application/json
Заголовки ответа: почему они важны, даже если мы «просто хотим JSON»
Сервер в ответе тоже часто ставит Content-Type. И это полезно: мы можем понять, что пришло. Например, если ожидали JSON, а сервер вернул HTML-страницу (да, так бывает: «красивый error page»), то Content-Type: text/html — это очень жирная подсказка: «осторожно, не декодируй это как JSON».
Мы не будем пока писать парсер заголовков HTTP (это отдельная профессия и путь в мир седых волос), но минимально полезная идея такая: заголовки — это метаданные, а тело — это байты, и метаданные помогают понять, как эти байты трактовать.
6. Тело ответа: сначала байты (Data), потом смысл
Тело ответа — это данные, которые сервер прислал. На уровне HTTP это просто последовательность байтов. В Swift на практике это будет Data. А вот «превратить это в строку» или «превратить это в JSON-модель» — это следующий шаг, и он может не получиться.
Это важное переключение мышления: не «сервер вернул строку», а «сервер вернул байты, и я пытаюсь интерпретировать их как строку/JSON согласно заголовкам и ожиданиям».
Data → String (например, для диагностики)
Иногда нам нужно посмотреть первые символы ответа: для логов, для отладки, для сообщений об ошибке. Конвертация бывает успешной только если encoding совпал (часто UTF‑8).
import Foundation
let data = Data("Hello, HTTP".utf8)
let text = String(data: data, encoding: .utf8) ?? "<not utf8>"
print(text) // Hello, HTTP
Если String(data:encoding:) вернул nil, это не «Swift вредничает». Это значит: «я не могу гарантировать, что эти байты — корректный UTF‑8 текст».
Data → JSON-модель (Decodable)
Когда сервер возвращает JSON, мы обычно хотим сразу превратить его в тип. Это то, что мы уже делали на теме Codable/Decodable, просто теперь источник байтов — не файл и не строка в коде, а сеть.
Пусть наш сервер когда-нибудь будет отдавать «книгу»:
import Foundation
struct BookDTO: Decodable {
let id: Int
let title: String
}
let json = #"{"id":42,"title":"Swift для людей"}"#
let data = Data(json.utf8)
let book = try JSONDecoder().decode(BookDTO.self, from: data)
print(book.title) // Swift для людей
Обратите внимание на важную деталь: декодирование может бросить ошибку (throws). То есть «тело пришло» ещё не означает «мы смогли его прочитать как ожидаемую модель».
Пустое тело — это тоже нормально
Не все успешные ответы обязаны содержать тело. Например, статус 204 No Content буквально говорит: «успешно, но данных нет». Это не ошибка и не «сервер забыл». Это нормальная часть протокола.
Вот почему клиентский код обычно разделяет: «проверить статус» и «работать с телом». Сначала статус, потом тело.
7. Учимся читать ответ: статус → headers → body
Чтобы не прыгать сразу в URLSession, полезно сделать маленький учебный слой: будто мы уже получили ответ, и теперь должны корректно его интерпретировать. Это тренирует правильный порядок мыслей: статус → headers → body.
Смоделируем «ответ сервера» как структуру:
import Foundation
struct HTTPResponseDraft {
let statusCode: Int
let headers: [String: String]
let body: Data
}
Теперь напишем функцию, которая пытается «показать человеку» тело, но аккуратно, без предположений:
import Foundation
func debugBody(_ data: Data) -> String {
String(data: data, encoding: .utf8) ?? "<body is not utf8>"
}
let response = HTTPResponseDraft(
statusCode: 404,
headers: ["Content-Type": "application/json"],
body: Data(#"{"error":"Not found"}"#.utf8)
)
print(debugBody(response.body)) // {"error":"Not found"}
Смысл здесь не в том, что мы «всё сделали правильно», а в том, что мы формируем привычку: тело — это Data, и его надо интерпретировать осторожно.
Как это связано с реальными запросами в Swift
На Apple-платформах и в Foundation реальные HTTP-запросы обычно выполняют через URLSession, а запрос описывают через URLRequest. Это базовая инфраструктура, и позже мы к ней перейдём. Но сегодняшняя лекция специально про «азбуку»: если вы не понимаете, что такое методы, статусы, заголовки и тело, то URLSession будет выглядеть как «случайный генератор трёх optional’ов».
В руководствах по серверной разработке на Swift (например, в Vapor) очень наглядно видно, как маршруты различаются по методам (GET/POST) и как JSON идёт через тело запроса/ответа. Даже если мы сейчас пишем не сервер, а клиент, этот взгляд полезен: сервер тоже мыслит «метод + путь + тело + заголовки».
8. Типичные ошибки
Ошибка №1: «Если пришёл ответ — значит успех».
Новички часто проверяют только факт, что «ответ существует», и начинают декодировать тело. Но HTTP устроен иначе: сервер может совершенно корректно ответить статусом 404 или 500, и тело при этом может быть хоть JSON, хоть HTML. Правильная логика всегда начинается со статус-кода, и только затем вы решаете, что делать с телом.
Ошибка №2: путаница Accept и Content-Type.
Когда вы ставите Content-Type: application/json, вы говорите: «моё тело запроса — JSON». Когда вы ставите Accept: application/json, вы говорите: «я хочу получить JSON в ответе». Если перепутать, иногда «прокатит», иногда сервер вас проигнорирует, а иногда вернёт ошибку. Хуже всего, что баг может проявляться не всегда — а только на конкретном endpoint’е.
Ошибка №3: считать, что тело ответа — это всегда UTF‑8 строка.
Тело ответа — это байты. Это может быть JSON (часто UTF‑8), но может быть и картинка, PDF, архив, или просто данные в другой кодировке. Даже текстовый ответ не обязан быть UTF‑8. Поэтому конвертация Data → String должна быть опциональной, и если она не получилась — это нормальная ситуация, а не повод «дожать через !».
Ошибка №4: пытаться декодировать JSON при любом статусе.
Иногда API в случае ошибки (4xx/5xx) отдаёт тело другого формата: например, { "message": "..." } вместо ожидаемой модели книги. Если вы при статусе 404 попытаетесь декодировать BookDTO, вы получите decoding error и потеряете реальную причину: «книги не существует». Поэтому статус-код должен влиять на то, что именно вы декодируете, и нужно разделять «HTTP-ошибка» и «ошибка декодирования».
Ошибка №5: игнорировать заголовки, а потом удивляться «почему декодирование странное».
Заголовки — это подсказки о формате. Если Content-Type не тот, что вы ждёте, это сильный сигнал остановиться и хотя бы в логах показать тело как текст (если получается) или как «байты N длины». Когда заголовки игнорируются полностью, отладка превращается в гадание: «сервер плохой» или «клиент кривой» — а ответ уже прямо говорил, что случилось.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ