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)
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
@
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 restapipasses 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.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)
The same bug has been reported on the Gutenberg repo - https://github.com/WordPress/gutenberg/issues/67556.