Make WordPress Core

Opened 7 years ago

Closed 5 years ago

Last modified 4 years ago

#42961 closed defect (bug) (fixed)

REST API: Cannot pass empty object url encoded data

Reported by: steffanhalv's profile steffanhalv Owned by: kadamwhite's profile kadamwhite
Milestone: 5.4 Priority: normal
Severity: normal Version: 4.7
Component: REST API Keywords: has-patch has-unit-tests commit
Focuses: rest-api Cc:

Description

Sending empty arrays to endpoints through Rest API is not handled correctly by WP core.

Ex.
Querystring:
mysite.com/wp-json/...?my_empty_array[]=&... <- This works not

JSON:
data: { my_empty_array: [] } <- This works

When posting using json, empty array is handled correctly. When using querystring, it fails.

Please see https://github.com/woocommerce/woocommerce/issues/18249

Could also have a possible relationship with ticket:
https://core.trac.wordpress.org/ticket/42752 (https://github.com/woocommerce/woocommerce/issues/17954)

Attachments (1)

42961.diff (1.9 KB) - added by TimothyBlynJacobs 5 years ago.

Download all attachments as: .zip

Change History (16)

#1 @dd32
7 years ago

  • Version changed from 4.9.1 to 4.7

This appears to be the correct behaviour, although an unexpected one at that.

Ex. Querystring:
mysite.com/wp-json/...?my_empty_array[]=&... <- This works not

In PHP applications, var[]= is always treated as being set to an empty string, that means that this is interpreted as array( '' ).

JSON:
data: { my_empty_array: [] } <- This works

When using JSON, you can pass more specific types than what you can in a PHP Query string, which results in this being a literal array()


All that being said, this feels a little awkward, and I'd prefer to see it standardised on the JSON behaviour, but I'm not sure it's really possible though.

One option would be to run array_filter() on any array items in the query string, but doing so also means moving away from standard PHP URL parsing quirks.

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


7 years ago

#3 @schlessera
7 years ago

One option would be to run array_filter() on any array items in the query string, but doing so also means moving away from standard PHP URL parsing quirks.

Going with "standard PHP URL parsing quirks" also seems wrong to me, though, as the URL should be parsed in a reliable way, no matter the language the server is written in. The code should implement whatever is the expected behavior from the URL alone, even if it differs from standard PHP behavior.

#4 @steffanhalv
7 years ago

Yes, I agree. Current handling of empty arrays in querystrings force apps to post using json, which is very stricted by same origin policy. It can be handled by using method override though, but it would be nice to accept this in querystrings to, as querystrings is actually supposed to be supported by the api.

#5 @steffanhalv
6 years ago

  • Focuses coding-standards added
  • Severity changed from normal to blocker

This makes it impossible to delete arrays completely using query string, so it becomes a blocker for my use case.

Any progress on this?

Version 0, edited 6 years ago by steffanhalv (next)

#6 @TimothyBlynJacobs
5 years ago

  • Keywords reporter-feedback added
  • Severity changed from blocker to normal

@steffanhalv could you give an example of the array you are trying to clear in the query string?

#7 @steffanhalv
5 years ago

@TimothyBlynJacobs I already did here https://github.com/woocommerce/woocommerce/issues/18249.

But here is some other examples:

If you have a post with the id <id> it is not possible to delete the last category or tag or role or any other 'last item of an array' using the rest api with queryparams.

This works (replacing or adding to an array ):

POST /wp/v2/posts/<id>?categories[0]=...&categories[1]=...&...
POST /wp/v2/posts/<id>?tags[0]=...&...
POST /wp/v2/users/<id>?roles[0]=...&...

Response = success

None of these works (trying to remove the last item of an array):

POST /wp/v2/posts/<id>?categories[]=&...
POST /wp/v2/posts/<id>?tags=null&...
POST /wp/v2/users/<id>?roles[]=null&...
POST /wp/v2/users/<id>?roles[]=0&...
POST /wp/v2/users/<id>?roles=0&...
POST /wp/v2/users/<id>?roles=&...
POST /wc/v2/products?attributes[0]=...&...

Response =

code:"rest_invalid_param"
data:{status: 400, params: {attributes: "attributes[0] is not of type object."}}
message:"Invalid parameter: attributes"

A post request to any of this endpoints has no possible ways to clear the arrays using queryparams.
Yes, JSON works but JSON is not accepted in POST requests for many servers as it has CORS restrictions which is not the case for queryparams. So the conclusion is that empty arrays is not handled correctly by the WP core.

Also please notice that I have not URL Encoded the strings in this example, but it is when Im doing the actual request.

Last edited 5 years ago by steffanhalv (previous) (diff)

#8 @TimothyBlynJacobs
5 years ago

  • Focuses coding-standards removed
  • Keywords has-patch has-unit-tests 2nd-opinion added; reporter-feedback removed
  • Milestone changed from Awaiting Review to 5.4
  • Summary changed from Rest API doesnt handle empty arrays in querystring correctly to REST API: Cannot pass empty object url encoded data

Thanks for providing that context @steffanhalv! A couple of things to untangle here.

The REST API does send CORS headers. As far as I know, the intention is that any WordPress with the REST API enabled would be able to accept JSON by default: https://github.com/WordPress/wordpress-develop/blob/bf3295d10f066ef6bf670bb272aae9db400e8be5/src/wp-includes/rest-api/class-wp-rest-server.php#L239 Is there a specific deficiency you noticed in the CORS setup that WordPress Core could address?

If you were not aware, the REST API accepts form url encoded data as post data.

For the actual issue, I'm not able to replicate if the schema is setup properly. Given the following REST API route:

<?php
add_action( 'rest_api_init', function () {
        register_rest_route(
                'my-ns',
                'my-route',
                [
                        'methods'  => WP_REST_Server::EDITABLE,
                        'args'     => [
                                'array' => [
                                        'type'              => 'array',
                                        'items'             => array(
                                                'type' => 'string',
                                        ),
                                        'validate_callback' => 'rest_validate_request_arg',
                                        'sanitize_callback' => 'rest_sanitize_request_arg',
                                ],
                        ],
                        'callback' => function ( WP_REST_Request $request ) {
                                return new WP_REST_Response( $request->get_params() );
                        },
                ]
        );
} );

And the given request:

curl --request POST \
  --url http://trunk.test/wp-json/my-ns/my-route \
  --header 'accept: application/json' \
  --header 'content-type: application/x-www-form-urlencoded' \
  --data array=

I receive the following response:

{
  "array": []
}

If there is not items schema set, then rest_sanitize_value_from_schema() will bail by just casting the value to an array and you'll get an array of a single string.

Taking a look at your error message, though, it looks like you are trying to set an empty object not an array. Doing a bit of research, it doesn't look like there is an RFC about how this should work, if anyone knows of one, please mention it. Languages also aren't consistent about it. For example, in nodejs.

> const qs = require( 'querystring' );
> qs.stringify( { obj: {} } );
'obj='
> qs.parse( 'obj=' );
{ obj: '' }

In the REST API currently, obj= fails because it is not an object. And given that you can create an empty array with arr=. I think we can allow obj= as an empty object. There shouldn't be any BC concerns because that value is currently blocked by rest_validate_value_from_schema, so we wouldn't be transforming a previously valid value into a different value. rest_sanitize_value_from_schema will already convert anything invalid into an empty array so we should be good on that side as well.

I've uploaded a patch that does that for review. I'm provisionally marking this for 5.4, but again this needs review before being committed.

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


5 years ago

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


5 years ago

#11 @TimothyBlynJacobs
5 years ago

This still looks good to me. @kadamwhite what do you think?

#12 @TimothyBlynJacobs
5 years ago

  • Keywords early added
  • Milestone changed from 5.4 to 5.5

Punting to 5.5.

#13 @kadamwhite
5 years ago

  • Keywords commit added; 2nd-opinion early removed
  • Milestone changed from 5.5 to 5.4
  • Owner set to kadamwhite
  • Status changed from new to assigned

This does look good, and it's my oversight that we didn't land it earlier in the cycle. I'm going to pull this back in for 5.4 as I don't see any further changes we'd need to make.

#14 @kadamwhite
5 years ago

  • Resolution set to fixed
  • Status changed from assigned to closed

In 47362:

REST API: Correctly infer empty objects passed via query parameters.

Permit passing an empty object as the string "?obj=". The type of the passed empty argument is inferred from the registered schema.

Props TimothyBlynJacobs, steffanhalv, schlessera, dd32.
Fixes #42961.

#15 @TimothyBlynJacobs
4 years ago

In 47811:

REST API: Add @since entries for rest_validate_value_from_schema().

See #49572, #48818, #44949, #50053, #48820, #49720, #42961, #44975, #43392, #38583.

Note: See TracTickets for help on using tickets.