Schedule a Call

arrow_backBack to Blogs
July 27, 2026 / Liferay / 15 min read
Pyronite AuthorPyronite Engineering Team

HTTP QUERY Method (RFC 10008) Meets Liferay DXP

HTTP QUERYRFC 10008Headless APIsLiferay DXPIntegration

On June 19, 2026, the IETF published RFC 10008 — "The HTTP QUERY Method" — the first genuinely new general-purpose HTTP method in decades. It's a safe, idempotent, cacheable method that can carry a request body: in plain words, "a GET that can send a body."

If you build headless experiences on Liferay DXP, this matters more than it might seem. Liferay's headless APIs lean on long ?filter= URLs and POST-for-read search endpoints precisely because HTTP never had a proper verb for "complex, read-only query." Now it does.

This guide covers what QUERY is, why your Liferay APIs should care, and a complete step-by-step integration into Liferay DXP headless today — custom JAX-RS resources, CORS, caching semantics, client consumption, and the gotchas nobody reads about until production.


Table of Contents

  1. What Is HTTP QUERY? The One-Minute Version
  2. The 25-Year Gap It Closes: GET vs. POST
  3. How QUERY Actually Works (RFC 10008)
  4. Why QUERY Matters for Liferay DXP Headless
  5. Ecosystem Readiness: What Works Today
  6. Prerequisites
  7. Step-by-Step: Adding a QUERY Endpoint to Liferay DXP
  8. Consuming Your QUERY Endpoint (Clients)
  9. GET vs. QUERY vs. POST: The Decision Guide
  10. Real-World Liferay Use Cases
  11. Pitfalls & Troubleshooting
  12. Best Practices & Governance
  13. How Pyronite Can Help
  14. FAQ

1. What Is HTTP QUERY? The One-Minute Version

  • RFC 10008, published June 19, 2026 as a Proposed Standard by the IETF HTTPbis working group (authors: Julian Reschke, James M. Snell, Mike Bishop — from greenbytes, Cloudflare, and Akamai). The work ran for years as draft-ietf-httpbis-safe-method-w-body — fourteen draft revisions before earning the RFC number.
  • IANA has registered QUERY in the HTTP Method Registry as safe and idempotent — it's a full standard, not an experiment. A new header field, Accept-Query, was registered alongside it.
  • Semantics: the request content (body) describes a query; the server executes it read-only and returns the result. Because it's safe and idempotent, caches may store the response and clients/proxies may automatically retry it — the two superpowers POST never had.

As one analyst put it: "QUERY gives the web's favorite workaround a protocol identity… machine-readable intent: retry engines, caches, and autonomous agents act on what a method declares, not on what documentation intends." Under automation — API gateways, service meshes, AI agents — declared semantics become policy. That's the point.


2. The 25-Year Gap It Closes: GET vs. POST

Every API engineer knows this wall — and Liferay headless APIs run into it more than most:

Property`GET``QUERY``POST`
Safe (read-only)⚠️ Not guaranteed
Idempotent (retry-safe)⚠️ Not guaranteed
CacheableOnly for future GET/HEAD
Request body"No defined semantics" (RFC 9110)✅ Expected✅ Expected

Why GET breaks down for real queries — all four reasons are called out in the RFC itself:

  1. Unknown length limits. A request crosses browsers, proxies, and frameworks that never agreed on limits; RFC 9110 only recommends supporting ~8,000 octets. Long OData-style filter strings, nested conditions, big ID lists, sorting and pagination all eat into that fast.
  2. Encoding is hostile. Structured JSON filters become unreadable, escaped URL spaghetti.
  3. Every combination becomes a "distinct resource." Caching gets useless when each permutation is a different URI.
  4. Exposure. URIs live in browser history, bookmarks, access logs, and referrers. Sensitive filter values in query strings leak.

So everyone fell back to POST-for-reads — which works everywhere but surrenders safety, idempotency, and caching. QUERY is the purpose-built middle path.

⚖️ "What about WebDAV's SEARCH/REPORT/PROPFIND?" They existed, but carry WebDAV's namespace baggage and never generalized. QUERY was named and specified for the whole web, deliberately.

3. How QUERY Actually Works (RFC 10008)

A request — complex product search:

QUERY /products HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json

{
  "filter": { "category": "shoes", "price": { "gte": 100, "lte": 200 } },
  "sort": ["-rating", "price"],
  "page": { "number": 2, "size": 20 }
}

The response shapes you must know:

  • 200 OK + results — the normal case.
  • Accept-Query (response header) — how a server advertises which query media types it accepts for the target resource (think Accept-Patch, but for queries). Returned on the resource or in OPTIONS responses.
  • The "equivalent resource" — a QUERY doesn't have to stay anonymous. The server can mint a URI for "this exact query on this resource" and return:
  • Content-Location: → "here is where this result lives" → enables caching the answer.
  • Location: → "here is a URI you can plain-GET to repeat this query" → the escape hatch: expensive QUERY once, then cheap cacheable GETs. This Location bridge is deliberately built into the standard so QUERY can degrade gracefully across infrastructure that never learned the verb — likely the feature that carries it through its awkward early years.

4. Why QUERY Matters for Liferay DXP Headless

Liferay's headless surface (REST Builder-generated APIs under /o/...) currently speaks two dialects for reads:

  • GET with giant query stringsfilter=, search=, sort=, aggregationTerms=, flatten=... Powerful, but URLs get long, escaped, and fragile as filters go nested/relational.
  • POST-for-reads — case in point: Liferay's Search APIs (/o/search, GA in DXP 2025.Q4+/2026.Q1) document exactly two verbs: GET /search (keyword search, no facets) and POST /search (JSON body when you need facets and rich request shaping). GraphQL is POST too.

Both exist because QUERY didn't. The moment QUERY lands in frameworks:

  • Faceted search, reporting endpoints, and structured Object queries get cacheable (portal product/catalog queries = CDN gold).
  • Retries become provably safe for flaky networks across your React/Next.js frontends and mobile apps.
  • Agentic consumers win big. AI agents and MCP tools call your APIs autonomously; a method that declares "safe, read-only, retry me" lets agent runtimes, gateways, and caches enforce policy mechanically instead of trusting your swagger description.
  • The Liferay community has already clocked this: there's an open feature request on Liferay's forums — "The case for experimental HTTP Query support in Liferay" — arguing exactly for QUERY as filters become nested and developers hand-build POST /search endpoints instead. Native REST Builder support hasn't shipped as of this writing (July 2026) — which is why this guide shows you how to integrate it today in custom modules, cleanly, so you can drop it when Liferay catches up.

Mapping today's Liferay patterns to a QUERY-native design:

Today in Liferay headlessPainQUERY-native design
GET /o/headless-admin-list-type/v1.0/...?filter=...(escaped OData)URL limits, unreadableQUERY + JSON query document
POST /o/search for faceted searchNo caching, no safe retryQUERY /search under /o, cacheable results
Hand-rolled POST /advanced-search in custom modulesVerb lies about intentQUERY — intent is protocol-level
GraphQL POST /o/graphql for readsUncacheable by defaultQUERY as the GraphQL transport for read operations (spec-compatible pattern)

5. Ecosystem Readiness: What Works Today

LayerStatus (July 2026)Impact
RFC / IANA✅ RFC 10008, registered safe+idempotentStable contract to build against
Tomcat / Liferay runtime✅ Passes arbitrary method tokens to the servlet/JAX-RS layerNo platform blocker
JAX-RS (CXF) in Liferay OSGi✅ Supports custom method annotations (@HttpMethod("QUERY"))This is our integration path (below)
Liferay REST Builder generator❌ Does not emit QUERY operations yetUse custom JAX-RS resources for now; watch the community request
OpenAPI 3.2✅ Can document query operationsSpec your endpoint properly
Node.js✅ Parses QUERY natively (http module, since early 2024)BFF/Next.js server-side calls fine
Gohttp.NewRequest("QUERY", ...); method-string routingMicroservices fine
Spring (RestClient)⚠️ Works via RequestMethod.valueOf("QUERY"); first-class support in progressUsable today
Browsers (fetch/XHR)⚠️ Method token works same-origin; cross-origin always preflights (QUERY is not CORS-safelisted)Plan CORS (Step 6)
CDNs / WAFs / API gateways⚠️ Some reject unknown methods (e.g., certain managed CDNs 403 anything outside GET/HEAD/PUT/POST/PATCH/DELETE/OPTIONS)Allowlist QUERY or use the Location bridge fallback
HTML forms❌ Not yet (WHATWG work in progress)Irrelevant for APIs

Net-net: perfect for server-to-server, BFF-to-DXP, and same-origin portal frontends today; add allowlists for anything else.


6. Prerequisites

  1. Liferay DXP 2026.Q1 LTS (or 7.4 U-recent GA) — any modern runtime works; Tomcat bundle assumed.
  2. Blade CLI / Liferay Workspace for module scaffolding.
  3. Basic OSGi + JAX-RS familiarity.
  4. Access to Control Panel (for CORS) and your infra layer (proxy/WAF rules).
  5. A read path worth optimizing — e.g., an Object collection with heavy filters, or a custom search page substituting SQL-ish logic.

7. Step-by-Step: Adding a QUERY Endpoint to Liferay DXP

We'll build a cacheable, retry-safe QUERY /o/ai-ready/search resource backed by Liferay's search engine.

Step 1 — Scaffold the module

blade create -t api pyronite-query-api

Add the usual portal-vulcan / portal-odata (optional) dependencies in build.gradle.

Step 2 — Declare the custom method annotation

JAX-RS defines methods via annotation meta-marked with @HttpMethod — custom verbs included:

package com.pyronite.query.api.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.ws.rs.HttpMethod;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@HttpMethod("QUERY")
public @interface QUERY {
}

Step 3 — Implement the resource (safe by contract)

@Path("/search")
public class KnowledgeSearchResource {

    @QUERY
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response query(KnowledgeQuery query) {
        // 1) Validate strictly — body is untrusted input
        validate(query);

        // 2) Execute READ-ONLY work only (Liferay search engine / Objects local service)
        PageResult result = _searchService.execute(query);

        // 3) Return with honest caching semantics
        return Response.ok(result)
                .header(HttpHeaders.CACHE_CONTROL, "private, max-age=60")
                // Optionally: .header("Accept-Query", "application/json")
                // Optionally: .contentLocation(queryUri) for cacheable equivalence
                .build();
    }

    @Reference
    private SearchService _searchService;
}

Hard rules: no mutations, no side effects, no "safe except…". The moment a QUERY handler writes anything, you have broken the protocol contract — clients and caches will retry it.

Step 4 — Register the JAX-RS application (OSGi)

@Component(
    property = {
        "osgi.jaxrs.application.base=/ai-ready",
        "osgi.jaxrs.name=Pyronite.Query.Api",
        "auth.verifier.guest.allowed=false"
    },
    service = Application.class
)
public class QueryApiApplication extends Application {

    @Override
    public Set<Object> getSingletons() {
        return Set.of(_knowledgeSearchResource);
    }

    @Reference
    private KnowledgeSearchResource _knowledgeSearchResource;
}

Deploy (blade deploy), then sanity-check with a simple request — authentication/permission checks run through Liferay's standard pipeline, so nothing special is required for QUERY.

Step 5 — Smoke-test the wire

curl -i -X QUERY \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -u svc.api@example.com:test \
  https://dxp.example.com/o/ai-ready/search \
  -d '{"filter":{"type":"kb-article","published":true},"page":{"size":20}}'

Expect 200 OK with results. A 405 Method Not Allowed means your resource annotation isn't being matched (check package registration/logs); a pass-through error at the edge means Step 7.

Step 6 — CORS for browser clients (cross-origin)

Because QUERY isn't on the CORS safelist, every cross-origin browser call preflights. In Control Panel → Instance Settings → Security → CORS, add a pattern for your base (/o/ai-ready/*) and ensure the allowed methods list includes QUERY plus headers Content-Type, Authorization. Same-origin portal UIs (React fragment, customer-site Next app served from the same domain) need nothing.

Step 7 — Open the infrastructure path

Walk every hop and confirm QUERY survives: nginx/Apache vhost → any LB → WAF → CDN. Most pass arbitrary verbs by default; managed CDNs and strict WAF profiles are the usual blockers (some return 403 for non-standard methods). Two sanctioned workarounds:

  • Allowlist QUERY at the edge (preferred), or
  • Location bridge: respond Location: /o/ai-ready/search-cached?id= once, and clients plain-GET thereafter. Ugly-flexible, spec-endorsed.

Step 8 — Caching discipline

Decide per endpooint:

  • Public, stable results → include a deterministic Content-Location + Cache-Control: max-age=... so shared caches can serve identical queries.
  • User-specificCache-Control: private, and consider whether the query content itself is sensitive enough to keep out of any shared cache. Bodies are good for secrecy; caches are literal. Choose deliberately.

Step 9 — Document & operationalize

  • Describe the operation with OpenAPI 3.2 (query verb) so client generators don't choke.
  • Confirm your APM/log pipeline doesn't flag QUERY as suspicious; add it to dashboards/alerts allowlists.
  • Add integration tests: idempotent repeats, abort-and-retry mid-response, permission scoping per user, payload validation fuzzing.

8. Consuming Your QUERY Endpoint (Clients)

Browser fetch (same-origin):

const res = await fetch('/o/ai-ready/search', {
  method: 'QUERY',
  headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
  body: JSON.stringify(query),
  credentials: 'include'
});
const data = await res.json();

Spring (Java) — via enum-bridge until first-class support lands:

SearchResponse r = restClient
    .method(HttpMethod.valueOf("QUERY"))
    .uri("https://dxp.example.com/o/ai-ready/search")
    .contentType(MediaType.APPLICATION_JSON)
    .body(query)
    .retrieve()
    .body(SearchResponse.class);

Go:

req, _ := http.NewRequest("QUERY", url, bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
// Safe + idempotent ⇒ retrying after a transport failure is always correct.

Server-to-server consumers (integration jobs, AI agents via MCP tool wrappers) should treat QUERY responses as first-class cacheable citizens — that, after all, is why the verb exists.


9. GET vs. QUERY vs. POST: The Decision Guide

ScenarioVerb
Fetch a known resource / simple list with small paramsGET
Deep filters, nested conditions, large ID sets, faceted search, reports — all read-onlyQUERY
Create/update/delete, workflow actions, anything mutatingPOST / PUT / PATCH / DELETE
Audit-log-sensitive SOCs uncomfortable with new verbsStay GET/POST until governance signs off — but document the intent

QUERY is not a replacement for GET or POST — it's the missing third leg for complex reads.


10. Real-World Liferay Use Cases

  1. Faceted enterprise search — QUERY the /o/ai-ready/search style resource instead of POST-for-facets; cache identical searches across users.
  2. Insurance quote lookups — hundreds of filter permutations (filter=status eq 'PENDING' and agentId in (...)) that previously blew past URL limits.
  3. Dealer/partner portals — catalog queries with nested relational filters over Liferay Objects.
  4. Reporting endpoints — "give me JSON body describing the report window; return aggregates" — read-only by nature, cacheable by design.
  5. AI-agent consumption — MCP tools/AI Hub agents hitting your read APIs can retry fearlessly on flaky networks; policy engines can allow-list QUERY as "read-only traffic."
  6. GraphQL transport alternative — run read-only GraphQL documents over QUERY to recover HTTP caching semantics.

11. Pitfalls & Troubleshooting

SymptomLikely causeFix
405 Method Not AllowedAnnotation not scanned / wrong @HttpMethod tokenVerify @QUERY meta-annotation, resource registration, app base path
Edge 403 before reaching LiferayWAF/CDN rejects unknown methodAllowlist QUERY or use the Location bridge fallback
CORS preflight failsQUERY missing from allowed methodsAdd QUERY in Liferay CORS pattern (Step 6)
POST /search "works the same" — why bother?It works, but isn't cache/ retry/agents-safeQUERY declares intent; POST-for-read can never
Client generator ignores query opsTooling still on OpenAPI ≤3.1Upgrade to OpenAPI 3.2 or hand-write that client call
Someone writes in a QUERY handlerProtocol contract broken → cache poisoning, double-effectsCode review + tests enforcing read-only handlers
Queries with PII cached by shared cacheCache hits the wrong audienceprivate/no-store for user-specific; Content-Location only for stable public queries
Monitoring floods with "unknown method" alertsNOC rules predate RFC 10008Update alert dictionaries; it's a standard now
REST Builder module won't accept QUERYGenerator emits GET/POST/PUT/DELETE/PATCH onlyCustom JAX-RS resource (this guide) until native support ships

12. Best Practices & Governance

  • Read-only handlers, always. The safety contract is the whole point.
  • Validate the body like any untrusted input — size caps, schema validation, depth limits.
  • Return Accept-Query so clients can discover accepted query formats.
  • Give queries identity — hash stable public queries → Content-Location; unlocks caching + observability.
  • Careful with shared caches + PII. Safe ≠ shareable.
  • Test retries for free. Idempotency means your chaos/retry tests should pass trivially — make them prove it.
  • Track the Liferay community request for native QUERY support; keep your custom resource thin so you can retire it when generated APIs arrive.
  • Version your query document schema ("v": 1), not just your URI.

13. How Pyronite Can Help

At Pyronite, headless architecture is our home turf — Liferay REST Builder APIs, custom OSGi/JAX-RS services, and React/Next.js frontends on top. For teams exploring QUERY:

  • Rapid implementation — production-grade QUERY endpoints (validation, caching semantics, permission parity) dropped into your existing workspace, the exact pattern above.
  • Search & performance engineering — facet/query design against Liferay's search engine, plus cache strategies that actually move your Core Web Vitals and infra spend.
  • API governance — OpenAPI 3.2 docs, security review read-only contracts, CORS/WAF hardening, and tests proving idempotency.
  • Future-proofing for AI — wrap QUERY endpoints as MCP tools / Liferay AI Hub agent capabilities so your agentic integrations inherit declared-safety semantics end to end.

👉 Connect with us — a QUERY-readiness review of your current headless layer takes about a week and ends with a working proof-of-concept endpoint.


14. FAQ

Is QUERY official, or still a draft?
Official. RFC 10008, Proposed Standard, June 19, 2026 — registered at IANA as safe + idempotent.

Does Liferay support QUERY natively in generated headless APIs?
Not as of July 2026. There's an active community feature request for experimental support. Custom JAX-RS resources (Section 7) are the clean path today.

Why not just keep using POST for complex reads?
It works, but POST is not safe, idempotent, or cacheable. Every gateway, retry loop, cache, and AI agent treats "safe" mechanically — POST can't promise it; QUERY can.

Can browsers call QUERY?
Same-origin fetches can send the method token today. Cross-origin calls always trigger a CORS preflight (QUERY isn't safelisted), so configure Liferay CORS accordingly. HTML forms don't support it yet.

Can QUERY responses be cached like GET?
Yes — that's a headline feature. Provide Content-Location (+ sane Cache-Control) for stable queries; use private semantics for user-specific ones.

Is sending a body with GET the same thing?
No — GET bodies have "no defined semantics" and some stacks reject them. QUERY is the standardized, well-defined answer to that exact ambiguity.

Does it work with Liferay's Search API (/o/search)?
Liferay's built-in /o/search still documents GET + POST (POST for facets). QUERY is your custom/integration path today — and the obvious evolutionary fit if Liferay adopts it for those endpoints.


Key Takeaways

  1. QUERY is real and standardized — RFC 10008 (June 2026): safe + idempotent + cacheable + body-carrying.
  2. It fixes the exact pain Liferay headless APIs feel — long filter URLs and POST-for-facet-search.
  3. You can integrate it today in Liferay DXP via a custom JAX-RS module, @HttpMethod("QUERY") annotation, CORS + infra allowlisting — no platform surgery.
  4. Guard the contract — read-only handlers, strict validation, honest cache headers.
  5. Design for the transition — thin custom resources now, native support (likely) later; Location bridge for legacy edges.

References

  • RFC 10008 — The HTTP QUERY Method (IETF Datatracker, June 2026) — datatracker.ietf.org
  • draft-ietf-httpbis-safe-method-w-body (14 drafts; IESG approval Nov 2025) — httpwg/http-extensions
  • InfoWorld — "IETF publishes QUERY method to allow safe and idempotent HTTP requests" (July 2026)
  • Liferay Learn — Search APIs (/o/search, GA DXP 2025.Q4+/2026.Q1) and API Query Parameters — learn.liferay.com
  • Liferay Community — "The case for experimental HTTP Query support in Liferay" — discuss.liferay.com/t/782

© 2026 Pyronite Technologies LLP. Originally published at pyronite.in.