<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sanjay Ponraj</title>
    <description>The latest articles on DEV Community by Sanjay Ponraj (@sanjayponraj).</description>
    <link>https://dev.to/sanjayponraj</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4015071%2F6233e897-2dbd-4a53-8c6c-65e8a51e9ce7.png</url>
      <title>DEV Community: Sanjay Ponraj</title>
      <link>https://dev.to/sanjayponraj</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sanjayponraj"/>
    <language>en</language>
    <item>
      <title>HTTP finally has a proper way to send a query: meet the QUERY method</title>
      <dc:creator>Sanjay Ponraj</dc:creator>
      <pubDate>Sat, 04 Jul 2026 13:06:00 +0000</pubDate>
      <link>https://dev.to/sanjayponraj/http-finally-has-a-proper-way-to-send-a-query-meet-the-query-method-2jfa</link>
      <guid>https://dev.to/sanjayponraj/http-finally-has-a-proper-way-to-send-a-query-meet-the-query-method-2jfa</guid>
      <description>&lt;p&gt;For decades we've had exactly two bad options for "read something using a complex input": cram everything into a GET URL, or lie and use POST. In June 2026, the IETF standardized a real fix:QUERY method (RFC 10008). Here's what it is, why it exists, and how to actually use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: GET and POST both fall&amp;nbsp;short
&lt;/h2&gt;

&lt;p&gt;Say you have a search endpoint. The natural, RESTful choice is GET:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nf"&gt;GET&lt;/span&gt; &lt;span class="nn"&gt;/contacts?select=surname,email&amp;amp;match=email=*@example.*&amp;amp;limit=10&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example.org&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GET is safe, idempotent, and cacheable. Perfect until your query grows. Then you hit real walls the RFC calls out explicitly.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;URL length limits you can't predict, because the request passes through proxies, gateways, and CDNs you don't control.&lt;/li&gt;
&lt;li&gt;URLs get logged and bookmarked. Putting something sensitive in the URL means it lands in access logs, browser history, and referrer headers.&lt;/li&gt;
&lt;li&gt;Encoding overhead: nesting a rich filter object into a query string is ugly and inefficient.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So everyone reaches for the workaround: POST.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nf"&gt;POST&lt;/span&gt; &lt;span class="nn"&gt;/contacts&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example.org&lt;/span&gt;
&lt;span class="na"&gt;Content-Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/json&lt;/span&gt;
&lt;span class="s"&gt;{ "match": { "email": "*@example.*" }, "select": ["surname", "email"], "limit": 10 }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This solves the size problem, but throws away the semantics. Nothing in the protocol says this POST is safe or idempotent. Caches won't cache it. Intermediaries won't retry it after a dropped connection. Every GraphQL API that sends read queries over POST lives with exactly this compromise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix:&amp;nbsp;QUERY
&lt;/h2&gt;

&lt;p&gt;QUERY is the missing middle. It takes the request body from POST and the safe + idempotent + cacheable semantics from GET:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nf"&gt;QUERY&lt;/span&gt; &lt;span class="nn"&gt;/contacts&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example.org&lt;/span&gt;
&lt;span class="na"&gt;Content-Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/json&lt;/span&gt;
&lt;span class="na"&gt;Accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/json&lt;/span&gt;
&lt;span class="s"&gt;{ "match": { "email": "*@example.*" }, "select": ["surname", "email"], "limit": 10 }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The input goes in the body (any media type you like - JSON, SQL, JSONPath, XSLT, form-encoded), but because the method is defined as safe and idempotent, caches and intermediaries can treat it like GET: cache the response, and safely retry after a connection failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  A complete request/response
&lt;/h2&gt;

&lt;p&gt;The response to a successful QUERY is just the result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nf"&gt;QUERY&lt;/span&gt; &lt;span class="nn"&gt;/contacts&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example.org&lt;/span&gt;
&lt;span class="na"&gt;Content-Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/x-www-form-urlencoded&lt;/span&gt;
&lt;span class="na"&gt;Accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/json&lt;/span&gt;
&lt;span class="s"&gt;select=surname,givenname,email&amp;amp;limit=10&amp;amp;match=%22email=*@example.*%22&lt;/span&gt;
&lt;span class="s"&gt;HTTP/1.1 200 OK&lt;/span&gt;
&lt;span class="na"&gt;Content-Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/json&lt;/span&gt;
&lt;span class="s"&gt;[&lt;/span&gt;
&lt;span class="s"&gt;{ "surname": "Smith", "givenname": "John", "email": "smith@example.org" },&lt;/span&gt;
&lt;span class="s"&gt;{ "surname": "Jones", "givenname": "Sally", "email": "sally.jones@example.com" },&lt;/span&gt;
&lt;span class="s"&gt;{ "surname": "Dubois", "givenname": "Camille", "email": "camille.dubois@example.net" }&lt;/span&gt;
&lt;span class="s"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The clever part: turning a query into a GET-able&amp;nbsp;URL
&lt;/h2&gt;

&lt;p&gt;Caching a QUERY is trickier than caching a GET, because the cache key has to include the request body, not just the URL. RFC 10008 offers an elegant escape hatch: the server can hand back a URL you can GET afterward, via two different response headers.&lt;br&gt;
&lt;code&gt;Content-Location&lt;/code&gt; → a URL for this result (a snapshot):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt; &lt;span class="ne"&gt;OK&lt;/span&gt;
&lt;span class="na"&gt;Content-Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/json&lt;/span&gt;
&lt;span class="na"&gt;Content-Location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/contacts/stored-results/17&lt;/span&gt;
&lt;span class="na"&gt;Location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/contacts/stored-queries/42&lt;/span&gt;
&lt;span class="s"&gt;GET /contacts/stored-results/17 HTTP/1.1 # fetch the same result again&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Location&lt;/code&gt; → a URL for the query itself (re-run it later, without resending the body):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /contacts/stored-queries/42 HTTP/1.1 # re-runs the query, fresh results
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the best of both worlds: send the big body once via QUERY, then switch to plain, cache-friendly GETs-even conditional ones with ETag/If-None-Match for cheap &lt;code&gt;304 Not Modified&lt;/code&gt; responses.&lt;br&gt;
Error handling worth&amp;nbsp;knowing&lt;br&gt;
The RFC is specific about status codes, which makes for robust APIs:&lt;br&gt;
Missing Content-Type → &lt;code&gt;400&lt;/code&gt;. Servers must not content-sniff.&lt;br&gt;
Query format not supported → &lt;code&gt;415&lt;/code&gt; (pair it with &lt;code&gt;Accept-Query&lt;/code&gt;).&lt;br&gt;
Body valid, but query unprocessable (e.g., syntactically correct SQL referencing a missing table) → &lt;code&gt;422 Unprocessable Content&lt;/code&gt;.&lt;br&gt;
Requested response type via Accept not available → &lt;code&gt;406 Not Acceptable&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things to&amp;nbsp;watch
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;CORS preflight required. QUERY isn't a CORS-safelisted method, so browsers will send an OPTIONS preflight. (Browser &lt;code&gt;fetch()&lt;/code&gt; support is still rolling out-check before relying on it client-side.)&lt;/li&gt;
&lt;li&gt;Caching needs the body. A cache must read the request content to compute the key. The RFC allows normalization (e.g., +json structural normalization) to improve hit rates-but sloppy normalization can cause false-positive cache hits.&lt;/li&gt;
&lt;li&gt;Sensitive data in generated URLs. If the server mints a &lt;code&gt;Location&lt;/code&gt;/&lt;code&gt;Content-Location&lt;/code&gt; URL for a query that contained secrets, that URL shouldn't encode the sensitive parts (they'd end up in logs-the very thing QUERY helps you avoid).&lt;/li&gt;
&lt;li&gt;Redirects differ from POST. Unlike POST, a &lt;code&gt;301/302&lt;/code&gt; on a QUERY does not get rewritten to a GET; the agent re-sends a QUERY to the new target. (303 See Other does mean "GET the result over there.")&lt;/li&gt;
&lt;li&gt;Adoption curve. It's a Proposed Standard authored with Cloudflare and Akamai, so CDN/edge support may land before your web framework and HTTP client do. Expect a transition period.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  When to use&amp;nbsp;it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Reach for QUERY when a read operation has a body that's too big, too structured, or too sensitive for a URL:&lt;/li&gt;
&lt;li&gt;Search/filter endpoints with rich filter objects.&lt;/li&gt;
&lt;li&gt;GraphQL read queries (finally, safe semantics instead of POST).&lt;/li&gt;
&lt;li&gt;Reporting/analytics queries (SQL, JSONPath, XSLT over a dataset).&lt;/li&gt;
&lt;li&gt;Any "GET with a body" you've been faking with POST.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>http</category>
      <category>query</category>
      <category>graphql</category>
      <category>restapi</category>
    </item>
  </channel>
</rss>
