Чи можна відправляти body в GET-запиті?
GET request body - технічно надіслати можна, і він пройде через TCP, але RFC 7231 розділ 4.3.1 не визначає його семантику, тому більшість серверів відкидають його ще до того, як запит досягне твого коду.
Теорія
TL;DR
- Тіло GET-запиту технічно надходить на рівні TCP. Що з ним відбувається далі - залежить від сервера.
- RFC 7231 каже: payload у GET «не має визначеної семантики». Сервер може ігнорувати, переслати або повернути 411.
- Nginx відкидає GET bodies до того, як запит досягає додатку. Express body-parser пропускає їх за замовчуванням.
- Проксі-шари в продакшені (AWS ALB, Varnish) видаляють тіло без повідомлення.
- Для фільтрів - query params. Для складних даних - POST.
Швидкий приклад
// Клієнт надсилає тіло - сервер його не бачить
fetch('/api/users', {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filter: 'active' })
})
.then(res => res.json())
.then(data => console.log(data));
// Сервер (Express):
app.get('/api/users', (req, res) => {
console.log(req.body); // {} - body-parser пропустив GET
res.json({ users: allUsers }); // Фільтрація не відбулась
});Тіло пройшло в TCP-пакеті, але було відкинуте до того, як запустився обробник маршруту. Жодної помилки. Саме ця тиша і робить GET bodies такими непомітними в продакшені.
Що насправді говорить RFC 7231
Розділ 4.3.1 RFC 7231 формулює так: "A payload within a GET request message has no defined semantics." Одне речення - і вся проблема. Специфікація не забороняє тіло, вона каже, що сервер не зобов'язаний з ним щось робити. Тому сервери поводяться по-різному: одні ігнорують, інші пересилають далі, треті повертають 411.
Одночасно специфікація вимагає, щоб GET був безпечним (safe) і ідемпотентним (idempotent). Безпечний метод не повинен змінювати стан сервера. Як тільки ти передаєш логіку фільтрації в тілі - ти покладаєшся на поведінку, яку специфікація ніколи не гарантувала.
Як різні сервери обробляють тіло
Nginx з версії 0.7 відкидає GET bodies у модулі ngx_http_core_module ще до того, як запит потрапляє в додаток. Apache mod_proxy може переслати тіло далі, але записує попередження в лог. Express body-parser перевіряє req.method і пропускає GET та HEAD - тому req.body на GET-маршруті завжди {}, якщо не перевизначити параметр { type: '*/*' }.
AWS ALB видаляє GET bodies до того, як вони досягають Lambda. CDN-сервіси на кшталт Varnish і CloudFront будують ключ кешу тільки з URL, ігноруючи тіло. Якщо два різних клієнти надсилають однаковий URL з різними тілами - обидва отримають одну й ту ж кешовану відповідь. Це не проблема продуктивності, це баг у коректності даних.
Правила вибору
- Просте отримання даних з фільтрами: query params (
/users?active=true&role=admin) - Складні об'єкти фільтрації або дані більше приблизно 2KB: POST на ендпоінт пошуку (
POST /users/search) - Потрібне кешування: тільки query params - RFC 7234 не включає тіло запиту в ключ кешу
- GraphQL: завжди POST - GET з тілом для змінних ненадійний і вже прибраний в Apollo до версії 3.0
Elasticsearch - це свідомий виняток. Їхній ендпоінт _search приймає JSON-тіло при GET-запитах для підтримки повного Query DSL. В документації ES це прямо зазначено, і POST пропонується як альтернатива. Це рішення конкретної команди, а не загальний патерн.
Типові помилки
Помилка 1: Очікувати заповнений req.body в GET-обробнику Express
// Неправильно
app.use(express.json());
app.get('/users', (req, res) => {
const { filter } = req.body; // Завжди undefined
res.json(db.query(filter));
});
// Варіант виправлення: перейти на POST, або явно налаштувати middleware
app.use(express.json({ type: '*/*' }));
// Але додавати це лише заради GET bodies - погана угодаbody-parser перевіряє req.method і виходить при GET і HEAD. Тіло ніколи не парситься.
Помилка 2: Передавати великий обсяг даних у тілі GET
// Неправильно - Safari може повернути 400, браузери обмежують розмір
fetch('/graphql', {
method: 'GET',
body: JSON.stringify(largeVariables) // 413 Payload Too Large у деяких клієнтах
});
// Правильно: POST відповідно до специфікації GraphQL
fetch('/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, variables: largeVariables })
});Safari відомий тим, що відхиляє GET-запити з тілом більше 2KB. Специфікація GraphQL рекомендує POST для всіх нетривіальних запитів.
Помилка 3: Розраховувати на кешування CDN при GET з тілом
// Хибне припущення - обидва запити використовують один запис у кеші
// Запит A: GET /api/data + body: { "tenant": "acme" }
// Запит B: GET /api/data + body: { "tenant": "contoso" }
// Contoso отримає дані Acme з кешу
// Правильно: фільтр в URL
// GET /api/data?tenant=acme
// GET /api/data?tenant=contosoRFC 7234 будує ключ кешу з URL і окремих заголовків. Тіло запиту до ключа не входить.
Помилка 4: Працює локально, ламається в продакшені
Локальні Flask або plain Node HTTP сервери зазвичай парсять все, що отримують. Проблема з'являється після деплою за Nginx або cloud load balancer, який видаляє тіло. Розрив між dev і prod тут особливо болючий: код виглядає правильно локально, запит завершується з 200, і фільтрація просто перестає працювати без жодного повідомлення про помилку.
Де це зустрічається в реальному коді
- Elasticsearch
_search: GET з JSON-тілом для Query DSL. Документація прямо рекомендує POST як альтернативу. - Apollo GraphQL (до версії 3.0): підтримував GET body для змінних запиту. Прибрано на користь POST.
- GitHub Enterprise: GET
/search/codeприймає тіло в деяких версіях (не задокументовано). - Splunk 9.x: GET
/search/jobs/exportвикористовує тіло для SPL-запитів.
Всі ці випадки - спадкові рішення або одноразовий вибір конкретних команд. Жоден з них не є патерном для нового коду.
Питання на співбесіді
Q: Чому мій GET з curl -d працює локально, але падає в продакшені?
A: Локальні dev-сервери парсять все, що отримують. Продакшн Nginx або CloudFront видаляє GET bodies до того, як вони досягають додатку. TCP-пакет приходить, але сервер відкидає тіло.
Q: Чи кешують CDN тіло GET-запиту?
A: Ні. RFC 7234 визначає ключ кешу як URL плюс окремі заголовки. Тіло не входить в ключ, тому CDN поверне кешовану відповідь незалежно від вмісту тіла.
Q: Як Axios і fetch поводяться при відправці GET body?
A: Обидва надсилають тіло на рівні TCP. Axios виводить попередження в консоль у деяких версіях. Fetch мовчить. В обох випадках ключова - поведінка сервера, а більшість серверів тіло відкидає.
Q: Мені потрібно передати 10KB даних фільтрації при GET-подібній операції. Що використовувати?
A: POST на ендпоінт пошуку. Назви його /search або /query і задокументуй як операцію читання. Додай заголовки ETag і Cache-Control, якщо бекенд має підтримувати кешування - деякі reverse proxies дозволяють це налаштувати і для POST.
Q: HTTP/2 щось змінює в цьому питанні?
A: Ні. HTTP/2 змінює фреймінг і мультиплексування, але не семантику методів. Поведінка GET body на рівні додатку залишається невизначеною.
Приклади
Базовий: тіло надіслано, тіло проігноровано
// client.js
const response = await fetch('/api/products', {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ category: 'electronics' })
});
const data = await response.json();
// data містить ВСІ товари, а не тільки electronics
// Тіло було передане, але ніколи не прочитане
// server.js (Express)
app.use(express.json());
app.get('/api/products', (req, res) => {
console.log(req.body); // {}
res.json(db.getAll()); // Повертає все без фільтрації
});Тіло покинуло клієнт, пройшло мережу і було відкинуто Express body-parser. Фільтрація не відбулась. Помилки не було.
Середній рівень: query params проти POST для складних фільтрів
// Неправильно: фільтр у тілі GET
// GET /api/orders + body: { "status": "pending", "userId": 42 }
// req.body = {} на сервері
// Правильно: прості фільтри в query string
// GET /api/orders?status=pending&userId=42
app.get('/api/orders', (req, res) => {
const { status, userId } = req.query; // Працює надійно скрізь
const orders = db.orders.filter(
o => o.status === status && o.userId === Number(userId)
);
res.json(orders);
});
// Правильно: складні фільтри через POST
// POST /api/orders/search
app.post('/api/orders/search', express.json(), (req, res) => {
const { filters, sort, pagination } = req.body; // Парситься коректно
res.json(db.orders.search(filters, sort, pagination));
});Query params для простих випадків. Окремий POST-ендпоінт для всього складного. Обидва підходи передбачувані на будь-якому проксі, CDN і фреймворку.
Просунутий рівень: Elasticsearch GET body (легітимний виняток)
# Elasticsearch приймає тіло в GET для свого Query DSL
curl -X GET "localhost:9200/products/_search" \
-H 'Content-Type: application/json' \
-d '{
"query": {
"bool": {
"must": [{ "match": { "category": "electronics" } }],
"filter": [{ "range": { "price": { "lte": 500 } } }]
}
},
"sort": [{ "price": { "order": "asc" } }]
}'// Офіційний ES JS client надсилає POST під капотом
const result = await client.search({
index: 'products',
body: {
query: {
bool: {
must: [{ match: { category: 'electronics' } }],
filter: [{ range: { price: { lte: 500 } } }]
}
}
}
});ES додав підтримку GET body, бо Query DSL занадто складний для URL-кодування. Офіційний JS-клієнт все одно надсилає запит як POST під капотом. Якщо звертатись до REST API напряму через GET з тілом - це працює, бо команда ES спеціально реалізувала таку підтримку. Більшість інших серверів цього не зробила.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.