085 —Joomla
Joomla 3 saves 500ing: the missing #__assets row, found in 40 minutes
A Joomla 3 site started throwing 500s the moment an editor hit Save. The cause was a single missing row in #__assets. Here is the trace, with SQL.
The Loom landed at 07:14 on a Tuesday. A Dutch agency we work with had a client on Joomla 3.10, a regional events publisher, and every save on every article was returning a 500. Not a flash of red in the admin, not a validation message: a hard Apache error page with the wrong charset, the kind that makes editors call the agency owner directly. The editor had been trying to publish a piece about a town fair for an hour. The agency lead had been awake for twenty minutes and had already tried the two things you try first: clearing the Joomla cache and restarting PHP-FPM. Neither moved the needle.
What follows is the actual trace, written down because Joomla 3 is going to keep doing this to people. Extended security support ended in August 2023, and the PHP versions these sites usually run on are mostly out of support too. The combination produces a very specific class of Joomla 3 incident where the error message is uninformative and the cause is structural.
The first ten minutes: read what the server is actually saying
The Joomla admin showed a generic 0 - Save failed with the following error: -1 banner, which is the kind of error code that exists to ruin afternoons. That number is not an HTTP code and not a MySQL code. It comes from the nested-set logic inside JTableNested, and on its own it tells you almost nothing.
The first useful move was to stop looking at the browser and start looking at the box. The agency had SSH on this host, so we tailed two things in parallel:
tail -f /var/log/apache2/error.log
tail -f /var/www/example/administrator/logs/error.php
Joomla writes its own log under administrator/logs/ when log_path is set in configuration.php, and it is almost always more useful than the Apache log for application errors. Within one save attempt the Joomla log produced the line that broke the case open:
PHP Fatal error: Uncaught RuntimeException: Left-Right data inconsistency.
Node has no valid parent. in /var/www/example/libraries/joomla/table/nested.php:760
That is the real signature. JTableNested is the class behind anything in Joomla that uses nested sets: categories, menus, and the #__assets table that backs the permission system. When it cannot find a node's parent during a rebuild, it throws and the save fails. The 500 was just the catch-all wrapper around it.
Nested sets and the #__assets table
If you have only worked on WordPress, the #__assets table is worth understanding because Joomla puts a lot of its ACL weight on it. Every component, category and item has a corresponding row in #__assets that stores its position in a permissions tree using lft and rgt columns. When you save an article in Joomla 3, the save path roughly does this:
- Look up the parent asset for this article (usually the category's asset, or the component root).
- Insert or update the article's row in
#__assets, computing newlftandrgtvalues relative to the parent. - Insert or update the article in
#__contentand linkasset_idback.
Step one is where this site was dying. The category the editor had picked, "Evenementen," had been created two years earlier by an admin who has since left. Somewhere along the way, almost certainly during a botched extension uninstall, the category's row in #__assets had been deleted but the row in #__categories still pointed at the now-missing asset_id. Joomla looked up the parent asset, got nothing back, and the nested-set rebuild threw.
The 20-minute trace: confirming the missing row
From the error we already had two strong hypotheses: a broken nested-set tree, or a dangling asset_id. Both are checkable with three short queries. Here is the sequence we actually ran, against the live database with a fresh backup taken first.
-- 1. Find any category whose asset_id does not exist in #__assets
SELECT c.id, c.title, c.asset_id
FROM jos_categories c
LEFT JOIN jos_assets a ON a.id = c.asset_id
WHERE c.asset_id > 0
AND a.id IS NULL;
This returned exactly one row: category 47, "Evenementen", with asset_id = 312, and no matching row in jos_assets. That confirmed the dangling reference.
-- 2. Verify the nested-set tree is still consistent for the rest of #__assets
SELECT id, name, lft, rgt
FROM jos_assets
WHERE rgt < lft
OR lft < 0
OR rgt < 0;
Empty result set. The tree itself was fine. The corruption was scoped to one missing leaf, which is the cheap version of this problem.
-- 3. Find the parent we want to graft under (the com_content root asset)
SELECT id, name, lft, rgt
FROM jos_assets
WHERE name = 'com_content';
That gave us id = 8, with usable lft and rgt bounds. At this point the diagnosis was complete and the fix was a structural one: rebuild category 47's asset row under com_content and re-point asset_id.
The fix: rebuild the asset, then let Joomla do the rest
There is a temptation to hand-craft an INSERT into #__assets with calculated lft and rgt values. Do not do this. The safer route is to insert a placeholder row, point the category at it, and then let Joomla rebuild the tree from the admin, which is a documented operation in the Joomla docs.
-- Insert a minimal asset row owned by com_content (parent id 8)
INSERT INTO jos_assets
(parent_id, lft, rgt, level, name, title, rules)
VALUES
(8, 0, 0, 1, 'com_content.category.47', 'Evenementen', '{}');
-- Re-point the category at the new asset row
UPDATE jos_categories
SET asset_id = LAST_INSERT_ID()
WHERE id = 47;
Then, from the Joomla admin, System → Global Configuration → Permissions, hit Save once. Joomla detects the dirty lft/rgt values and rebuilds the nested set for the whole #__assets table. After that one save, the editor went back in, tried the article from earlier, and it saved cleanly on the first attempt. From the Loom landing to the green save banner was forty minutes, most of it spent reading logs and running the dump.
Maintenance habits for Joomla 3 sites
Three things to take away, because this is not a one-off pattern.
First, on any legacy site still on Joomla 3, run the dangling-asset_id query above as a routine health check. It takes milliseconds and it catches problems before an editor does. A weekly cron that emails the agency owner if the result set is non-empty is fifteen minutes of work and several Tuesday mornings saved.
Second, JTableNested errors almost always trace back to #__assets, #__categories, or #__menu. When you see "Left-Right data inconsistency" in a log, you are looking at a nested-set table somewhere, and the fix pattern is the same: find the broken row, decide whether to delete or graft, then let the admin rebuild.
Third, every change you make to these tables should be reversible. We were able to move quickly because the pre-fix mysqldump was sitting on the box and we could have rolled it back in under a minute if the rebuild had misbehaved.
When we built Pier, this incident shape was one of the ones we kept hitting on customer sites, so the way we handled it was to make every database write through the MySQL editor snapshot the affected rows first and write them to version history, the same way file edits are versioned. The fix above would still be the fix; the difference is that the rollback is a click instead of a mysql < dump.sql.
If you maintain a Joomla 3 site today, open a database client and run the dangling-asset_id query against it before lunch. If it returns a row, you have just found a future 07:14 Loom.
— Questions —
What does the Joomla error 'Left-Right data inconsistency' actually mean?
It means a nested-set table (usually #__assets, #__categories or #__menu) has a row whose parent cannot be located, so JTableNested cannot recompute lft and rgt values during save.
Is it safe to edit #__assets directly in MySQL?
Only with a full database dump taken first, and only by inserting placeholder rows and letting Joomla rebuild the tree from the admin. Hand-calculating lft and rgt is how people corrupt the whole table.
Does this happen on Joomla 4 and Joomla 5 as well?
The underlying nested-set model is similar, but the asset rebuild is more defensive in newer versions. The dangling asset_id pattern is mostly a Joomla 3 problem caused by old extension uninstalls.
How do I rebuild the #__assets table from the Joomla admin?
Go to System, then Global Configuration, then the Permissions tab, and hit Save. Joomla detects dirty lft and rgt values and rebuilds the nested set for the whole table.
What is the quickest health check I can run today?
A LEFT JOIN from #__categories to #__assets on asset_id, filtering for NULL on the assets side. Any row returned is a category whose save will fail.