HTTP/HTTPS на пальцах для разработчика: методы, заголовки, коды и типичные ошибки клиентов
Соберём ментальную модель HTTP и разберём, как правильно строить запросы, что читать в ответах и где чаще всего ломается интеграция.
Содержание
HTTP/HTTPS на пальцах для разработчика: методы, заголовки, коды и типичные ошибки клиентов
HTTP и HTTPS — это не «протоколы из учебника», а рабочая инфраструктура любой интеграции: от веб-страницы в браузере до межсервисного общения в микросервисной архитектуре. Большинство проблем “оно работает в Postman, но не работает в проде” — это не магия, а несоответствие ожиданий: клиент неправильно формирует запрос, сервер по-своему интерпретирует заголовки, а обработчик ответа предполагает, что статус-коды и тело всегда выглядят одинаково.
Ниже — ментальная модель HTTP/HTTPS “на пальцах” с практическими примерами: как строить запросы, что читать в ответах, какие заголовки реально важны и где чаще всего ломается интеграция.
Ментальная модель: что на самом деле происходит в HTTP
HTTP как договор между двумя программами
HTTP — протокол прикладного уровня: он описывает, как клиент запрашивает ресурсы/действия у сервера и как сервер отвечает. В отличие от “транспорта” (TCP/QUIC), HTTP не гарантирует доставку и порядок сам по себе — это забота ниже уровнем. Но он задаёт структуру сообщения: метод, путь, заголовки, тело запроса и аналогично для ответа.
Типичный запрос выглядит так (схематично):
- Метод:
GET,POST,PUT,PATCH,DELETEи т.д. - URI/путь: например
/api/v1/orders/123 - Версия:
HTTP/1.1 - Заголовки запроса:
Host,Authorization,Content-Type,Accept,User-Agent,Idempotency-Keyи т.п. - Тело запроса (обычно не для GET/DELETE): JSON, форма, бинарные данные
Ответ:
- Статус-код:
200,201,204,400,401,404,409,429,500… - Заголовки ответа:
Content-Type,Location,Retry-After,ETag,Cache-Control… - Тело ответа (иногда пустое): JSON, HTML, текст ошибки…
HTTP/HTTPS почти всегда идет поверх транспорта, а HTTPS добавляет TLS: шифрование и верификацию сервера.
HTTPS: где возникают “неочевидные” проблемы
HTTPS = HTTP поверх TLS. В реальной жизни ошибки часто происходят не в HTTP, а в TLS или в параметрах соединения:
- неверный CA/цепочка сертификатов на машине клиента;
- SNI (Server Name Indication) — когда клиент не отправил правильное имя хоста, и сертификат не соответствует домену;
- запреты старых протоколов/шифров (например, клиент пытается на
TLS 1.0/1.1, а сервер требует1.2+); - проверка имени хоста (hostname verification) игнорируется “в тестах”, а в проде запрещена.
Иногда проблема выглядит как HTTP-ошибка, но по факту — на уровне TLS: handshake неуспешен, запрос не доходит до сервера.
Методы: что они значат для сервера и клиента
Методы — это не просто “глаголы”. Они меняют ожидания по идемпотентности, семантике кэша, обработке на прокси/балансерах.
GET — чтение без побочных эффектов (идеально)
GET /resource должен возвращать представление ресурса. Ключевые нюансы:
- Тело в GET обычно игнорируется практическими библиотеками; не стоит на него рассчитывать.
- Кэширование возможно, но не гарантировано — зависит от заголовков.
- Параметры передаются в query string:
?limit=20&cursor=....
Пример:
GET /api/v1/users?limit=10 HTTP/1.1
Host: example.com
Accept: application/json
Authorization: Bearer <token>
POST — создание или действие
POST обычно означает “создай/выполни” с передачей данных в теле. Сервер может создать ресурс и вернуть 201 Created + Location.
Пример:
POST /api/v1/orders HTTP/1.1
Host: example.com
Content-Type: application/json; charset=utf-8
Accept: application/json
Authorization: Bearer <token>
{"customerId":123,"items":[{"sku":"A1","qty":2}]}
PUT — полная замена ресурса
PUT /resource/{id} обычно воспринимается как “замени представление целиком”. И часто он идемпотентен: повторный запрос с тем же телом должен дать тот же результат (если сервер корректно реализован).
PATCH — частичное изменение
PATCH применяется для частичных обновлений. Тоже обычно стараются сделать его идемпотентным на практике (например, с операциями “set if absent”, но это зависит от API).
DELETE — удаление
DELETE может вернуть 204 No Content или 200 OK (иногда — с удалённым объектом). Важное: повторный DELETE часто должен быть безопаснее (идемпотентнее), но не все серверы реализуют это аккуратно.
Типичная ошибка клиента по методам
Клиенты часто выбирают метод “по удобству”:
- отправляют JSON в
GET(и сервер его игнорирует); - используют
PUTкак “частичный update” (а сервер заменяет поле наnull, ломая данные); - повторяют
POSTпри сетевых таймаутах без идемпотентности (и получают двойные заказы).
Коды статуса: что читать и как строить обработку
Статус-код — главный сигнал для клиента. Не стоит пытаться “угадать” по телу. Нужна система: разделять успешные, клиентские ошибки, серверные ошибки и специфические случаи.
2xx: успешные ответы
Частые варианты:
200 OK— стандартный успех, тело обычно присутствует.201 Created— ресурс создан. Часто естьLocationна новый URL.202 Accepted— принято к обработке, но результат будет позже (асинхронщина).204 No Content— успех без тела (например, удаление или действие без ответа).
Пример стратегии:
- Если
204— не пытайтесь читать JSON. - Если
201/202— возможно, нужно следить заLocationили job status.
3xx: редиректы
Клиент может следовать редиректам автоматически, но это не всегда безопасно:
- редирект может быть на другой метод (в зависимости от реализации клиента);
- может утечь
Authorizationзаголовок, если редирект на другой хост и библиотека сделала “не то” (в современных библиотеках обычно аккуратнее, но не везде).
Для HTTP-клиентов в коде стоит понимать настройки: следовать редиректам или обрабатывать вручную.
4xx: ошибки клиента
Ключевые коды:
400 Bad Request— синтаксически неверный запрос (например, неверный JSON, отсутствует обязательное поле).401 Unauthorized— обычно нет аутентификации или токен невалиден (важно:WWW-Authenticateможет подсказать схему).403 Forbidden— аутентификация есть, но прав недостаточно.404 Not Found— ресурс не найден. Иногда скрывают наличие (security through obscurity), особенно в приватных API.409 Conflict— конфликт состояния (например, версия не совпадает).413 Payload Too Large— тело слишком большое.415 Unsupported Media Type— неверныйContent-Type.422 Unprocessable Entity— часто используют для валидации семантики (возможны детали в теле).429 Too Many Requests— rate limit. Часто естьRetry-Afterи/или поля в теле.
5xx: ошибки сервера
500 Internal Server Error— общая ошибка.502 Bad Gateway/503 Service Unavailable/504 Gateway Timeout— проблемы на уровне прокси/балансера/апстрима.- В этих случаях клиенту часто нужен ретрай, но аккуратно: не вечно и не всегда.
Типичная ошибка: считать, что “если статус 200 — тело всегда JSON”
Если сервер возвращает Content-Type: text/plain или пустое тело — парсер JSON упадёт. Правильнее:
- проверять
Content-Type; - учитывать
204; - в ошибках читать тело и/или структуру ошибок, но не считать её единственным источником правды.
Заголовки, которые действительно важны
Заголовки — это механизм настройки поведения “по договорённости”. Некоторые обязательны, многие — критичны для корректной интерпретации данных.
Accept и Content-Type: договор про форматы
Content-Type— как интерпретировать тело запроса.Accept— что клиент ожидает получить в ответе.
Ошибки интеграций почти всегда здесь:
- отправили JSON, но указали
Content-Type: application/x-www-form-urlencoded; - не указали
Accept, и сервер вернул HTML-страницу ошибку; - не совпали кодировки/границы multipart.
Пример:
Content-Type: application/json; charset=utf-8
Accept: application/json
Host
В HTTP/1.1 заголовок Host обязателен. Для HTTPS он связан с SNI: хотя SNI формируется из домена в TLS handshake, а Host помогает в HTTP-мультихостинге. Если отправлять запросы “в обход” стандартных библиотек, можно получить “роутинг мимо” нужного виртуального хоста.
Authorization
Чаще всего это Bearer <token> или Basic. Важно:
- не логировать токены в plaintext;
- корректно обрабатывать 401: иногда нужно обновлять токен;
- аккуратно с редиректами (см. 3xx).
Пример:
Authorization: Bearer eyJhbGciOi...
User-Agent
Обычно не критично для корректности, но иногда влияет на поведение WAF/Rate limiting/feature flags. В интеграциях часто ставят дефолт.
Idempotency-Key (или аналоги)
Для POST, который может приводить к побочным эффектам (создание заказа, платеж, отправка письма), многие API поддерживают идемпотентность через ключ.
Практический смысл для клиента:
- при сетевых ретраях можно повторить запрос без дублирования эффекта.
Пример:
POST /api/v1/payments
Idempotency-Key: 7d6f3a1c-2c7e-4c3e-a0b4-1b6b3a3a1b21
Content-Type: application/json
Accept: application/json
Authorization: Bearer <token>
{"orderId":"123","amount":100.50,"currency":"RUB"}
ETag и If-Match / If-None-Match: оптимистичная конкуренция
Для корректной работы при конкурентных обновлениях:
- клиент получает
ETagиз ответа; - при обновлении добавляет
If-Match: <etag>; - сервер принимает обновление только если версия совпадает.
Пример:
GET /api/v1/profile HTTP/1.1
Accept: application/json
Ответ:
HTTP/1.1 200 OK
Content-Type: application/json
ETag: "v12"
Далее обновление:
PUT /api/v1/profile HTTP/1.1
Content-Type: application/json
If-Match: "v12"
{"name":"Алина"}
При конфликте — 412 Precondition Failed или 409 Conflict (зависит от API).
Retry-After: когда ретраить
Для 429 Too Many Requests сервер часто указывает время ожидания. Клиенту лучше уважать это, иначе можно устроить “само-DDoS” на собственную интеграцию.
Как правильно строить запросы: чеклист
Ниже — практический список, который стоит применять к любому интеграционному коду.
1) Правильно формируйте URL и query
- кодируйте параметры (особенно если есть пробелы/кириллица/слеши);
- не смешивайте path и query строкой “вручную”;
- следите за завершающими слешами:
/resourceи/resource/иногда ведут к разным маршрутам.
2) Всегда указывайте Content-Type для тел
Если отправляете JSON — делайте это явно.
3) Всегда обрабатывайте пустые ответы и разные Content-Type
204— нет тела;Content-Typeопределяет, как парсить (JSON/text/xml/binary);- в ошибках тело может быть структурированным, но не всегда — сервер может вернуть HTML от прокси/WAF.
4) Не полагайтесь на “успех по факту парсинга”
Типичный анти-паттерн: “раз JSON распарсился — значит всё ок”. Нет: сервер может вернуть {"error":"..."}
с 200 (иногда так делает фронт-ориентированное API) — или вернуть {"message":"..."}
с 400. Нужно смотреть статус-код.
5) Таймауты и ретраи — отдельная тема (и отдельные риски)
- сетевые таймауты не равны серверным ошибкам;
POSTбез идемпотентности может приводить к дубликатам;- ретраи на
4xxобычно не помогают (кроме отдельных случаев вроде429).
Что читать в ответах: структура и нюансы
Content-Type — ориентир для парсинга
Представьте, что вы ожидаете JSON, но сервер вернул text/html (например, страницу ошибки от обратного прокси).
Если клиент без проверки начинает парсить JSON — получит “ложную” ошибку парсинга вместо реальной причины.
Практика:
- прочитать заголовок
Content-Type; - если
application/json— парсить JSON; - иначе — логировать тело как строку (с ограничением размера).
Location при 201 Created
Если сервер создаёт ресурс, часто он возвращает Location: /api/v1/orders/123.
Клиент должен использовать его для перехода, вместо того чтобы строить URL “на глаз” по шаблону.
Заголовки кэширования (если актуально)
Если API использует кэш:
Cache-Control/ETagзадают поведение;- иногда уместен
If-None-Matchдля получения304 Not Modified.
Ошибки валидации: 400 vs 422
Разные API по-разному разделяют “синтаксическая ошибка” и “семантическая”. Если вы видите 422 — часто в теле будет список полей/нарушений. Клиенту стоит уметь отображать это человеку или маппить в доменные ошибки.
Примеры кода: запрос, корректная обработка статусов и ошибок
Ниже — пример на JavaScript (Node.js) с fetch. Логика универсальна и применима к любому стеку: важно не конкретное API, а подход.
Универсальный клиент-обёртка
async function requestJson(url, { method = 'GET', headers = {}, body } = {}) {
const finalHeaders = {
Accept: 'application/json',
...headers,
};
let finalBody = undefined;
if (body !== undefined) {
// Если передали строку — считаем, что Content-Type уже выставлен снаружи
if (typeof body !== 'string') {
finalHeaders['Content-Type'] = finalHeaders['Content-Type'] ?? 'application/json; charset=utf-8';
finalBody = JSON.stringify(body);
} else {
finalBody = body;
}
}
const res = await fetch(url, {
method,
headers: finalHeaders,
body: finalBody,
});
// 204: нет тела
if (res.status === 204) return { status: res.status, data: null };
const contentType = res.headers.get('content-type') || '';
const isJson = contentType.includes('application/json');
if (res.ok) {
if (isJson) {
return { status: res.status, data: await res.json() };
}
// если успех, но не json — возвращаем текст
return { status: res.status, data: await res.text() };
}
// Для ошибок: попытаться прочитать тело, но не ломать обработку, если формат неожиданный
let errorBody;
if (isJson) {
errorBody = await res.json().catch(() => null);
} else {
errorBody = await res.text().catch(() => null);
}
return {
status: res.status,
error: errorBody,
};
}
Использование:
const r = await requestJson('https://api.example.com/api/v1/orders', {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
body: { customerId: 123, items: [{ sku: 'A1', qty: 2 }] },
});
if (r.error) {
// корректная маршрутизация ошибок по статус-кодам
if (r.status === 401) {
// обновить токен или пробросить пользователю
} else if (r.status === 429) {
// ретрай с учётом Retry-After
} else if (r.status === 422) {
// показать валидационные ошибки
}
} else {
console.log('OK', r.data);
}
Что добавить для идемпотентности POST
Если сервер поддерживает Idempotency-Key, стоит генерировать ключ на стороне клиента:
Комментарии
Пока нет комментариев