Перейти к основному содержимому

curl / fetch — API-запросы


Для кого эта статья

Готовые примеры HTTP-запросов с разбором каждой важной строки — как в галерее Turtle для рисования, только для API, curl и fetch.

Подойдёт, если вы:

  • делаете лабораторную, курсовую или pet-проект с бэкендом;
  • гуглите «curl post json», «fetch api пример», «как проверить api»;
  • учитесь JavaScript, Python, сетям или тестированию API;
  • видите в браузере 401, 404, 500 и хотите понять, виноват сервер или фронт.

Как запускать:

ИнструментГде
curlТерминал Windows (CMD), WSL, Linux, macOS. В PowerShell — curl.exe
fetchКонсоль браузера (F12 → Console) или Node.js 18+
requestsPython 3 после pip install requests

Учебные API бесплатны, регистрация не нужна: jsonplaceholder, httpbin.

Сначала теория

Полный справочник по curl — Утилита curl. Postman и коллекции — Работа с Postman и curl. Async и fetch в JS — Примеры JavaScript. Bash-скрипты — однострочники и скрипты, production-каркас — Примеры скриптов Linux.


Что такое API-запрос простыми словами

API — способ, которым программа спрашивает у сервера данные или отправляет их. Обычно по HTTP (как сайт в браузере, но ответ чаще JSON, а не красивая страница).

Один запрос состоит из:

ЧастьПримерЗачем
МетодGET, POSTЧто вы хотите сделать (прочитать, создать, удалить)
URLhttps://api.example.com/posts/1Куда стучаться
ЗаголовкиContent-Type: application/jsonФормат тела, токен, язык
Тело{"title":"hello"}Данные для POST/PUT (у GET тела обычно нет)

Ответ сервера тоже из кода статуса, заголовков и тела:

КодИмяОбычный смысл
200OKВсё хорошо, данные в теле
201CreatedРесурс создан (часто после POST)
400Bad RequestКлиент прислал мусор (битый JSON, не те поля)
401UnauthorizedНужен логин или токен
404Not FoundНет такого URL или id
500Internal Server ErrorУпал бэкенд — чинить сервер

curl — программа в терминале: удобно для лабораторных, проверки «жив ли сервер», CI.
fetch — встроенная функция в JavaScript (сайт, Node): то же HTTP, но из кода приложения.


Как пользоваться статьёй

  1. Найдите свой сценарий в оглавлении (GET, POST, токен, ошибки).
  2. Скопируйте код целиком.
  3. Прочитайте блок Разбор под примером.
  4. Выполните Попробуйте — одна маленькая правка закрепляет тему.
  5. Если fetch в браузере «ломается», повторите тот же URL в curl — так отделяют CORS от бага API.

Словарь за 30 секунд

ТерминСмысл
RESTСтиль API: ресурсы по URL, методы GET/POST/PUT/DELETE
JSONТекстовый формат {"ключ": "значение"} — стандарт для API
QueryПараметры в URL после ?, например ?userId=1
BearerТокен в заголовке Authorization: Bearer …
CORSПравила браузера: с чужого сайта нельзя читать ответ без разрешения сервера
-s (curl)Тихий режим — без progress-bar, только ответ
res.ok (fetch)true, если статус 200–299

Основы — с чего начать

Обязательный шаблон curl

Задача: один раз увидеть рабочий GET и понять, что curl печатает тело ответа в терминал.

curl "https://jsonplaceholder.typicode.com/posts/1"

Разбор:

  • Слово curl — запуск утилиты.
  • Строка в кавычках — URL. Кавычки обязательны, если в адресе есть & или пробелы.
  • Флагов нет → метод GET по умолчанию.
  • В терминале появится JSON — это тело ответа. Код 200 curl сам в stdout не пишет (его можно вывести флагом -w, см. ниже).
  • В stderr может мелькнуть progress-bar — это нормально.

Попробуйте: добавьте -s (silent) — останется только JSON:

curl -s "https://jsonplaceholder.typicode.com/posts/1"

Обязательный шаблон fetch

Задача: получить JSON в JavaScript (браузер F12 → Console или файл node app.mjs).

const res = await fetch('https://jsonplaceholder.typicode.com/posts/1');
const data = await res.json();
console.log(data.title);

Разбор:

  • fetch(url) отправляет GET и возвращает Promise с объектом Response.
  • await ждёт ответа сети (в консоли браузера top-level await разрешён).
  • res.json() читает тело как JSON и тоже асинхронный — нужен второй await.
  • data.title — поле из ответа сервера.
  • Если статус 404 или 500, fetch не бросает ошибку — только res.ok === false. Поэтому ниже добавляют проверку.

С проверкой ошибки HTTP:

const res = await fetch('https://jsonplaceholder.typicode.com/posts/1');
if (!res.ok) {
throw new Error(`HTTP ${res.status} ${res.statusText}`);
}
const data = await res.json();
console.log(data);

Разбор:

  • res.ok — сокращение для статусов 200–299.
  • res.status — число (200, 404, …), res.statusText — короткая фраза (OK, Not Found).
  • throw останавливает выполнение — в консоли увидите красную ошибку с кодом.

Попробуйте: подставьте URL https://jsonplaceholder.typicode.com/posts/99999 — часто приходит пустой объект и статус 404; проверка !res.ok сработает.

PowerShell и имя curl

В PowerShell curl часто — это Invoke-WebRequest, другой синтаксис. Пишите curl.exe или откройте CMD / WSL.


Стартовые запросы

Простые сценарии «с нуля» — с них удобно начинать отчёт по лабораторной или первый тест своего API.

GET — получить один пост (JSON)

Задача: прочитать ресурс по id — самый частый запрос в учебниках и реальных API.

curl -s "https://jsonplaceholder.typicode.com/posts/1"

Разбор:

  • -ssilent: убрать полоску загрузки, в stdout только ответ.
  • Путь /posts/1 — ресурс «пост номер 1».
  • Ответ — одна «простыня» JSON; переносы в body могут быть как \n.

Пример фрагмента ответа:

{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit\n..."
}
ПолеСмысл
userIdАвтор поста (связь с /users/1)
idИдентификатор поста
titleЗаголовок
bodyТекст

Попробуйте: замените 1 на 2 в URL и сравните title.


GET — список с фильтром (query-параметры)

Задача: не качать все 100 постов, а только три поста пользователя userId=1.

curl -s "https://jsonplaceholder.typicode.com/posts?userId=1&_limit=3"

Разбор:

  • После ? идут query-параметры: имя=значение, пары разделяют &.
  • userId=1 — фильтр «посты пользователя 1» (учебный API понимает такой параметр).
  • _limit=3 — взять не больше трёх записей (соглашение многих API).
  • Кавычки вокруг URL обязательны в bash/zsh: иначе символ & запустит команду в фоне.

Попробуйте: уберите _limit=3 и посмотрите, как вырос ответ (можно прокрутить или передать в jq).


POST — отправить JSON (создать запись)

Задача: отправить на сервер новый объект — типичный сценарий «создать заказ / пост / пользователя».

curl -s -X POST \
-H "Content-Type: application/json" \
-d '{"title":"hello","body":"text","userId":1}' \
"https://jsonplaceholder.typicode.com/posts"

Разбор построчно:

ФрагментСмысл
-X POSTЯвно указать метод POST (создание / действие с телом)
-H "Content-Type: application/json"Заголовок: «в теле лежит JSON» — без него сервер может вернуть 400
-d '{...}'data — тело запроса одной строкой
Обратный слэш \В Linux/macOS перенос команды на следующую строку (в CMD — символ ^)
URL в концеАдрес коллекции /posts — создаём элемент в коллекции

Ответ jsonplaceholder не сохраняется в базу, но выглядит как настоящий:

{
"title": "hello",
"body": "text",
"userId": 1,
"id": 101
}

Поле id: 101 — сервер «притворился», что выдал новый id.

Проверить статус 201 Created:

curl -s -o /dev/null -w "HTTP %{http_code}\n" -X POST \
-H "Content-Type: application/json" \
-d '{"title":"hello","body":"text","userId":1}' \
"https://jsonplaceholder.typicode.com/posts"

Разбор флагов:

  • -o /dev/null — тело ответа выбросить (на Windows в Git Bash можно -o NUL).
  • -w "HTTP %{http_code}\n" — после запроса напечатать шаблон; %{http_code} подставит число (201).
  • В лабораторной можно скриншотить строку HTTP 201.

Попробуйте: уберите заголовок Content-Type и посмотрите, изменится ли ответ на другом API (httpbin покажет, что пришло).


Только код ответа (жив ли сервер)

Задача: для отчёта или мониторинга — не читать JSON, а узнать «сервер ответил 200 или нет».

curl -s -o /dev/null -w "%{http_code}\n" "https://httpbin.org/status/200"

Разбор:

  • httpbin /status/200 всегда отвечает кодом 200 — учебный эндпоинт.
  • Ожидание в терминале: одна строка 200.

Проверка «сервис упал»:

curl -s -o /dev/null -w "%{http_code}\n" "https://httpbin.org/status/503"

Должно напечатать 503.

С флагом fail curl завершит команду с ошибкой при 4xx/5xx — удобно в скриптах:

curl -sf "https://httpbin.org/status/200" && echo OK

Разбор:

  • -f — при HTTP ≥ 400 curl вернёт ненулевой exit code.
  • && echo OK — напечатает OK только если curl успешен.

Попробуйте: замените URL на /status/500echo OK не выполнится.


Заголовки ответа и тело

Задача: увидеть Content-Type, cookies, версию HTTP — при отладке «приходит JSON или HTML».

curl -s -i "https://httpbin.org/get?check=1"

Разбор:

  • -iinclude headers: сначала заголовки, пустая строка, потом тело.
  • Первая строка HTTP/2 200 — версия протокола и статус.
  • Дальше content-type: application/json и т.д.
  • Тело JSON — httpbin вернул ваш query check=1 в поле args.

Только заголовки (метод HEAD):

curl -sI "https://example.com"

Разбор:

  • Заглавная I в -I — режим HEAD: тела нет, только заголовки.
  • Быстро проверить, жив ли сайт и какой content-type.

fetch — POST с JSON (браузер или Node)

Задача: то же, что curl POST, но из JavaScript — как на фронтенде React/Vue или в Node.

const res = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: 'hello',
body: 'text',
userId: 1,
}),
});

if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}

const created = await res.json();
console.log('Новый id:', created.id);

Разбор:

СтрокаСмысл
Второй аргумент fetchНастройки запроса
method: 'POST'Метод HTTP
headers: { ... }Заголовки; браузер может добавить свои (User-Agent и др.)
JSON.stringify({...})Объект JS → строка JSON для тела
created.idПоле из ответа сервера

Попробуйте: в DevTools → Network найдите запрос posts, вкладки Headers и Payload — сверьте с кодом.


Примеры запросов — углубление

1. REST — все методы на одном URL

Задача: для отчёта показать разницу GET, PUT, PATCH, DELETE на одном ресурсе /posts/1.

Сначала переменная в bash (чтобы не копировать длинный URL):

BASE="https://jsonplaceholder.typicode.com/posts/1"

GET — прочитать

curl -s "$BASE"

Разбор: $BASE подставляет URL; кавычки сохраняют строку целиком.

PUT — заменить ресурс целиком

curl -s -X PUT \
-H "Content-Type: application/json" \
-d '{"id":1,"title":"new title","body":"new body","userId":1}' \
"$BASE"

Разбор: PUT ожидает полный объект — все поля, которые сервер хранит. Отправили только title — в строгих API остальные поля могут обнулиться.

PATCH — изменить часть

curl -s -X PATCH \
-H "Content-Type: application/json" \
-d '{"title":"only title changed"}' \
"$BASE"

Разбор: PATCH — «заплатка», только изменённые поля. В лабораторных часто путают с PUT — в отчёте напишите, какой метод требует задание.

DELETE — удалить

curl -s -X DELETE "$BASE"

Разбор: тела запроса обычно нет; ответ может быть пустым или {}. У jsonplaceholder удаление тоже «игрушечное».

Попробуйте: после каждого запроса выполните GET и сравните title (для учебного API изменения могут не сохраняться между запросами — это нормально для тренажёра).


2. Авторизация — Bearer и Basic

Bearer-токен (JWT, API key)

Задача: передать секрет в заголовке — стандарт для современных API.

TOKEN="your-token-here"
curl -s -H "Authorization: Bearer $TOKEN" "https://httpbin.org/bearer"

Разбор:

  • TOKEN=... — переменная shell; в отчёт токен не вставляйте.
  • Заголовок Authorization: Bearer <токен> — слово Bearer и пробел обязательны.
  • httpbin /bearer вернёт JSON с полем, куда подставил сервер ваш токен — проверка, что заголовок дошёл.

Попробуйте: подставьте TOKEN=test123 и найдите в ответе "test123".

HTTP Basic Auth

Задача: логин и пароль в одном заголовке (старые API, внутренние панели).

curl -s -u "admin:secret" "https://httpbin.org/basic-auth/admin/secret"

Разбор:

  • -u "login:password" — curl кодирует пару в Base64 и шлёт заголовок Authorization: Basic ….
  • URL httpbin содержит admin и secret — сервис сверяет с -u.
  • Успех — JSON "authenticated": true.

Попробуйте: неверный пароль -u "admin:wrong" — будет 401.


3. Тело из файла, форма и загрузка файла

JSON из файла

Задача: большой JSON не вставлять в командную строку, а хранить в payload.json.

cat > payload.json <<'EOF'
{"title":"from file","body":"text","userId":1}
EOF

curl -s -X POST \
-H "Content-Type: application/json" \
-d @payload.json \
"https://jsonplaceholder.typicode.com/posts"

Разбор:

  • cat > payload.json &lt;&lt;'EOF' — создать файл here-document (Linux/macOS/Git Bash).
  • -d @payload.json — символ @ значит «прочитать тело из файла».
  • Удобно для CI и повторяющихся тестов.

HTML-форма (поля login/password)

curl -s -X POST -d "login=user&password=pass" "https://httpbin.org/post"

Разбор:

  • Формат ключ=значение&ключ2=значение2application/x-www-form-urlencoded (как форма в браузере без файлов).
  • curl сам выставит подходящий Content-Type, если не переопределять.

Загрузка файла (multipart)

curl -s -X POST \
-F "file=@./photo.jpg" \
-F "description=avatar" \
"https://httpbin.org/post"

Разбор:

  • -Fform multipart: можно файл + текстовые поля.
  • file=@./photo.jpg — имя поля file, @ — путь к файлу на диске.
  • В ответе httpbin покажет имена полей и размер файла (файл должен существовать).

Попробуйте: создайте пустой test.txt и подставьте вместо photo.jpg.


4. fetch — таймаут (не ждать вечно)

Задача: если сервер завис, через 5 секунд прервать запрос — важно для UI и лабораторных по асинхронности.

async function fetchJson(url, { timeoutMs = 5000 } = {}) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);

try {
const res = await fetch(url, { signal: controller.signal });
if (!res.ok) {
throw new Error(`HTTP ${res.status} ${res.statusText}`);
}
return await res.json();
} finally {
clearTimeout(timer);
}
}

const post = await fetchJson('https://jsonplaceholder.typicode.com/posts/1');
console.log(post.title);

Разбор:

ЭлементСмысл
AbortControllerОбъект «кнопка отмены» для fetch
controller.signalПередаётся в fetch; при abort() запрос обрывается
setTimeout(..., timeoutMs)Через N мс вызвать abort()
finally + clearTimeoutУбрать таймер, если ответ пришёл раньше — иначе утечка таймеров
&#123; timeoutMs = 5000 &#125; = &#123;&#125;Параметр по умолчанию 5 секунд

При обрыве в консоли будет AbortError.

Попробуйте: timeoutMs: 1 и URL https://httpbin.org/delay/5 — запрос оборвётся раньше ответа.


5. fetch — повтор при сбое сервера (retry)

Задача: при временной ошибке 502/503 повторить запрос 2–3 раза — типичный паттерн для сетевых лабораторных.

async function fetchWithRetry(url, { attempts = 3, baseMs = 300 } = {}) {
let lastError;
for (let i = 0; i < attempts; i++) {
try {
const res = await fetch(url);
if (res.status >= 500) {
throw new Error(`HTTP ${res.status}`);
}
if (!res.ok) {
throw new Error(`HTTP ${res.status} (no retry)`);
}
return await res.json();
} catch (e) {
lastError = e;
if (String(e.message).includes('no retry')) throw e;
if (i === attempts - 1) break;
await new Promise((r) => setTimeout(r, baseMs * 2 ** i));
}
}
throw lastError;
}

Разбор:

  • Цикл for — до attempts попыток.
  • status >= 500 — вина сервера → можно повторить.
  • 400, 401, 404 — помечены (no retry) → повтор бессмысленен, сразу throw.
  • baseMs * 2 &#42;&#42; i — пауза растёт: 300 мс, 600 мс, 1200 мс (экспоненциальная задержка).

Попробуйте: вызовите с https://httpbin.org/status/503 и attempts: 2 — увидите две попытки в Network.


6. Python — библиотека requests

Задача: то же HTTP из Python — частый язык на курсе «программирование» и «веб-разработка».

Установка один раз:

pip install requests
import requests

# --- GET ---
r = requests.get(
"https://jsonplaceholder.typicode.com/posts/1",
timeout=10,
)
r.raise_for_status()
data = r.json()
print("Заголовок:", data["title"])

# --- POST JSON ---
r = requests.post(
"https://jsonplaceholder.typicode.com/posts",
json={"title": "hello", "body": "text", "userId": 1},
timeout=10,
)
r.raise_for_status()
created = r.json()
print("Новый id:", created["id"])

Разбор:

СтрокаСмысл
requests.get(url, timeout=10)GET с лимитом 10 с на весь запрос
r.raise_for_status()Если статус 4xx/5xx — исключение HTTPError
r.json()Разобрать тело как JSON (словарь Python)
json={...} в postБиблиотека сама сериализует dict и ставит Content-Type

Попробуйте: замените posts/1 на posts/99999 без raise_for_status и с ним — сравните поведение.


7. Отладка — когда «не работает»

ЗадачаКомандаЧто смотреть
Весь обмен TLS + заголовкиcurl -v -s "https://httpbin.org/get"Строки > запрос, < ответ
Редиректыcurl -sL "https://httpbin.org/redirect/2"-L follow redirects
Время ответаcurl -s -o /dev/null -w "time %&#123;time_total&#125;s code %&#123;http_code&#125;\n" URLСекунды и код
Лимит ожиданияcurl -s --max-time 10 URLОбрыв через 10 с

Эхо POST — «дошло ли то, что я отправил»:

curl -s -X POST \
-H "Content-Type: application/json" \
-d '{"title":"hello"}' \
"https://httpbin.org/post"

Разбор: в JSON ответа найдите объект "json": &#123; "title": "hello" &#125; — это зеркало вашего тела.

Попробуйте: отправьте невалидный JSON в -d '{broken' — сравните ответ httpbin и реального API.


8. Скрипт health-check для cron или отчёта

Задача: bash-скрипт, который возвращает 0 при «сервер жив» и 1 при ошибке — связка с однострочниками Bash и production-скриптами.

#!/usr/bin/env bash
set -euo pipefail

URL="${1:-https://httpbin.org/status/200}"
TIMEOUT="${2:-5}"

STATUS="$(curl -fsS -o /dev/null -w '%{http_code}' --max-time "$TIMEOUT" "$URL" || echo "000")"

if [[ "$STATUS" == "200" ]]; then
echo "OK $URL"
exit 0
fi

echo "FAIL $URL status=$STATUS" >&2
exit 1

Разбор построчно:

СтрокаСмысл
#!/usr/bin/env bashЗапуск через bash
set -euo pipefailСтрогий режим: ошибка → выход; пустые переменные — ошибка
URL="$&#123;1:-...&#125;"Первый аргумент скрипта или URL по умолчанию
curl -fsS-f fail на 4xx/5xx, -s тихо, -S показать ошибку
|| echo "000"Если curl упал по сети — считать код 000
exit 0 / exit 1Код для cron/CI: 0 успех, 1 провал

Запуск:

chmod +x healthcheck.sh
./healthcheck.sh
./healthcheck.sh https://httpbin.org/status/503

Попробуйте: добавить в отчёт скриншот вывода OK и FAIL.


9. CORS — fetch в браузере заблокирован

Задача: понять, почему в Postman/curl всё OK, а в React — «Failed to fetch».

Браузер для безопасности запрещает JavaScript читать ответ с другого домена, если сервер не отдал заголовки Access-Control-Allow-Origin.

ШагДействие
1Тот же URL в curl — если там 200 и JSON, бэкенд жив
2DevTools → Network — запрос ушёл, статус может быть 200, но консоль красная
3На сервере настроить CORS или в dev — proxy (Vite server.proxy)

Подробнее — отладка веб-приложений.

Разбор: curl и Postman не браузер — CORS на них не действует. Поэтому порядок отладки: сначала curl, потом fetch.


10. curl или fetch — что писать в отчёте

Критерийcurlfetch
Где запускатьТерминал, CI, Raspberry PiСайт, Node.js, React Native
Лабораторная «проверка API»Идеально — одна команда в отчётЕсли тема — JavaScript
Скачать файл-O, -o file.zipЧерез blob() / потоки
Показать преподавателюСкрин терминала с командой и ответомСкрин Console + Network

Частые вопросы (коротко)

Как отправить POST запрос curl с JSON?
-X POST -H "Content-Type: application/json" -d '&#123;"ключ":"значение"&#125;' URL — полный пример в разделе POST выше.

Почему curl ничего не печатает?
Возможно, ответ ушёл в файл (-o), или ошибка только в stderr. Добавьте -v или -w "%&#123;http_code&#125;".

Чем fetch отличается от axios?
fetch встроен в браузер и Node; axios — библиотека с удобствами (перехватчики, автоматический JSON). Подробное сравнение и типовые запросы — Fetch / axios — типовые запросы.

Можно ли без интернета?
Нужен доступ к учебным URL или своему localhost после запуска бэкенда (http://127.0.0.1:8080/...).

Как проверить свой API на localhost?

curl -s "http://127.0.0.1:8080/api/health"

Сервер должен быть уже запущен (npm run dev, uvicorn, dotnet run и т.д.).


Типичные ошибки

СимптомЧастая причинаЧто сделать
curl: command not foundcurl не установленWindows: curl.exe; Linux: sudo apt install curl
HTML вместо JSONНеверный URL или 404-страница-i и -w "%&#123;http_code&#125;"
400 Bad RequestБитый JSON или нет Content-TypeПроверить кавычки в -d, заголовок JSON
401 UnauthorizedНет токена-H "Authorization: Bearer $TOKEN"
403 ForbiddenТокен есть, но нет правДругой пользователь / роль
fetch Failed to fetchСеть, CORS, HTTPS mixedcurl с той же машины; вкладка Network
Команда «зависла»Нет таймаута--max-time 10 или AbortController
В PowerShell «не тот curl»Псевдоним Invoke-WebRequestcurl.exe

Шпаргалка — скопировать в тетрадь

# GET
curl -s "URL"

# GET + только код
curl -s -o /dev/null -w "%{http_code}\n" "URL"

# POST JSON
curl -s -X POST -H "Content-Type: application/json" \
-d '{"key":"value"}' "URL"

# Заголовки + тело
curl -s -i "URL"

# Токен
curl -s -H "Authorization: Bearer TOKEN" "URL"
// GET + проверка
const res = await fetch(url);
if (!res.ok) throw new Error(res.status);
const data = await res.json();

// POST JSON
await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key: 'value' }),
});

Чек-лист перед сдачей лабораторной

  • В отчёте есть команда или код и фрагмент ответа (или код статуса).
  • Для POST указан Content-Type: application/json.
  • URL в кавычках; для localhost указан порт.
  • Токены и пароли не вставлены в Git — только «из переменной окружения».
  • При ошибке fetch приложен скрин curl с тем же URL.
  • Таймаут указан (--max-time или timeout в requests).

Связанные материалы

ТемаСсылка
Теория curl, TLS, cookiesУтилита curl
Postman, коллекцииPostman и curl
HTTP и RESTИнтеграционное взаимодействие
JavaScriptПримеры JavaScript
Bash-скриптыОднострочники и скрипты, production
Практикум RESTREST и WebSocket
Примеры Turtle (формат галереи)Turtle на Python

См. также

Другие статьи этого же раздела в боковом меню (как на странице "О разделе").

Освоение главы0%