Make WordPress Core

Opened 3 years ago

Last modified 5 days ago

#59425 new defect (bug)

REST API: Preserve boolean values when generating next/prev Link headers

Reported by: jeremyfelt Owned by:
Priority: normal Milestone: Awaiting Review
Component: REST API Version: 5.2
Severity: normal Keywords: has-patch has-unit-tests
Cc: Focuses: rest-api

Description

When generating rel="next" and rel="prev" values for the Link header in the terms, users, search, revisions, posts, global styles, and comments controllers, existing parameter data is passed to urlencode_deep(), which converts boolean false to an empty string.

The example I'm focused on is a request for terms with hide_empty set to false.

An initial URL like:

https://example.org/wp-json/wp/v2/tags?hide_empty=false&per_page=100&context=edit&_locale=user

Returns a response with a Link header of:

<https://example.org/wp-json/wp/v2/tags?hide_empty&per_page=100&context=edit&_locale=user&page=2>; rel="next"

When this URL is followed, a 400 response is returned from the REST API with the error: is not of type boolean.

When hide_empty=true is passed with the URL instead, the REST API converts the parameter to 1 in the Link header, which it then seems to accept in future requests as if it was boolean.

The cause of this is the use of urlencode_deep() ​on request params when generating the header:

$base = add_query_arg( urlencode_deep( $request_params ), $collection_url );
wp> urlencode_deep( true );
=> string(1) "1"
wp> urlencode_deep( false );
=> string(0) ""
// which is really
wp> map_deep( true, 'urlencode' );
=> string(1) "1"
wp> map_deep( false, 'urlencode' );
=> string(0) ""

Which is really just urlencode() making sure it's dealing with a string, as expected.

I _think_ the answer is for the REST API to restore boolean values in the request params back to URL-compatible strings before building the Link header. While false being altered breaks things, it is also strange that true is altered as well.

I set the version as 5.2, as this is when the urlencode_deep() change was made. (See [45267] via #46199) I haven't investigated further, but before that change, hide_empty would just be dropped from the rel="next" link entirely.

Change History (5)

#1 @Mamaduka
23 months ago

The same bug has been reported on the Gutenberg repo - ​https://github.com/WordPress/gutenberg/issues/67556.

This ticket was mentioned in ​Slack in #core by mikachan. ​View the logs.


22 months ago

This ticket was mentioned in ​Slack in #core-restapi by joemcgill. ​View the logs.


22 months ago

#4 @mahbubwpwist
5 days ago

Reproduced on trunk (7.2-alpha-63166-src, ab2373a).

GET /wp/v2/tags?hide_empty=false&per_page=1 returns a next link of:

/wp/v2/tags?hide_empty&per_page=1&page=2

Following it returns a 400: hide_empty is not of type boolean.

The same add_query_arg( urlencode_deep( $params ), ... ) pattern is used for the pagination links in 10 controllers: posts, terms, users, comments, search, revisions, global styles revisions, font collections, and the two Abilities API controllers (list and categories).

I'd like to work on a patch with unit tests. My plan is to add a small shared helper that converts booleans before urlencode_deep() and use it in all 10 places, rather than patching each controller separately.

One question: should false become 0 (matching how true already becomes 1), or should both become 'true'/'false'? Both pass rest_is_boolean(). I'd lean towards 0/1 for consistency with the current output, unless there's a preference.

This ticket was mentioned in ​PR #14096 on ​WordPress/wordpress-develop by ​@mahbubwpwist.


5 days ago
#5

  • Keywords has-patch has-unit-tests added

Collection endpoints build their next/prev Link headers with add_query_arg( urlencode_deep( $params ), ... ). urlencode() turns false into an empty string, so a request such as /wp/v2/tags?hide_empty=false returns a next link containing hide_empty&page=2. Following that link fails with a 400: hide_empty is not of type boolean. This is what breaks the Terms List block with "Show empty terms" enabled (WordPress/gutenberg#67556).

This PR adds rest_urlencode_query_params(), which converts booleans to 1 and 0 before calling urlencode_deep(), and uses it in the 10 controllers that build pagination links: posts, terms, users, comments, search, revisions, global styles revisions, font collections, and the Abilities API list and categories controllers.

1/0 matches how true is already encoded, and how _http_build_query() treats false. Both forms pass rest_is_boolean(). Happy to switch to 'true'/'false' if that is preferred (question asked on the ticket).

Tests:

  • Unit tests for the new function (booleans, nested arrays, strings, integers).
  • Tags (hide_empty=false) and posts (sticky=false) controller tests asserting the next link keeps the value and can be followed. Both fail without the fix.
  • --group restapi passes locally (3608 tests).

Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 5.5, Claude Sonnet 5.5
Used for: Reproducing the bug, drafting the patch and tests, and running the test suite. I reviewed and tested the changes and take responsibility for them.

Note: See TracTickets for help on using tickets.