Every filtering API I have ever worked on eventually hits the same wall. Someone needs to filter by a date range, a status array, a nested customer field, and a product ID list all at once, and the options on the table are a query string that is about to blow past the server's line-length limit, or a POST request pretending to be a read. Neither is right. That gap is exactly why the HTTP QUERY method exists, and I wanted to see what it actually looks like in a real Laravel app rather than just reading the RFC.
The problem GET and POST cannot solve together
GET is safe and idempotent by definition. Proxies cache it, browsers can retry it, and search engines can crawl it without side effects. The catch is that everything has to fit in the URL, and URLs have hard limits: Nginx defaults to a 4096 byte request line, Apache to 8190 bytes, and older browsers cap out even lower. A filter with a status array, a date range, and a list of product IDs can pass that limit before you have added pagination.
POST solves the size problem because the payload lives in the body, not the URL. But POST is not safe and not idempotent. A load balancer will not cache it, a client cannot safely auto-retry it on a timeout, and semantically it means "create or change something", not "give me data matching this filter". Using POST to fetch data is a workaround, not a solution, and every API that does it (GraphQL included) inherits POST's caching and retry problems as a permanent tax.
QUERY, standardised in June 2026 as RFC 10008, is the method built to close that gap: a request body like POST, but safe and idempotent like GET.
What actually changes with QUERY
The specification is deliberately narrow. QUERY carries a request body of any content type the server declares support for (JSON, XML, form-encoded, or something custom), and it is defined as safe and idempotent in the same sense as GET. That single classification is what unlocks the rest:
- Proxies and CDNs are permitted to cache a
QUERYresponse, the same as GET. - Clients can retry a failed
QUERYrequest on a network error without risking duplicate side effects. - Filter values move out of the URL entirely, so they stop leaking into access logs, browser history, and referrer headers.
- The body has no practical length limit, bounded only by server configuration rather than a hard protocol ceiling.
Nothing about routing or resource semantics changes. QUERY is not a replacement for GET on simple lookups, it is the missing option for the case where a filter genuinely will not fit in a query string.
Laravel 13's native support
Laravel 13.19 added Http::query() as a first-class method on the HTTP client, built on top of Symfony HttpFoundation 7.4, which parses QUERY request bodies natively. Before this, sending a QUERY request meant dropping to the low-level Http::send() call and manually encoding the body:
// Laravel 12 and earlier, manual verb and manual body encoding
$response = Http::send('QUERY', $url, [
'body' => json_encode($filter),
'headers' => ['Content-Type' => 'application/json'],
]);
With native support, it collapses to one call, and it sends JSON by default, matching the behaviour of post(), put(), and patch():
use Illuminate\Support\Facades\Http;
$response = Http::query('https://api.example.com/orders', [
'status' => ['pending', 'processing', 'shipped'],
'date_from' => '2026-01-01',
'date_to' => '2026-06-30',
'customer' => ['country' => 'US', 'tier' => 'premium'],
'product_ids' => [101, 202, 303, 404, 505],
]);
No asJson() wrapper needed to get JSON encoding, and no asForm() unless the receiving server specifically expects form-encoded data. I had both wrappers in an earlier version of this code before realising they were redundant against the new default, which was a useful reminder to check a framework's defaults before reaching for the verbose form out of habit.
The routing gap nobody documents
The part that took longest to figure out was accepting incoming QUERY requests. Laravel's Route::match() helper looks like the obvious tool, but it only accepts the standard verb list it has always supported, GET, POST, PUT, PATCH, DELETE, OPTIONS. There is no Route::query() helper either, and passing 'QUERY' into match() silently does nothing useful. The actual answer is to register the verb directly on the underlying router:
use Illuminate\Support\Facades\Route;
// Route::match() does not cover QUERY, register it directly
Route::match(['GET', 'POST'], '/orders', [OrderController::class, 'index']);
app('router')->addRoute(['QUERY'], '/orders', [OrderController::class, 'index']);
Once the route is registered, reading the body needs no special handling. Symfony HttpFoundation 7.4 parses a QUERY body the same way it parses a POST body, so request()->all() and request()->input() work exactly as expected:
public function index(): JsonResponse
{
$method = request()->method();
$filter = request()->all();
$statusFilter = $filter['status'] ?? null;
return response()->json([
'verb_received' => $method,
'safe' => in_array($method, ['GET', 'QUERY']),
'idempotent' => in_array($method, ['GET', 'QUERY']),
'cacheable' => in_array($method, ['GET', 'QUERY']),
'results' => Order::query()
->when($statusFilter, fn ($q) => $q->whereIn('status', (array) $statusFilter))
->get(),
]);
}
That one controller handling three verbs side by side is the clearest way to see the difference: GET and QUERY both report themselves as safe, idempotent, and cacheable, and POST reports none of the three, despite all three returning the same shape of data.
Where it falls down today
Playing with this in a real project surfaced the actual limitation, which has nothing to do with Laravel. Adoption is close to zero outside of specs and demos. I pointed the outgoing Http::query() calls at JSONPlaceholder, a widely used public test API, and every one came back with a 404 or 405, because the server has no idea what to do with a verb it does not recognise. That is the honest state of things in 2026: the client and server tooling exists, but the wider HTTP ecosystem, reverse proxies, third-party APIs, testing tools, has not caught up yet.
- Most third-party APIs will reject
QUERYoutright until they explicitly add support for it. - Older reverse proxies and API gateways may not recognise the verb and could drop or mishandle the request.
- Client libraries and API testing tools (Postman, Insomnia, older HTTP clients) need explicit updates to send it at all.
- Realistic adoption timeline for general availability is closer to 2027 to 2028, per the same pattern PATCH followed after its own standardisation.
None of that makes it a wasted afternoon. For an internal API where you control both ends, client and server, there is no reason to wait. The mock endpoint in my test project accepts GET, POST, and QUERY on the same route and behaves correctly for all three today, using code that ships in a stable Laravel release.
When to reach for it
The decision is simpler than the RFC discussion makes it sound:
- Filter fits comfortably in a short query string and has no sensitive values: keep using GET.
- You are genuinely creating or mutating a resource: use POST, PUT, or PATCH as normal, QUERY is not a fit here regardless of body size.
- You are reading data with a filter too large or too sensitive for a URL, on an internal API you control end to end: use QUERY now.
- You are calling a third-party API: check their documentation first. Assume no support until proven otherwise.
Summary
HTTP QUERY exists to remove a workaround that has been baked into API design for years: using POST to fetch data because GET could not carry a large enough filter safely. Laravel 13's Http::query() makes the outgoing side trivial, and Symfony HttpFoundation 7.4 makes the incoming side just work once you know that Route::match() will not register the verb for you. The gap right now is not the framework, it is the rest of the internet catching up. If you own both sides of an API boundary, there is no reason to wait for that.
If you are experimenting with this or want to talk through where it fits in your own API, reach out via the contact section on the main site.