Skip to content

REST API: Let routes turn off _fields, _embed and response links - #14671

Open
jorgefilipecosta wants to merge 3 commits into
WordPress:trunkfrom
jorgefilipecosta:add/rest-route-response-processing-options
Open

jorgefilipecosta wants to merge 3 commits into
WordPress:trunkfrom
jorgefilipecosta:add/rest-route-response-processing-options

Conversation

@jorgefilipecosta

@jorgefilipecosta jorgefilipecosta commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

Follow up to #14547 (comment).

This PR adds the response_processing route and handler option, which lets a route serve its response data as returned:

// Turn off all processing.
'response_processing' => false,

// Or turn off only some parts.
'response_processing' => array(
	'fields' => false,
	'embed'  => false,
	'links'  => false,
),
  • fields: when false, the _fields parameter does not filter the response data, in rest_filter_response_fields() and in WP_REST_Controller::get_fields_for_response().
  • embed: when false, the _embed parameter does not embed linked resources. The links are still added as _links.
  • links: when false, the response links are not added to the response data as _links. Embedding needs the links, so embed is off too.

Every feature is on by default. Each one resolves on its own: the handler setting applies first, then the route setting, as with allow_batch. The option covers these three features only. Authentication, permission checks, status codes, headers, JSON encoding and _envelope work as before, and keys of the data itself, such as its own _links, stay as returned.

The settings are resolved once, when the request is matched, by WP_REST_Server::resolve_response_processing(). They are then read from the request attributes and the matched handler, so controllers can read them with $request->get_attributes(). WP_REST_Server::get_response_processing() returns them for a request, and matches the request to its route when the response came from the rest_pre_dispatch filter, so cached responses follow the same settings. OPTIONS requests are served the route description, so every feature is on for them. They apply to served, enveloped, batched and preloaded requests, and rest_preload_api_request() no longer casts the data to an array. The index lists the settings of an endpoint that turns off a feature.

The second commit serves data that is not an array as returned even without the option: rest_filter_response_fields() leaves scalars and objects alone and keeps list items that are not arrays, and WP_REST_Server::response_to_data() does not add links to them, unless the object implements ArrayAccess, and leaves list items that are not arrays alone when embedding.

register_rest_route(
	'my-ns/v1',
	'/run',
	array(
		'methods'             => 'GET',
		'callback'            => 'my_callback',
		'permission_callback' => '__return_true',
		'response_processing' => false,
	)
);

This lets a route return its data unchanged, e.g. the abilities run endpoint, whose result follows the ability's output schema and can be an object, a list or a scalar value. #14547 uses the option for that endpoint.

Drafted with AI assistance and reviewed by me.

Trac ticket: https://core.trac.wordpress.org/ticket/66267

Testing

Verify unit tests are passing:

npm run test:php -- --filter 'Tests_REST_Server|Tests_REST_Controller' --group 66267

…nks.

Add the `allow_fields`, `allow_embed` and `allow_links` route and
handler options. When an option is false, the `_fields` parameter does
not filter the response data, the `_embed` parameter does not embed
linked resources, or the response links are not added to the response
data as `_links`. A handler option takes precedence over the route
option, as with `allow_batch`.

This lets a route serve its response data as returned, for example data
that follows a schema of its own or that is not an array.

See #66267.
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs.

Core Committers: Use this line as a base for the props when committing in SVN:

Props jorgefilipecosta, gziolo.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.


Props Bot commands

You can trigger Props Bot actions by commenting on this PR:

  • @propsbot add <username> to give props to a GitHub user. Requires write access.
  • @propsbot remove <username> to leave a user out of the props. Requires write access.
  • @propsbot refresh to refresh this list. Adding the props-bot label works too.

@github-actions

Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@gziolo

gziolo commented Oct 22, 2026

Copy link
Copy Markdown
Member

Thanks for picking this up so quickly, @jorgefilipecosta! The opt-out works well, and all checks pass on my side. I spent some time going back and forth with Claude and Codex on the API shape, trying to find the easiest contract to use. Here's where I ended up.

What if we combine the three options into one response_processing option? The main use case is an endpoint like ability execution. It owns its output schema and just needs WordPress to leave its data alone, whether the result is an object, a list or a scalar.

// Turn off all processing.
'response_processing' => false,

// Or turn off only some parts.
'response_processing' => array(
	'fields' => false,
	'embed'  => false,
	'links'  => false,
),

How it could work

  • No option means nothing changes.
  • Settings combine one feature at a time: handler, then route, then the core default. A false at either level turns off the whole group.
  • Turning off links also turns off embedding, since embedding needs links.
  • It only covers _fields, _embed and adding _links. Auth, permissions, status codes, headers and JSON encoding work as before. We still need to figure out how _envelope fits in.
  • When processing is off, the endpoint's own keys (like its own _links or _embedded) stay untouched.

Where it should apply

  • Work the setting out in one place and use it everywhere these features run. That includes controller field selection (get_fields_for_response()) and collections, not only the final output. Controllers can already read the matched handler with $request->get_attributes().
  • Cached responses too. Right now is_response_option_enabled() returns true when a response has no matched route, so a response returned early from rest_pre_dispatch gets processed again.
  • rest_preload_api_request() casts the data with (array), so "ok" becomes ["ok"]. A preload test with a scalar would catch it.
  • get_data_for_route() could list the setting for each endpoint, the way it does for allow_batch, so clients can see it in the index.

Nice to have

rest_filter_response_fields() and response_to_data() could skip their work when the data isn't an array. Then routes that forget to opt out still don't break.

That gives abilities a simple "keep my data as is" switch, and other routes can still turn off single features. Happy to help with any of it!

@jorgefilipecosta

Copy link
Copy Markdown
Member Author

Hi @gziolo,

We still need to figure out how _envelope fits in.

I think it does not need an option. It only wraps the response in body, status and headers and does not touch the data, so when processing is off the body is the data as returned. There is a test for that case, and the batch endpoint relies on the same wrapper for every sub-response, so I guess a route should not be able to turn it off.

@jorgefilipecosta jorgefilipecosta changed the title REST API: Allow routes to disable _fields, _embed and response links REST API: Let routes turn off _fields, _embed and response links Oct 22, 2026
@jorgefilipecosta
jorgefilipecosta force-pushed the add/rest-route-response-processing-options branch from c93e5de to b6af13a Compare October 22, 2026 14:55
Replace the `allow_fields`, `allow_embed` and `allow_links` options with
one `response_processing` route and handler option. It is `false` to
serve the response data as returned, or an array with `fields`, `embed`
and `links` keys to turn off single features: the `_fields` parameter
filtering the data, the `_embed` parameter embedding linked resources,
and the response links being added to the data as `_links`. Each
feature resolves on its own, the handler setting over the route
setting, and `embed` is off whenever `links` is off.

The settings are resolved once, when the request is matched, and are
read from the request attributes and the matched handler by the server,
by `rest_filter_response_fields()` and by
`WP_REST_Controller::get_fields_for_response()`. A response returned by
the `rest_pre_dispatch` filter follows the settings of the route it was
returned for. The index lists the settings of an endpoint that turns
off a feature, and `rest_preload_api_request()` no longer casts the
data to an array.

See #66267.
@jorgefilipecosta
jorgefilipecosta force-pushed the add/rest-route-response-processing-options branch from b6af13a to 8548db3 Compare October 22, 2026 15:17
Comment thread src/wp-includes/rest-api/class-wp-rest-server.php Outdated

@gziolo gziolo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks so much, @jorgefilipecosta, for working through all the feedback so carefully! The single response_processing option is simple to use, and it covers everything we discussed: controller field selection, cached responses, preloading, the index, and data that is not an array. Your answer about _envelope makes sense to me too. I left one small inline suggestion about ArrayAccess data to consider before this lands. This is ready from my side. 🚀

@jorgefilipecosta
jorgefilipecosta force-pushed the add/rest-route-response-processing-options branch from 8548db3 to 7ec8c2f Compare October 23, 2026 09:06
Comment thread src/wp-includes/rest-api/class-wp-rest-server.php Outdated
`rest_filter_response_fields()` leaves the response unchanged when its
data is not an array, and keeps list items that are not arrays.
`WP_REST_Server::response_to_data()` returns scalar and object data as
it is instead of adding links to it, and leaves list items that are not
arrays alone when embedding. Routes that return such data no longer
need the `response_processing` option to avoid type errors.

See #66267.
@jorgefilipecosta
jorgefilipecosta force-pushed the add/rest-route-response-processing-options branch from 7ec8c2f to ba8e2f3 Compare October 23, 2026 09:31
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.

2 participants