Suggest an editImprove this articleRefine the answer for “Can you send a body in a GET request?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**GET request body** - you can send one technically, but RFC 7231 does not define its meaning. Most servers (Nginx, Express body-parser, AWS ALB) discard it before your app code runs. Use query params for simple filters and POST for complex data.Shown above the full answer for quick recall.Answer (EN)Image**GET request body** - you can attach one in code and it will travel over TCP, but RFC 7231 section 4.3.1 does not define what it means, so most servers discard it before your application ever sees it. ## Theory ### TL;DR - Technically, the body reaches the server at the TCP level. What happens next depends entirely on the server. - RFC 7231 says the payload in a GET request "has no defined semantics" - servers can ignore it, forward it, or reject it with 411. - Nginx drops GET bodies before they hit your app. Express body-parser skips them by default. - Production proxy layers like AWS ALB and Varnish strip the body without warning. - For filters, use query params. For large data, use POST. ### Quick Example ```javascript // Client sends a body - server never sees it fetch('/api/users', { method: 'GET', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ filter: 'active' }) }) .then(res => res.json()) .then(data => console.log(data)); // Server (Express): app.get('/api/users', (req, res) => { console.log(req.body); // {} - body-parser skipped GET res.json({ users: allUsers }); // No filtering applied }); ``` The body traveled in the TCP packet but was dropped before the route handler ran. No error was thrown. That silence is what makes GET bodies so easy to ship without noticing the bug. ### What RFC 7231 Actually Says RFC 7231 section 4.3.1 states: "A payload within a GET request message has no defined semantics." That one sentence is the source of all the confusion. The spec does not say the body is forbidden - it says the server has no obligation to do anything with it. So different servers take different paths: some ignore it, some forward it, some return 411. The spec also requires GET to be safe and idempotent. A safe method must not change server state. The moment you pass filter logic in a body and depend on the server parsing it, you are relying on application behavior the spec never guaranteed. ### How Servers Handle It Nginx (since v0.7) drops GET bodies in `ngx_http_core_module` before the request reaches your application. Apache mod_proxy may forward it but logs a warning. Express body-parser checks `req.method` and skips parsing for GET and HEAD entirely - so `req.body` is always `{}` on a GET route unless you explicitly configure `{ type: '*/*' }`. AWS ALB drops GET bodies before they reach Lambda. CDNs like Varnish and CloudFront build cache keys from the URL only, not the body. So if two clients send the same URL with different bodies, they get the same cached response. That is not just a performance issue - it is a data correctness bug. ### Decision Rules - Pure data retrieval with simple filters: query params (`/users?active=true&role=admin`) - Complex filter objects or data over roughly 2KB: POST to a search endpoint (`POST /users/search`) - Caching needed: query params only - RFC 7234 does not include the request body in cache keys - GraphQL: POST always - GET with body variables is unreliable and was already deprecated in Apollo before v3.0 Elasticsearch is a deliberate exception. Their `_search` endpoint accepts a JSON body on GET requests to support the full Query DSL. The ES docs acknowledge this explicitly and recommend POST as the alternative. That is a conscious decision by one product team, not a general pattern to follow. ### Common Mistakes **Mistake 1: Trusting req.body in an Express GET handler** ```javascript // Wrong app.use(express.json()); app.get('/users', (req, res) => { const { filter } = req.body; // Always undefined res.json(db.query(filter)); }); // Fix: switch to POST, or configure middleware explicitly app.use(express.json({ type: '*/*' })); // But adding this just to support GET bodies is a bad trade ``` body-parser checks `req.method` and bails out on GET and HEAD. The body never gets parsed. **Mistake 2: Sending large filter data via GET body** ```javascript // Wrong - Safari may 400, browsers cap body size on GET fetch('/graphql', { method: 'GET', body: JSON.stringify(largeVariables) // 413 Payload Too Large on some clients }); // Fix: POST per GraphQL spec fetch('/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query, variables: largeVariables }) }); ``` Safari has been known to reject GET requests with a body above 2KB. The GraphQL spec recommends POST for all non-trivial queries. **Mistake 3: Expecting CDN caching to work with GET bodies** ```javascript // Wrong assumption - both requests share the same cache entry // Request A: GET /api/data, body: { "tenant": "acme" } // Request B: GET /api/data, body: { "tenant": "contoso" } // Contoso gets Acme data from cache // Fix: put the filter in the URL // GET /api/data?tenant=acme // GET /api/data?tenant=contoso ``` RFC 7234 builds cache keys from the URL and selected headers. Body content is not part of that key. **Mistake 4: Works locally, breaks in production** Local Flask or plain Node HTTP servers often parse whatever they receive. The issue appears after deploying behind Nginx or a cloud load balancer that strips the body. The dev/prod gap here is painful to debug - the code looks correct locally, the network request completes with 200, and the filtering just silently stops working. ### Real-World Usage - Elasticsearch `_search`: GET with JSON body for Query DSL. ES docs explicitly note POST is preferred. - Apollo GraphQL (pre-3.0): allowed GET body for query variables. Removed in favor of POST. - GitHub Enterprise: GET `/search/code` accepts body in some versions (undocumented). - Splunk 9.x: GET `/search/jobs/export` uses body for SPL queries. All of these are legacy decisions or one-off choices by specific teams. None are patterns to copy in new code. ### Follow-Up Questions **Q:** Why does my GET with `curl -d` work locally but fail in production? **A:** Local dev servers (Flask, plain Node HTTP) parse whatever arrives. Production Nginx or CloudFront strips GET bodies before they reach the app. The TCP packet arrives, but the server discards the body. **Q:** Is a GET body ever cached by CDNs? **A:** No. RFC 7234 defines cache keys as URL plus selected headers. The body is not part of the key, so a CDN returns the same cached response regardless of body content. **Q:** How do Axios and fetch differ when sending a GET body? **A:** Both send the body at the TCP level. Axios prints a console warning in some versions. Fetch is silent. Either way, what matters is server-side behavior - and most servers discard it. **Q:** I need to send 10KB of filter data with a GET-like operation. What do I use? **A:** POST to a search endpoint. Name it `/search` or `/query` and document it as a read operation. Add `ETag` and `Cache-Control` headers if the back end needs to support caching on POST - some reverse proxies allow that with configuration. **Q:** Does HTTP/2 change anything here? **A:** No. HTTP/2 changes framing and multiplexing, not method semantics. GET body behavior is still undefined at the application layer. ## Examples ### Basic: Body Sent, Body Ignored ```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 contains ALL products, not just electronics // The body was transmitted but never read // server.js (Express) app.use(express.json()); app.get('/api/products', (req, res) => { console.log(req.body); // {} res.json(db.getAll()); // Returns everything }); ``` The body left the client, traveled over the network, and was dropped by Express body-parser. No filtering happened. No error was thrown. ### Intermediate: Query Params vs POST for Complex Filters ```javascript // Wrong: filter buried in GET body // GET /api/orders + body: { "status": "pending", "userId": 42 } // req.body = {} on the server. Order status: chaos. // Right: simple filters go in query params // GET /api/orders?status=pending&userId=42 app.get('/api/orders', (req, res) => { const { status, userId } = req.query; // Works reliably everywhere const orders = db.orders.filter( o => o.status === status && o.userId === Number(userId) ); res.json(orders); }); // Right: complex filters go to a POST search endpoint // POST /api/orders/search app.post('/api/orders/search', express.json(), (req, res) => { const { filters, sort, pagination } = req.body; // Parsed correctly res.json(db.orders.search(filters, sort, pagination)); }); ``` Query params for simple cases. A dedicated POST endpoint for everything else. Both patterns are predictable across every proxy, CDN, and framework stack. ### Advanced: Elasticsearch GET Body (the Legitimate Exception) ```bash # Elasticsearch accepts GET body for its 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 // The official ES JS client sends POST internally const result = await client.search({ index: 'products', body: { query: { bool: { must: [{ match: { category: 'electronics' } }], filter: [{ range: { price: { lte: 500 } } }] } } } }); ``` ES added GET body support because the Query DSL is too complex for URL encoding. The official JavaScript client sends the actual HTTP request as POST under the hood anyway. If you hit the REST API directly with GET and a body, it works because the ES team specifically built that support. Most servers did not.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.