Make WordPress Core

Opened 7 months ago

Last modified 2 months ago

#64921 accepted defect (bug)

REST API POST /wp/v2/posts: Undefined property stdClass::$id (line 766)

Reported by: swissspaceboy Owned by: westonruter
Priority: normal Milestone: Future Release
Component: General Version: 4.7
Severity: normal Keywords: has-patch needs-testing reporter-feedback has-test-info has-unit-tests early
Cc: Focuses: rest-api

Description (last modified by sabernhardt)

Creating posts via the standard WordPress REST API (POST /wp/v2/posts) generates PHP warnings in class-wp-rest-posts-controller.php.

Warnings

PHP Warning: Undefined property: stdClass::$id in
wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php on line 766
PHP Warning: Undefined property: stdClass::$post_parent in
wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php on line 769

Steps to reproduce

  • Authenticate via Application Password (Basic Auth)
  • Send POST /wp-json/wp/v2/posts with body: {"title": "Test", "content": "<p>Hello</p>", "status": "draft"}
  • Post is created successfully, but warnings are logged

Environment

WordPress 6.9.4
PHP 8.3
Apache / LiteSpeed

Confirmed on 3 independent installations

Site A: WordPress + Polylang
Site B: WordPress + Polylang
Site C: WordPress only (no Polylang, no multilingual plugin)
All 3 produce identical warnings, confirming this is a core issue.

Impact

  • Post creation works — no functional breakage
  • 2 PHP warnings per REST API call pollute php-error.log
  • Automated workflows creating multiple posts amplify the log noise

Analysis

​Lines 766-769 of class-wp-rest-posts-controller.php access $post->id and $post->post_parent on a stdClass object that lacks these properties. The $post object returned at this stage appears incomplete — possibly a regression in how prepare_item_for_response() receives the newly created post.

Expected behavior

No PHP warnings when creating posts via REST API.

Attachments (2)

64921-tests-create-item.diff​ (2.4 KB ) - added by softglaze 2 months ago.
PHPUnit coverage for create_item() with a slug on draft or pending status. Red on trunk, green with PR 11327.
64921-tests-update-item-filtered-object.diff​ (2.9 KB ) - added by softglaze 2 months ago.
PHPUnit coverage for update_item() when rest_pre_insert_post returns an object without ID (repro from comment:13). Red on trunk.

Download all attachments as: .zip

Change History (22)

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


7 months ago
#1

  • Keywords has-patch added

Trac ticket: https://core.trac.wordpress.org/ticket/64921
## Use of AI Tools

#2 @sabernhardt
7 months ago

  • Description modified (diff)

#3 @abcd95
7 months ago

  • Keywords needs-testing added

Reproduction Report

Description

This report validates whether the reported issue can be reproduced.

Environment

WordPress: 6.9.4-src
PHP: 8.3.30
Server: nginx/1.29.5
Database: mysqli (Server: 8.4.8 / Client: mysqlnd 8.3.30)
Browser: Chrome 146.0.0.0
OS: macOS
Theme: Twenty Twenty-Five 1.4
MU Plugins: None activated
Plugins:
Test Reports 1.2.0

Actual Results

❌ Error condition does not occur (not reproduced).
Successfully created a post via POST /wp-json/wp/v2/posts.
No PHP warnings were observed in logs during or after the request.

Additional Notes

The environment was explicitly switched to the 6.9.4 tag from wordpress-develop and rebuilt before testing.
Debug logging was enabled, but no warnings related to stdClass::$id or stdClass::$post_parent were generated.
This suggests the issue may be environment-specific, dependent on additional factors (e.g., plugins, configuration), or not reproducible in a clean setup.

#4 @swissspaceboy
7 months ago

Hi,

Thanks for the reproduction test. Per further analysis, it is probably related to a shared plugin called "Easy Table of Contents".
I will write to the plugin author to get this fixed.

Many thanks for your help. Ticket can be closed.

Didier.

#5 @westonruter
7 months ago

  • Milestone Awaiting Review → 7.1
  • Version 6.9.4 → 4.7

It's still an issue, though, as core should be more resilient to how plugins are filtering the post object being inserted. I'm assuming this is due to the plugin filtering "rest_pre_insert_{$this->post_type}". It's clear that this is also closely related to code quality which would have been identified by implementing PHPStan static analysis (see #64898), as I can see many issues where assumptions are made about what types will be in WP_REST_Posts_Controller. So I'll make some type improvements and look at bypassing the unique slug check when there is no ID.

#6 @westonruter
7 months ago

  • Owner set to westonruter
  • Status new → accepted

#7 @westonruter
7 months ago

Oh, what's more, is that the existing logic is wrong even in the ideal case because $prepared_post->id shouldn't ever exist. The valid property is $prepared_post->ID because \WP_REST_Posts_Controller::prepare_item_for_database() returns a faux-WP_Post object, not a post object prepared for the REST API response.

I see this was also caught by @immeet94 in ​https://github.com/WordPress/wordpress-develop/pull/11327

Last edited 7 months ago by westonruter (previous) (diff)

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


7 months ago
#8

This primarily is for fixing calls to wp_unique_post_slug() in \WP_REST_Posts_Controller::create_item() and \WP_REST_Posts_Controller::update_item(). However, as part of that I also looked at WP_REST_Posts_Controller::prepare_item_for_database() and all methods that called it to ensure that they also had their static analysis issues addressed. I don't intend to commit this entire PR as-is. Subsets would be committed at a time, ideally with tests to catch the holes identified by static analysis

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

## Use of AI Tools

This ticket was mentioned in ​Slack in #core-test by ozgur_sar. ​View the logs.


7 months ago

#10 @nikunj8866
7 months ago

Reproduction Report

Description

❌ This report validates that the reported issue can not be reproduced.

Environment

  • WordPress: 6.9.4
  • PHP: 8.3.29
  • Server: Apache/2.4.43 (Win32) mod_fcgid/2.3.9a
  • Database: mysqli (Server: 10.6.23-MariaDB / Client: mysqlnd 8.3.29)
  • Browser: Chrome 146.0.0.0
  • OS: Windows 10/11
  • Theme: Twenty Twenty-Five 1.4
  • MU Plugins: None activated
  • Plugins:
    • Easy Table of Contents 2.0.82.1
    • Test Reports 1.2.1

Steps to Reproduce

  1. Enabled debug logging in wp-config.php:
    define('WP_DEBUG', true);
    define('WP_DEBUG_LOG', true);
    define('WP_DEBUG_DISPLAY', false);
    
  2. Installed and activated Easy Table of Contents 2.0.82.1.
  3. Enabled Table of Contents for Posts under Settings > Table of Contents.
  4. Created an Application Password via Users > Profile > Application Passwords.
  5. Sent the following POST request via Postman:
  6. Checked wp-content/debug.log for PHP warnings after the request.

Actual Results

❌ Error condition does not occur (not reproduced).

  • Post was created successfully (HTTP 201 response received in Postman).
  • No PHP warnings related to stdClass::$id or stdClass::$post_parent were found in wp-content/debug.log.

#11 @nikunj8866
7 months ago

  • Keywords needs-test-info added

#12 @nikunj8866
7 months ago

  • Keywords reporter-feedback added

#13 @softglaze
3 months ago

  • Keywords has-test-info added; needs-test-info removed

Reproduced on current trunk — with an important update: the plugin-filtered object no longer just emits warnings, it now breaks post updates via the REST API entirely.

Environment: Windows host, wordpress-develop Docker environment, current trunk (7.1-beta3-src), WP_DEBUG and WP_DEBUG_LOG enabled. Requests issued via rest_do_request() as an administrator (same controller path as an authenticated external request).

Reproduction: per comment:5, the warnings require a plugin filtering the prepared post object. I used this minimal mu-plugin to simulate one returning an incomplete stdClass:

<?php
add_filter( 'rest_pre_insert_post', function ( $prepared_post ) {
    $broken = new stdClass();
    foreach ( get_object_vars( $prepared_post ) as $key => $value ) {
        $broken->$key = $value;
    }
    unset( $broken->ID, $broken->post_parent );
    return $broken;
} );

Results on trunk:

Case Result
POST /wp/v2/posts (create), no mu-plugin 201, post created, no warnings — matches comment:4 and comment:6 (why clean-install attempts couldn't reproduce)
POST /wp/v2/posts (create), mu-plugin active 201, post created, no warnings — the create path currently tolerates the incomplete object
PUT /wp/v2/posts/{id} (update), no mu-plugin 200, update succeeds
PUT /wp/v2/posts/{id} (update), mu-plugin active 400 invalid_post "Invalid post ID." + PHP Warning: Undefined array key "ID" in wp-includes/post.php on line 5319 (also written to debug.log)

The filtered object flows into wp_update_post(), which can't find ID in the cast array, warns at post.php:5319, and the entire update request fails. So on current trunk the impact of a rest_pre_insert_{$post_type} filter returning a plain stdClass has escalated from PHP warnings (as originally reported on 6.9.4) to a hard REST failure — which supports the position in comment:5 that core should handle filtered objects defensively.

PR 11327 check: with the same mu-plugin active on the PR branch, the update fails identically (400 invalid_post, same warning at its older base's post.php:5273). The ->id → ->ID correction is valid but does not address the filtered-object fragility; the broader hardening in PR 11336 appears to be the fix that would. (Note: both PR branches are based on older trunk and would need a rebase.)

Happy to re-test PR 11336 once its conflicts are resolved.

This ticket was mentioned in ​Slack in #core-test by softglaze. ​View the logs.


3 months ago

#15 @vedantere
3 months ago

Trac #64921 — REST API POST /wp/v2/posts undefined property warnings — Patch Testing Report

Patch tested: PR #11327 (fix: id and parent error_log warning)

Environment: macOS, Local by Flywheel, WordPress 7.0.2, PHP 8.5.6, WP_DEBUG_LOG on. Used softglaze's mu-plugin technique (a rest_pre_insert_post filter returning a stripped stdClass missing ID/post_parent, simulating a plugin like Easy Table of Contents) since the bug isn't reproducible on a clean install without a misbehaving plugin in the loop — confirmed that independently, same as abcd95 and nikunj8866 found earlier in the ticket.

What the patch fixes (confirmed):

Before: POST /wp/v2/posts with a slug param and status: draft logs Undefined property: stdClass::$id and Undefined property: stdClass::$post_parent in class-wp-rest-posts-controller.php.

After patch: those two warnings are gone. This part of the fix works as intended.
What the patch does NOT fix (confirmed — corroborates softglaze's comment)

Independently reproduced the same escalation softglaze found on trunk, here on 7.0.2: with the mu-plugin active, PUT /wp/v2/posts/{id} (update) fails with 400 invalid_post "Invalid post ID." and logs Undefined array key "ID" in wp-includes/post.php — both before and after applying PR #11327.

This makes sense from the diff alone: PR #11327 only touches create_item()'s wp_unique_post_slug() call. It never touches update_item() or wp_update_post(), so it can't affect the update path at all.

#16 @softglaze
3 months ago

PR 11336 still has merge conflicts against current trunk (trunk was force-pushed ~3 weeks ago), so I haven't been able to test it — my offer in comment:13 stands once it's rebased.

In the meantime: comment:8 mentions wanting tests to catch the holes static analysis identified. I'm happy to write PHPUnit coverage for the filtered-object case if that's useful — the rest_pre_insert_{$post_type} filter returning a plain stdClass, asserting that update_item() survives it rather than failing with 400 invalid_post. That's the path that regressed between 6.9.4 and trunk, and it's the one my mu-plugin repro already exercises.

Let me know if you'd like that as a separate patch here, or whether you'd rather it come with the hardening subset.

@softglaze
2 months ago

PHPUnit coverage for create_item() with a slug on draft or pending status. Red on trunk, green with PR 11327.

@softglaze
2 months ago

PHPUnit coverage for update_item() when rest_pre_insert_post returns an object without ID (repro from comment:13). Red on trunk.

#17 @softglaze
2 months ago

  • Keywords has-unit-tests added

Following up on comment:16 — attaching PHPUnit coverage for both halves of this. Neither patch changes behaviour; both are tests only.

Environment for everything below:

WordPress   7.1-beta4-62899-src
trunk       ce2eb54348
PHP 8.3.32 · MariaDB 11.8.6 · PHPUnit 9.6.35

64921-tests-create-item.diff​ — the create path

Covers create_item() creating a draft or pending post that also carries a slug, which is the branch that reaches wp_unique_post_slug() and the $prepared_post->id access identified in comment:7. Two cases via data provider, draft and pending.

Nothing in tests/phpunit/tests/rest-api/rest-posts-controller.php currently enters that branch — the existing slug tests all run against a published post — so the suite stays green while the warnings occur. This is the same combination vedantere confirmed manually in comment:15; this just puts it in the suite.

On trunk (line 766 in 6.9.4 is line 769 here):

1) WP_Test_REST_Posts_Controller::test_create_item_with_slug_and_unpublished_status with data set "draft status" ('draft')
Undefined property: stdClass::$id

/var/www/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php:769
/var/www/src/wp-includes/rest-api/class-wp-rest-server.php:1293
/var/www/tests/phpunit/includes/spy-rest-server.php:71

Tests: 2, Assertions: 0, Errors: 2.

Identical for the pending data set. phpunit.xml.dist sets convertWarningsToExceptions="true", so this is a failure rather than log noise.

With the two-line change from PR 11327 applied:

OK (2 tests, 6 assertions)

Full class, unchanged:

npm run test:php -- --filter WP_Test_REST_Posts_Controller
OK (283 tests, 2638 assertions)

64921-tests-update-item-filtered-object.diff​ — the filtered-object case from comment:13

This is the coverage I offered in comment:16. The filter callback is the mu-plugin from comment:13 rewritten as a test fixture: it rebuilds the prepared post without ID or post_parent, then asserts that update_item() completes and writes to the post named in the request.

On trunk:

1) WP_Test_REST_Posts_Controller::test_update_item_with_filtered_post_object_missing_id
Undefined array key "ID"

/var/www/src/wp-includes/post.php:5335
/var/www/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php:980
/var/www/src/wp-includes/rest-api/class-wp-rest-server.php:1293
/var/www/tests/phpunit/includes/spy-rest-server.php:71

Tests: 1, Assertions: 0, Errors: 1.

Same failure as comment:13 — post.php was line 5319 on beta3, 5335 here.

Note on PR 11336

Still can't apply it because of the conflicts, so this is not a test result. But the trace above enters wp_update_post() from class-wp-rest-posts-controller.php:980, which is wp_update_post( wp_slash( (array) $post ), true, false ) — and that line does not appear in PR 11336's diff at all. The PR changes the update_item() slug branch above it to $post->ID ?? 0, which removes the undefined property access, but the array cast on line 980 still drops the key and get_post( null ) still returns the 400. So the second patch looks like it would stay red until update_item() re-asserts the ID from the request. Happy to confirm once 11336 is rebased.

Both patches are red on trunk by design and are meant to land with their respective fixes rather than before them. Offered for the subsets described in comment:8, not as alternatives to either PR.

​@softglaze commented on ​PR #11327:


2 months ago
#18

Attached PHPUnit coverage for this on the ticket (64921-tests-create-item.diff). It covers create_item() with a slug on a draft or pending post — the branch that reaches line 769. Nothing in rest-posts-controller.php currently enters it, since the existing slug tests all run against a published post, which is why the suite stays green while the warnings occur.

Against trunk at ce2eb54348: red with Undefined property: stdClass::$id, green with this change, and OK (283 tests, 2638 assertions) for the full controller class.

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


2 months ago

#20 @adrianduffell
2 months ago

  • Keywords early added
  • Milestone 7.1 → Future Release

This was discussed in today's bug scrub. It looks close but I’d like to punt it since RC 1 is due in a few days time. It looks valuable to fix soon, so keeping the early tag for consideration in 7.2.

Note: See TracTickets for help on using tickets.