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

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

Дочитали статтю?