Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Чи можна відправляти body в GET-запиті?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**GET request body** - технічно надіслати можна, але RFC 7231 не визначає його семантику. Більшість серверів (Nginx, Express body-parser, AWS ALB) відкидають тіло ще до того, як воно досягне твого коду. Для фільтрів використовуй query params, для складних даних - POST.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**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 спеціально реалізувала таку підтримку. Більшість інших серверів цього не зробила.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.