cURLParse cURL Converter

GET with a Body: The Request Shape Nobody Tells You About

cURLParse Guides · Updated 2026-10-14 · All guides

RFC 9110 is blunt: GET payloads have "no defined semantics" and servers may reject them. Yet curl -G --data-urlencode "q=cat" sends query params on a GET, Elasticsearch's _search accepts a real JSON body on GET, and a surprising number of internal APIs ship this way. Knowing why it's contentious tells you when you can safely use it.

The four shapes, as the wire actually sees them

Everything below was captured against a local origin that prints its request line and body, using curl 8.5.0. Same server, same payload, four commands:

commandorigin received
curl -d 'a=1' http://origin/POST / body=a=1
curl -X GET -d 'a=1' http://origin/GET / body=a=1
curl -G -d 'b=2' 'http://origin/?a=1'GET /?a=1&b=2 body=(none)
curl -G --data-urlencode 'q=a&b=2' 'http://origin/'GET /?q=a%26b%3d2 body=(none)

Read the first two rows together, because they overturn the folklore. Bare -d implies POST — true. But -X GET overrides that inference and you get a GET carrying the body, with no warning, no note on stderr, and no non-zero exit. curl -X GET -d '{"query":"x"}' is not a lint error waiting to happen; it is a GET request with a JSON payload that will work in Postman, work on your laptop, and die at the first CDN in production.

The -G rows show what the flag is actually for: it relocates the data into the URL and takes it out of the body. Notice it appends to an existing query string with & rather than replacing it, and notice --data-urlencode encoding the & inside your value as %26 — without that, q=a&b=2 would have been two parameters and your search would have silently become q=a.

Why servers tolerate or reject it

A GET body must survive three proxies, a CDN cache, and two load balancers between client and app. Some of those will strip the body (cache-friendly behavior), and several CDNs hard-400 it as a request-smuggling-defense measure. Your dev server accepting the pattern is not evidence production will.

Query string vs body: the actual tradeoff

Query strings get logged in plain text in every intermediary access log, capped at 8–12KB by common defaults, and double as cache keys. A body for a read avoids both the length limit and the log leak. That's the real argument for POST-as-search, which is why Elasticsearch and GraphQL adopted POST for complex reads — and why curl's -G flag exists: it puts the data in the URL while keeping the method GET, the honest version of the pattern.

One sharp edge on -G, verified rather than remembered:

curl -G -d 'q=cat dog' http://origin/search
# curl: (3) URL rejected: Malformed input to a URL function   (exit 3)

-d does not percent-encode. -G then has to splice a literal space into a URL, and curl refuses instead of sending something illegal — so a bare unquoted space in a -G payload is a hard failure, while the same value through --data-urlencode ships as q=cat+dog. If you are assembling URLs by hand, --data-urlencode is the flag; -d is for bodies.

The other failure mode is quieter and worse. The two-flag combination -X GET -d produces a request that is legal at the transport layer and meaningless at the spec layer, so nothing between you and the origin can tell you it was a mistake. If you want a parameterized read, use -G. If you want a body, use POST. Reaching for both flags at once means someone wanted one and typed the other — and Quoting Hell shows how a stray character in that same body turns into a shell command.

The inverse shape — real query params and a real body — is unambiguous, always safe, and what the POST with query params example shows. For the full flag-by-flag map, including what -d silently does to newlines in your payload, see The cURL Flags That Actually Matter.

FAQ

Can I send a JSON body with GET in production? Not over the public internet. Behind your own load balancer with a server that documents support (Elasticsearch does), it works; on the open web, pick POST or use query params via -G.

Why does -d turn my GET into a POST? Bare -d implies POST by design. Add -G and curl moves the data into the query string instead, keeping the method GET. And if you write -X GET -d, curl obeys -X and sends the body on a GET — silently.

Does curl warn about a GET with a body? No. Nothing on stderr, exit code unaffected. This is a spec-level ambiguity, not a malformed request, so curl has no basis to refuse it. Expect the failure to surface several hops away from your machine.

Why is my search endpoint logged with my query in plaintext? Because query strings land in access logs at every hop. Anything sensitive in a search belongs in a POST body — that's the same reason passwords go in POST bodies.

Why did -G drop my parameters when the URL already had a query string? It shouldn't — -G appends with &, and a URL ending in ? or an already-present = can confuse hand-built URLs. Check the request line with curl -v; the answer is always in the first > line.

Developer Sponsor / Partner
Copied to clipboard!