Skip to content

feat: HTTP QUERY method support (RFC 10008)#8349

Open
soyuka wants to merge 2 commits into
api-platform:mainfrom
soyuka:feat/http-query-method
Open

feat: HTTP QUERY method support (RFC 10008)#8349
soyuka wants to merge 2 commits into
api-platform:mainfrom
soyuka:feat/http-query-method

Conversation

@soyuka

@soyuka soyuka commented Jun 24, 2026

Copy link
Copy Markdown
Member
Q A
Branch? main
Tickets RFC 10008
License MIT
Doc PR todo

What

Adds foundational support for the HTTP QUERY method (RFC 10008): a safe, idempotent collection operation that carries its parameters in the request body instead of the URI query string.

#[ApiResource(operations: [
    new Query(parameters: [
        'name' => new QueryParameter(filter: new PartialSearchFilter(), property: 'name'),
    ]),
])]
class Book {}
QUERY /books
Content-Type: application/x-www-form-urlencoded

name=foo&order[name]=asc

How

The whole modern parameter chain (parameter filters, parameter validation, security, strict query-parameter validation) reads values from the _api_query_parameters request attribute. So the feature is a single chokepoint: ParameterProvider sources _api_query_parameters from the QUERY body rather than the query string, negotiated by Content-Type:

  • application/x-www-form-urlencoded → shares the query-string grammar, reuses RequestParser::parseRequestParams
  • any JSON-based media type → decoded to an associative array
  • empty body → no parameters
  • unsupported media type → 415 with an Accept-Query response header
  • malformed JSON → 400

The new Query operation defaults to read=true, write=false, validate=false, deserialize=false. Because the read/write/validate listeners already derive their behaviour from the operation flags (not blindly from Request::isMethodSafe(), which only recognises GET/HEAD as safe), these defaults treat QUERY as a safe read and keep its body out of resource-input negotiation — no listener changes required. The QUERY body negotiation is a distinct concern owned by the provider, not ContentNegotiationProvider.

This deliberately targets the Parameter API only; the legacy #[ApiFilter] / _api_filters path is not wired (it is on the deprecation track).

Tests

tests/Functional/HttpQueryMethodTest.php: empty body returns the full collection, form-urlencoded and JSON bodies filter, unsupported content type → 415, malformed JSON → 400, unknown parameter → 400 via strict validation. Plus Query metadata defaults unit test.

Out of scope (follow-ups)

  • OpenAPI 3.2 query verb emission (PathItem has no query before 3.2)
  • HTTP cache / Vary: Accept-Query / Content-Location result URIs
  • JSON:API Transform*ParametersListener body sourcing
  • Nested JSON syntax for bracket-style filters ({"price":{"gt":100}})

@soyuka soyuka left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good first implementation but to be feature-complete we need to add OpenAPI support.

@soyuka

soyuka commented Jun 24, 2026

Copy link
Copy Markdown
Member Author

RFC 10008 coverage — remaining gaps

Tracking everything in the spec not yet wired, so we have the full picture. This PR lands the core (QUERY operation + body negotiation + 415/400/empty-body). The list below is what's left. We'll take OpenAPI first.

1. OpenAPI emission (do first)

  • No query verb on PathItem — slot only exists in OpenAPI 3.2, so it requires bumping/extending the OpenAPI model.
  • No request schema generated from the operation's parameters metadata. The QueryParameter set should produce the QUERY body input schema (form-urlencoded + JSON variants), the way query-string params produce parameter objects today.

2. Accept-Query advertisement

  • Today Accept-Query is emitted only on the 415 error path (ParameterProvider::parseQueryParametersFromBody).
  • RFC § "Accept-Query": the server should advertise QUERY support proactively — at minimum on the OPTIONS response, ideally on successful QUERY responses too. Without it a client cannot discover QUERY support or the accepted media types without first triggering an error.

3. Content-Location header

  • RFC: a QUERY response MAY include Content-Location pointing to a resource that returns the same results via plain GET.
  • Not implemented. Relevant once we can mint a stable results URI.

4. Location + 303 See Other (equivalent resource / indirect response)

  • RFC: server MAY persist the query and respond 303 + Location so the client can GET the saved query later without resending the body.
  • Whole indirect-response mode is absent — PR only does direct 200 OK with results inline.

5. Conditional requests → 304 Not Modified

  • RFC: If-None-Match / If-Modified-Since evaluated against equivalent-resource semantics, yielding 304.
  • No conditional handling on the QUERY path.

6. Caching / Vary

  • Real caching is a proxy concern — we only emit HTTP cache headers, we don't cache. So scope here is headers, not a cache key implementation.
  • Caveat that still matters: QUERY carries parameters in the body, which proxies key on URI only. So a cacheable QUERY response is unsafe to cache unless we either signal it correctly or keep it non-cacheable. Need to decide what cache-control/Vary headers (if any) we emit for QUERY and document that proxy caching of body-keyed QUERY is generally unsafe.

Already noted as out-of-scope in PR description (kept here for completeness)

  • JSON:API Transform*ParametersListener body sourcing.
  • Nested JSON syntax for bracket-style filters ({"price":{"gt":100}}) — parser limitation, not strictly RFC.

Add a Query collection operation for the HTTP QUERY method: a safe,
idempotent operation that carries its parameters in the request body
instead of the URI query string.

- add HttpOperation::METHOD_QUERY
- add Query operation (CollectionOperationInterface), defaulting to
  read=true, write=false, validate=false, deserialize=false so it is
  treated as a safe read and its body is not negotiated as a resource
  input payload

ParameterProvider sources _api_query_parameters from the QUERY body
rather than the query string, negotiated by content type
(application/x-www-form-urlencoded shares the query-string grammar,
JSON-based media types decode to an associative array; unsupported
types yield 415 with an Accept-Query header). This single chokepoint
feeds the whole Parameter API unchanged: parameter filters, parameter
validation, security and strict query-parameter validation.

Symfony's Request::isMethodSafe() only treats GET/HEAD as safe, so
safety is derived from the operation flags instead, decoupling the
read/write/validate listeners with no listener changes.

Follow-ups: OpenAPI 3.2 query verb, HTTP cache/Vary on Accept-Query,
JSON:API body sourcing.
@soyuka soyuka force-pushed the feat/http-query-method branch from 354c9dc to 77d4b72 Compare June 25, 2026 10:01
…O criteria

Now that OpenAPI 3.2 support (api-platform#8350) adds the `query` field to PathItem,
wire the QUERY operation (RFC 10008) into the generated specification and
let it be driven by a dedicated input DTO.

OpenAPI:
- allow QUERY through OpenApiFactory's method gate (PathItem::$methods);
  it maps to the 3.2 `query` path item field via withQuery
- treat QUERY as a safe collection read: force the collection output schema
  and document a 200 collection response
- because QUERY carries its criteria in the body (not the URI), the
  "in: query" parameters become the request body, advertised for
  application/x-www-form-urlencoded and application/json. With a dedicated
  input class the body references that input schema like POST does;
  otherwise it is a flat object built from the query parameters. Path and
  header parameters stay where they are.

Input DTO (RFC 10008 criteria object):
- QueryParameter may now target properties, so a criteria DTO declares its
  own parameters; they are discovered on the operation's input class
- the JSON Schema factory builds an input schema for QUERY — a single
  criteria object, not a collection

Tests cover both the filter-only QUERY (flat body schema) and the
input-DTO QUERY (referenced schema + end-to-end body filtering).
@soyuka soyuka force-pushed the feat/http-query-method branch from 77d4b72 to baaf34d Compare June 25, 2026 14:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant