Description
Deleting a category from Site Settings → Categories fails on sites that use the wordpress-rs taxonomy path, and the confirmation dialog is left spinning on "Deleting category" forever.
Step-by-step reproduction instructions
- Sign in to an Atomic or self-hosted site whose taxonomy requests go through wordpress-rs (site added with an application password,
taxonomies_rest_api_migration enabled in the dev menu).
- Go to Site Settings → Categories.
- Create a new category. It is created successfully — the
POST /wp/v2/sites/<id>/categories response carries the new term, e.g. "id": 1447.
- Open that category and delete it.
Expected: the category is deleted, or a "Failed to delete category" message appears.
Actual: DELETE /wp/v2/sites/<id>/categories/631?force=true returns 404 rest_term_invalid ("Term does not exist.") — note the id does not match the one returned when the category was created — and the "Deleting category" spinner stays on screen indefinitely.
Screenshots, screen recording, code snippet
POST .../wp/v2/sites/<id>/categories 201 → { "id": 1447, "count": 0, ... }
GET .../wp/v2/sites/<id>/categories 200
DELETE .../wp/v2/sites/<id>/categories/631?force=true 404
^^^
{
"code": "rest_term_invalid",
"message": "Term does not exist.",
"data": { "status": 404 }
}
Environment info
Atomic site, categories served over the wordpress-rs path (taxonomies_rest_api_migration enabled). Both defects are present on trunk.
Please confirm that you have searched existing issues in the repo.
Technical findings
There are two independent defects, both introduced in #22252 (CMM-808, the wordpress-rs taxonomy port).
1. The delete request sends the local database id instead of the remote term id.
TaxonomyRsApiRestClient.deleteTerm identifies the term with term.id:
requestBuilder.terms().delete(
termEndpointType = termEndpointType,
termId = term.id.toLong() // local DB primary key
)
TermModel carries two ids — mId, the local @PrimaryKey row id, and mRemoteTermId, the id the REST API knows. Every sibling client uses the remote identity: TaxonomyXMLRPCClient uses getRemoteTermId(), TaxonomyRestClient uses getSlug(), and updateTerm in this very same class uses term.remoteTermId. Only deleteTerm uses the local one, so the API is asked to delete a term id that belongs to a different term (or to nothing at all) and answers 404 rest_term_invalid.
Creating a category works because creation does not need an id.
The same function has a mirror-image slip in its success path: the confirmation TermModel is built passing term.id.toLong() as the remoteTermId constructor argument, so even a delete that succeeded would hand the store a model whose remote id is really a local row id.
2. A failed deletion emits an event that nothing listens for.
TaxonomyStore reports the two outcomes with different causeOfChange values:
- success →
removeTerm() emits OnTaxonomyChanged(causeOfChange = REMOVE_TERM)
- failure → emits
OnTaxonomyChanged(causeOfChange = DELETE_TERM)
Every consumer handles only REMOVE_TERM — CategoryDetailViewModel, CategoriesListViewModel and SiteSettingsTagListActivity. Nothing subscribes to DELETE_TERM, so the failure is emitted into the void and the InProgress(R.string.deleting_cat) state that CategoryDetailViewModel.deleteCategory() posted is never cleared. The dialog spins indefinitely and the user is never told the deletion failed.
This second defect is client-agnostic: any failed term deletion hangs the dialog, including on WordPress.com and XML-RPC sites. The wrong id simply guarantees the failure every time on the wordpress-rs path.
Description
Deleting a category from Site Settings → Categories fails on sites that use the wordpress-rs taxonomy path, and the confirmation dialog is left spinning on "Deleting category" forever.
Step-by-step reproduction instructions
taxonomies_rest_api_migrationenabled in the dev menu).POST /wp/v2/sites/<id>/categoriesresponse carries the new term, e.g."id": 1447.Expected: the category is deleted, or a "Failed to delete category" message appears.
Actual:
DELETE /wp/v2/sites/<id>/categories/631?force=truereturns404 rest_term_invalid("Term does not exist.") — note the id does not match the one returned when the category was created — and the "Deleting category" spinner stays on screen indefinitely.Screenshots, screen recording, code snippet
Environment info
Atomic site, categories served over the wordpress-rs path (
taxonomies_rest_api_migrationenabled). Both defects are present ontrunk.Please confirm that you have searched existing issues in the repo.
Technical findings
There are two independent defects, both introduced in #22252 (CMM-808, the wordpress-rs taxonomy port).
1. The delete request sends the local database id instead of the remote term id.
TaxonomyRsApiRestClient.deleteTermidentifies the term withterm.id:requestBuilder.terms().delete( termEndpointType = termEndpointType, termId = term.id.toLong() // local DB primary key )TermModelcarries two ids —mId, the local@PrimaryKeyrow id, andmRemoteTermId, the id the REST API knows. Every sibling client uses the remote identity:TaxonomyXMLRPCClientusesgetRemoteTermId(),TaxonomyRestClientusesgetSlug(), andupdateTermin this very same class usesterm.remoteTermId. OnlydeleteTermuses the local one, so the API is asked to delete a term id that belongs to a different term (or to nothing at all) and answers404 rest_term_invalid.Creating a category works because creation does not need an id.
The same function has a mirror-image slip in its success path: the confirmation
TermModelis built passingterm.id.toLong()as the remoteTermId constructor argument, so even a delete that succeeded would hand the store a model whose remote id is really a local row id.2. A failed deletion emits an event that nothing listens for.
TaxonomyStorereports the two outcomes with differentcauseOfChangevalues:removeTerm()emitsOnTaxonomyChanged(causeOfChange = REMOVE_TERM)OnTaxonomyChanged(causeOfChange = DELETE_TERM)Every consumer handles only
REMOVE_TERM—CategoryDetailViewModel,CategoriesListViewModelandSiteSettingsTagListActivity. Nothing subscribes toDELETE_TERM, so the failure is emitted into the void and theInProgress(R.string.deleting_cat)state thatCategoryDetailViewModel.deleteCategory()posted is never cleared. The dialog spins indefinitely and the user is never told the deletion failed.This second defect is client-agnostic: any failed term deletion hangs the dialog, including on WordPress.com and XML-RPC sites. The wrong id simply guarantees the failure every time on the wordpress-rs path.