Skip to main content

Чи можна відправляти 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.

Швидкий приклад

javascript
// Клієнт надсилає тіло - сервер його не бачить 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

javascript
// Неправильно 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

javascript
// Неправильно - 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 з тілом

javascript
// Хибне припущення - обидва запити використовують один запис у кеші // Запит 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=contoso

RFC 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 на рівні додатку залишається невизначеною.

Приклади

Базовий: тіло надіслано, тіло проігноровано

javascript
// 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 для складних фільтрів

javascript
// Неправильно: фільтр у тілі 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 (легітимний виняток)

bash
# 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" } }] }'
javascript
// Офіційний 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 спеціально реалізувала таку підтримку. Більшість інших серверів цього не зробила.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.