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 )
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/postswithbody: {"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)
Change History (22)
This ticket was mentioned in PR #11327 on WordPress/wordpress-develop by @immeet94.
7 months ago
#1
- Keywords has-patch added
#3
@
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
@
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
@
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.
#7
@
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
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
@
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
- Enabled debug logging in wp-config.php:
define('WP_DEBUG', true); define('WP_DEBUG_LOG', true); define('WP_DEBUG_DISPLAY', false); - Installed and activated Easy Table of Contents 2.0.82.1.
- Enabled Table of Contents for Posts under Settings > Table of Contents.
- Created an Application Password via Users > Profile > Application Passwords.
- Sent the following POST request via Postman:
- URL: http://yoursite.com/wp-json/wp/v2/posts
- Method: POST
- Authorization: Basic Auth (username + application password)
- Body (raw JSON):
{ "title": "Test Post", "content": "<p>Hello</p>", "status": "draft" }
- 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.
#13
@
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
@
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
@
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.
@
2 months ago
PHPUnit coverage for create_item() with a slug on draft or pending status. Red on trunk, green with PR 11327.
@
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
@
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.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)
Trac ticket: https://core.trac.wordpress.org/ticket/64921
## Use of AI Tools