Как делать11 октября 2026 г.8 мин чтения
Spring BootШаг 1 из 7

HTTP и JSON: запросы, ответы, заголовки и формат данных

Коротко

HTTP-запрос — это текстовое сообщение из метода, адреса, заголовков и необязательного тела; ответ — из статус-кода, заголовков и тела. JSON — формат, в котором тело обычно передаёт данные: объекты, массивы, строки в двойных кавычках, числа, true, false и null. Чтобы уверенно работать с API, нужно понимать три вещи: что означает метод (безопасен ли он и можно ли его повторить), что говорит класс статус-кода и какой Content-Type описывает тело.

ОсновыУровень: НачальныйHTTP и JSON

После материала вы сможете

  • разобрать HTTP-запрос и ответ на части: метод, адрес, заголовки, тело, статус-код
  • объяснить, какие методы безопасны и идемпотентны по RFC 9110 и почему POST нельзя слепо повторять
  • отправить GET и POST с JSON через curl и fetch и проверить статус ответа, а не только сам факт ответа
  • найти причину ошибки JSON.parse и предсказать, что JSON.stringify потеряет при сериализации

Проверено на

Node.js 24 LTS (24.18)curl 8.21HTTP/1.1JSON (RFC 8259)

Сверено с документацией: 11 октября 2026 г.

Содержание
  1. 01Из чего состоят запрос и ответ
  2. 02Методы: безопасность и идемпотентность
  3. 03Статус-коды и заголовки, которые нужно знать
  4. 04Практика: свой API на node:http, запросы через curl и fetch
  5. 05JSON: синтаксис, JSON.parse и JSON.stringify
  6. 06Запросы в DevTools и CORS в двух абзацах

Из чего состоят запрос и ответ

В уроке о том, как устроен сайт, запрос был «официантом» между браузером и сервером. Здесь заглянем внутрь: что именно официант несёт. HTTP (HyperText Transfer Protocol) — протокол обмена сообщениями: клиент отправляет запрос, сервер возвращает ответ. Клиентом может быть браузер, мобильное приложение, другой сервер или утилита curl в терминале.

Запрос в HTTP/1.1 — это обычный текст. Первая строка содержит метод, адрес (request target) и версию протокола. Дальше идут заголовки — пары «имя: значение», каждая на своей строке. Затем пустая строка, а после неё — необязательное тело. Так выглядит запрос, который создаёт заказ:

HTTP
POST /api/orders HTTP/1.1
Host: 127.0.0.1:8080
Content-Type: application/json
Content-Length: 27

{"item": "latte", "qty": 1}
HTTP-запрос с JSON-телом: стартовая строка, заголовки, пустая строка, тело

Ответ устроен так же, только первая строка другая: версия протокола, трёхзначный статус-код и короткая текстовая фраза, например HTTP/1.1 201 Created. Фраза носит справочный характер — программы ориентируются на число.

Методы: безопасность и идемпотентность

Метод сообщает серверу, чего хочет клиент. Стандарт RFC 9110 вводит два свойства, от которых зависит поведение браузеров, прокси и вашего собственного кода. Безопасный метод по смыслу только читает данные и не просит сервер ничего менять. Идемпотентный метод даёт тот же итог на сервере, сколько бы раз одинаковый запрос ни повторили.

GET

Для чего
Получить представление ресурса
Безопасный
Да
Идемпотентный
Да

HEAD

Для чего
То же, что GET, но только заголовки
Безопасный
Да
Идемпотентный
Да

POST

Для чего
Передать данные на обработку, создать запись
Безопасный
Нет
Идемпотентный
Нет

PUT

Для чего
Создать или полностью заменить ресурс по адресу
Безопасный
Нет
Идемпотентный
Да

PATCH

Для чего
Частично изменить ресурс (RFC 5789)
Безопасный
Нет
Идемпотентный
Нет

DELETE

Для чего
Удалить ресурс
Безопасный
Нет
Идемпотентный
Да

Идемпотентность — практическое свойство, а не теория. RFC 9110 прямо объясняет: если соединение оборвалось до ответа, клиент может повторить PUT или DELETE, потому что повтор не изменит итог. С POST так нельзя: повтор создаст второй заказ или спишет деньги дважды. Поэтому платёжные API добавляют к POST свои механизмы защиты от дублей, а формы блокируют повторную отправку.

Статус-коды и заголовки, которые нужно знать

Статус-код — трёхзначное число от 100 до 599. Первая цифра задаёт класс ответа, и по RFC 9110 клиент обязан понимать хотя бы класс: незнакомый код 471 он должен обработать как 400. Отсюда правило для своего кода: сначала проверяйте класс, потом конкретные значения.

2xx

Что значит
Успех: запрос получен, понят и принят
Когда встречается
200 OK — данные в теле; 201 Created — запись создана, адрес в Location; 204 No Content — успех без тела

3xx

Что значит
Нужно дополнительное действие
Когда встречается
301, 302, 308 — ресурс по другому адресу

400

Что значит
Сервер не может обработать запрос из-за ошибки клиента
Когда встречается
Тело — не корректный JSON

401

Что значит
Нет действующих учётных данных
Когда встречается
Токен не передан или истёк

404

Что значит
Ресурс не найден
Когда встречается
Заказа с таким id нет

415

Что значит
Формат тела не поддерживается
Когда встречается
Отправили JSON без Content-Type: application/json

422

Что значит
Синтаксис верный, но данные не проходят правила
Когда встречается
Количество товара равно нулю

5xx

Что значит
Сервер не смог выполнить корректный запрос
Когда встречается
500 — необработанная ошибка в коде сервера

Заголовки описывают сообщение, а не данные в нём. Имена заголовков не зависят от регистра: Content-Type и content-type — одно и то же. Node.js в req.headers приводит все имена к нижнему регистру, поэтому в коде сервера читают req.headers["content-type"].

  • Content-Type — формат тела: application/json, text/html; charset=utf-8. Если заголовка нет, получатель по RFC 9110 может считать тело просто байтами или угадывать тип — на это не стоит полагаться.
  • Content-Length — размер тела в байтах, не в символах: кириллическая буква в UTF-8 занимает два байта.
  • Authorization — учётные данные в виде «схема значение», например Bearer YOUR_TOKEN. Такой токен даёт доступ любому, у кого он оказался, поэтому передавать его можно только по HTTPS.
  • Cache-Control — правила кэширования. no-store запрещает сохранять ответ, no-cache разрешает хранить, но требует перепроверки перед каждым использованием, max-age=N — ответ свежий N секунд.
  • Location — адрес созданного ресурса в ответе 201 или новый адрес при перенаправлении.

Практика: свой API на node:http, запросы через curl и fetch

Лучший способ увидеть HTTP — поднять маленький сервер и отправить ему запросы. Понадобится только Node.js 24: модуль node:http встроен, внешних пакетов нет. Сервер хранит заказы в памяти, отдаёт заказ по GET и создаёт новый по POST, проверяя заголовок, синтаксис JSON и сами данные — каждой проверке соответствует свой статус-код.

server.mjs
import { createServer } from "node:http";

const orders = new Map([[1, { id: 1, item: "cappuccino", qty: 2 }]]);
let nextId = 2;

function sendJson(res, status, data, headers = {}) {
  const body = JSON.stringify(data);
  res.writeHead(status, {
    "Content-Type": "application/json; charset=utf-8",
    "Content-Length": Buffer.byteLength(body),
    ...headers,
  });
  res.end(body);
}

async function readBody(req) {
  const chunks = [];
  for await (const chunk of req) chunks.push(chunk);
  return Buffer.concat(chunks).toString("utf8");
}

const server = createServer(async (req, res) => {
  const url = new URL(req.url, "http://localhost");
  console.log(req.method, url.pathname, req.headers["content-type"] ?? "-");

  const match = url.pathname.match(/^\/api\/orders\/(\d+)$/);
  if (req.method === "GET" && match) {
    const order = orders.get(Number(match[1]));
    if (!order) return sendJson(res, 404, { error: "Order not found" });
    return sendJson(res, 200, order, { "Cache-Control": "no-store" });
  }

  if (req.method === "POST" && url.pathname === "/api/orders") {
    if (!req.headers["content-type"]?.startsWith("application/json")) {
      return sendJson(res, 415, { error: "Expected Content-Type: application/json" });
    }
    let input;
    try {
      input = JSON.parse(await readBody(req));
    } catch {
      return sendJson(res, 400, { error: "Request body is not valid JSON" });
    }
    if (typeof input?.item !== "string" || !Number.isInteger(input.qty) || input.qty < 1) {
      return sendJson(res, 422, { error: "Fields required: item (string), qty (integer >= 1)" });
    }
    const order = { id: nextId++, item: input.item, qty: input.qty };
    orders.set(order.id, order);
    return sendJson(res, 201, order, { Location: `/api/orders/${order.id}` });
  }

  sendJson(res, 404, { error: "Not found" });
});

server.listen(8080, "127.0.0.1", () => {
  console.log("API: http://127.0.0.1:8080");
});
Учебный сервер: данные в памяти и пропадают при перезапуске, нет авторизации — не для продакшена

Запустите node server.mjs и откройте второй терминал. Ключ -s у curl убирает индикатор прогресса, -i показывает статусную строку и заголовки ответа, -X задаёт метод, -H добавляет заголовок, -d передаёт тело. Команды ниже — для bash (macOS, Linux, Git Bash в Windows): в PowerShell кавычки экранируются иначе. Вывод — реальный, с нашего запуска:

terminal
$ curl -si http://127.0.0.1:8080/api/orders/1
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 36
Cache-Control: no-store
Date: Sun, 11 Oct 2026 21:56:00 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"id":1,"item":"cappuccino","qty":2}

$ curl -si -X POST http://127.0.0.1:8080/api/orders -H 'Content-Type: application/json' -d '{"item": "latte", "qty": 1}'
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Content-Length: 31
Location: /api/orders/2
Date: Sun, 11 Oct 2026 21:56:00 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"id":2,"item":"latte","qty":1}

$ curl -si -X POST http://127.0.0.1:8080/api/orders -H 'Content-Type: application/json' -d "{'item': 'latte'}"
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Content-Length: 42
Date: Sun, 11 Oct 2026 21:56:00 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"error":"Request body is not valid JSON"}

$ curl -si -X POST http://127.0.0.1:8080/api/orders -d '{"item": "latte", "qty": 1}'
HTTP/1.1 415 Unsupported Media Type
Content-Type: application/json; charset=utf-8
Content-Length: 51
Date: Sun, 11 Oct 2026 21:56:01 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"error":"Expected Content-Type: application/json"}
Четыре запроса — четыре разных ответа: 200, 201, 400 и 415

Обратите внимание на последний запрос. Без -H curl сам ставит Content-Type: application/x-www-form-urlencoded — так в журнале нашего сервера и записалось. Тело при этом содержит JSON, но сервер, как и положено, верит заголовку и отвечает 415. Это одна из самых частых причин «сервер не видит мои данные».

Теперь то же из кода. fetch встроен в Node.js начиная с 18-й версии и стабилен с 21-й; в браузере он работает так же. Перезапустите сервер, чтобы нумерация заказов началась заново, и выполните клиент:

client.mjs
const API = "http://127.0.0.1:8080/api";

async function request(method, path, body) {
  const res = await fetch(API + path, {
    method,
    headers: body === undefined ? {} : { "Content-Type": "application/json" },
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  const data = await res.json();
  console.log(method, path, "→", res.status, res.ok ? "ok" : "error", data);
  return data;
}

await request("GET", "/orders/1");
const created = await request("POST", "/orders", { item: "latte", qty: 1 });
await request("GET", `/orders/${created.id}`);
await request("POST", "/orders", { item: "latte", qty: 0 });
await request("GET", "/orders/999");
Клиент на fetch: тело превращается в JSON через JSON.stringify, ответ разбирается через res.json()
terminal
$ node client.mjs
GET /orders/1 → 200 ok { id: 1, item: 'cappuccino', qty: 2 }
POST /orders → 201 ok { id: 2, item: 'latte', qty: 1 }
GET /orders/2 → 200 ok { id: 2, item: 'latte', qty: 1 }
POST /orders → 422 error { error: 'Fields required: item (string), qty (integer >= 1)' }
GET /orders/999 → 404 error { error: 'Order not found' }
Реальный вывод: fetch не бросает исключение на 404 и 422

JSON: синтаксис, JSON.parse и JSON.stringify

JSON (JavaScript Object Notation) описан в RFC 8259. Значение в JSON — это объект, массив, число, строка или одно из трёх слов: true, false, null. Правила строже, чем в JavaScript: строки и имена полей только в двойных кавычках, без завершающих запятых, без комментариев, числа без ведущих нулей, а NaN и Infinity запрещены. Между системами JSON передают в кодировке UTF-8, медиатип — application/json.

В JavaScript с JSON работают две функции. JSON.stringify превращает значение в строку для отправки, JSON.parse — строку обратно в значение. Обе умеют молча терять данные или бросать исключения. Этот скрипт собирает типичные ловушки; ниже — реальный вывод Node.js 24:

json-pitfalls.mjs
const order = {
  id: 7,
  createdAt: new Date("2026-10-11T09:30:00Z"),
  comment: undefined,
  discount: NaN,
  format() {},
};
console.log(JSON.stringify(order));
console.log(JSON.stringify({ items: ["latte", undefined] }));
console.log(JSON.stringify({ id: 1, tags: ["new"] }, null, 2));

const broken = [
  "{'item': 'latte'}",
  '{"item": "latte",}',
  '{"qty": 012}',
  "",
];
for (const text of broken) {
  try {
    JSON.parse(text);
  } catch (error) {
    console.log(error.name + ":", error.message);
  }
}

console.log(JSON.parse('{"id": 9007199254740993}').id);

try {
  JSON.stringify({ total: 10n });
} catch (error) {
  console.log(error.name + ":", error.message);
}

const a = { name: "a" };
a.self = a;
try {
  JSON.stringify(a);
} catch (error) {
  console.log(error.name + ":", error.message.split("\n")[0]);
}
Что JSON.stringify теряет и на чём падает JSON.parse
terminal
$ node json-pitfalls.mjs
{"id":7,"createdAt":"2026-10-11T09:30:00.000Z","discount":null}
{"items":["latte",null]}
{
  "id": 1,
  "tags": [
    "new"
  ]
}
SyntaxError: Expected property name or '}' in JSON at position 1 (line 1 column 2)
SyntaxError: Expected double-quoted property name in JSON at position 17 (line 1 column 18)
SyntaxError: Unexpected number in JSON at position 9 (line 1 column 10)
SyntaxError: Unexpected end of JSON input
9007199254740992
TypeError: Do not know how to serialize a BigInt
TypeError: Converting circular structure to JSON
Тексты ошибок — движка V8 в Node.js 24; в Firefox и Safari формулировки другие

Поля comment и format исчезли

Почему
undefined и функции в объекте пропускаются, в массиве становятся null
Что делать
Передавайте null явно, если поле должно быть

discount стал null

Почему
NaN и Infinity в JSON недопустимы и сериализуются как null
Что делать
Проверяйте числа до отправки

Дата стала строкой

Почему
У Date есть метод toJSON, он возвращает строку ISO 8601
Что делать
При разборе превращайте строку обратно в Date сами

SyntaxError при разборе

Почему
Одинарные кавычки, завершающая запятая, ведущий ноль или пустая строка
Что делать
Оборачивайте JSON.parse в try/catch и отвечайте 400

9007199254740993 стало …992

Почему
Числа JSON разбираются в 64-битные числа с плавающей точкой; RFC 8259 гарантирует точность только для целых до 2^53 − 1
Что делать
Большие идентификаторы и суммы передавайте строками

TypeError у BigInt и циклической ссылки

Почему
Для них нет правила сериализации
Что делать
Преобразуйте значение заранее или используйте replacer

Запросы в DevTools и CORS в двух абзацах

  1. Откройте вкладку Network

    В Chrome или Edge — F12, затем Network. Обновите страницу или нажмите кнопку, которая отправляет запрос. Фильтр Fetch/XHR оставит только запросы к API.

  2. Посмотрите заголовки

    Кликните по запросу. На вкладке Headers — адрес, метод, статус-код, заголовки запроса и ответа.

  3. Посмотрите тело

    Payload — что отправил клиент, Response — сырой ответ сервера, Preview — тот же ответ в удобном виде.

  4. Повторите в терминале

    Правый клик по запросу → Copy → Copy as cURL. Получится команда curl с теми же заголовками: удобно, чтобы воспроизвести ошибку вне браузера и показать её бэкенд-разработчику.

Если тот же запрос работает в curl, но падает в браузере с ошибкой в консоли, почти наверняка дело в CORS (Cross-Origin Resource Sharing). Браузер по политике одного источника (same-origin policy) не даёт скрипту читать ответы с другого домена, порта или схемы, пока сервер явно не разрешит это заголовком Access-Control-Allow-Origin. Curl и серверный код этой политике не подчиняются — это защита пользователя в браузере, а не свойство HTTP.

Для «непростых» запросов — например, POST с Content-Type: application/json или с заголовком Authorization — браузер сначала отправляет предварительный запрос OPTIONS (preflight) и спрашивает, разрешены ли такой метод и заголовки. Подробности причины JavaScript не узнает: по соображениям безопасности они видны только в консоли браузера. Исправляют CORS на сервере, а не во фронтенде.

  • Метод соответствует действию: чтение — GET, создание — POST, без изменений данных через GET
  • У запроса с телом есть верный Content-Type
  • Код проверяет res.ok или res.status, прежде чем читать тело
  • JSON.parse входящих данных обёрнут в try/catch
  • Большие числа и деньги передаются строками
  • Токены и ключи — только в заголовке Authorization, только по HTTPS и не во фронтенд-коде
Как проверено5 · пройдено1 · не запускалосьсверено 11 октября 2026 г.
  • запущено, работает

    server.mjs (node:http, GET/POST, 200/201/400/404/415/422)

    node server.mjs на Node.js 24.18.0 (Windows 11), 127.0.0.1:8080; запросы curl 8.21.0 и client.mjs, вывод в статье — реальный из этого запуска (curl.out, client.out); журнал сервера подтвердил Content-Type: application/x-www-form-urlencoded при curl -d без -H

  • запущено, работает

    client.mjs (fetch)

    node client.mjs против свежезапущенного server.mjs, Node.js 24.18.0; вывод вставлен без правок

  • запущено, работает

    curl-команды раздела «Практика»

    выполнены в Git Bash (curl 8.21.0) против server.mjs; вывод, включая заголовок Date, — реальный

  • запущено, работает

    json-pitfalls.mjs

    node json-pitfalls.mjs, Node.js 24.18.0; тексты SyntaxError/TypeError — реальные сообщения V8

  • запущено, работает

    Решение упражнения DELETE (фрагмент)

    вставлен в копию server.mjs перед финальным 404; curl -X DELETE дважды: 204, затем 404

  • не запускалось

    Сырой HTTP-запрос (lang http)

    иллюстрация формата сообщения, сверена с MDN HTTP messages; Content-Length 27 соответствует телу

«Запущено» — код выполнен во временном проекте или контейнере, вывод в статье взят из прогона. «Частично» — проверено с заглушками вместо внешних сервисов. «Не запускалось» — нужны устройство, аккаунт или ключ; шаги сверены с официальной документацией. Файлы проверочного проекта и логи запусков сохранены в архиве InterLAB.

Расскажите о задаче — мы изучим запрос и свяжемся с вами

Есть задача?
Обсудим и оценим — бесплатно.

Расскажите о продукте или проблеме. Предложим архитектуру, срок и стоимость первой версии после изучения задачи.

  • Оценка объёма и рисков до старта
  • NDA — по вашему или нашему шаблону
  • Старт работ в течение недели

Не присылайте через форму пароли, ключи доступа и исходный код — безопасную передачу доступа обсудим отдельно.

Нажимая кнопку, вы соглашаетесь с обработкой персональных данных.