No Code Attached Yet Webservices
avatar dmlwebal
dmlwebal
11 Sep 2026

Summary

PATCH /api/index.php/v1/menus/site/items/{id} returns a 500 (Error loading form file) for every existing menu item, regardless of item type, unless the request payload explicitly re-sends "type" at the top level. Without it, ItemModel::preprocessForm() builds a malformed form filename and Form::loadFile() throws.

This is distinct from #44119 (merged 2024-09-25), which fixed component params not saving via the API for the create path. That fix does not cover editing an existing item via PATCH — the failure reported here happens before any params are even processed, at form-load time.

Environment

  • Joomla 5.4.8 (confirmed via libraries/src/Version.php)
  • Web Services API (com_menus), PHP 8.x, Apache/cPanel hosting
  • Confirmed the same code path (getUserState/getInput fallback in preprocessForm()) is unchanged on current 5.4-dev (commit e08f97c4) — not a 5.4.8 regression, a pre-existing gap

Steps to Reproduce

  1. Create/identify any existing menu item (any typecomponent, url, alias, etc.)
  2. PATCH /api/index.php/v1/menus/site/items/{id} with a Bearer token, payload containing only an unrelated field, e.g.:
    { "params": { "pageclass_sfx": "my-page-class" } }
  3. Observe: 500 Internal server error

Actual Result

{"errors":[{"code":500,"title":"Internal server error","detail":"Exception: Error loading form file in .../administrator/components/com_menus/src/Model/ItemModel.php:1249\nStack trace:\n#0 .../libraries/src/MVC/Model/FormBehaviorTrait.php(115): Joomla\\Component\\Menus\\Administrator\\Model\\ItemModel->preprocessForm()\n#1 .../administrator/components/com_menus/src/Model/ItemModel.php(526): Joomla\\CMS\\MVC\\Model\\FormModel->loadForm()\n#2 .../libraries/src/MVC/Controller/ApiController.php(448): Joomla\\Component\\Menus\\Administrator\\Model\\ItemModel->getForm()\n#3 .../libraries/src/MVC/Controller/ApiController.php(390): Joomla\\CMS\\MVC\\Controller\\ApiController->save()\n#4 .../api/components/com_menus/src/Controller/ItemsController.php(131): Joomla\\CMS\\MVC\\Controller\\ApiController->edit()\n..."}]}

Reproduced identically across three unrelated menu item types on the same install (com_gantry5 custom view, com_users login, and a third-party component view) — same trace, same line, every time. Not payload-shape dependent beyond the missing type field.

Side-effect: each failed attempt still checks the item out (checked_out / checked_out_time advance in the DB) before throwing, so repeated attempts leave the item locked without any indication in the API response that this happened.

Root Cause

administrator/components/com_menus/src/Model/ItemModel.php, preprocessForm():

if (!($type = $app->getUserState('com_menus.edit.item.type'))) {
    $type = $app->getInput()->get('type');

    /**
     * Note: a new menu item will have no field type.
     * The field is required so the user has to change it.
     */
}

$this->setState('item.type', $type);

getUserState('com_menus.edit.item.type') reads PHP session state that is only ever populated by the backend admin UI's edit screen (loading index.php?option=com_menus&task=item.edit&id=... sets this before the form is ever saved). The Web Services API is stateless and never touches this session key, so it always falls through to $app->getInput()->get('type') — but the API request body's type field is only read into the input object when the caller explicitly includes it. A PATCH that only intends to change an unrelated field (e.g. pageclass_sfx) has no reason to think it needs to echo type back, and gets no indication from the API schema/docs that it's required.

Further down, $typeFile = 'item_' . $type resolves to 'item_' (empty suffix) when $type is falsy, Path::find() fails to locate item_.xml, and Form::loadFile() throws JERROR_LOADFILE_FAILED.

The inline comment ("a new menu item will have no field type... required so the user has to change it") makes clear this fallback was designed around the create-new-item, admin-UI flow, where the type genuinely isn't known yet and the UI prompts for it. It doesn't account for the edit-existing-item-via-API flow, where type is already known (it's on the existing row) but never gets echoed back into the request unless the caller happens to know they need to.

Workaround (confirmed working)

Always include "type" explicitly at the top level of the PATCH payload, echoing the item's existing type, even when unrelated to the field(s) actually being changed:

{
  "type": "component",
  "params": { "pageclass_sfx": "my-page-class" }
}

This returns 200 and the change persists correctly (verified via independent re-fetch, not just the PATCH response echo).

Suggested Fix

preprocessForm() (or getForm() upstream of it) should fall back to the menu item's own DB-loaded type value (available via $this->getItem($pk) / the loaded Table object) when both getUserState() and getInput()->get('type') come back empty and a primary key is present — i.e. only require an explicit type on the payload when actually creating a new item (pk == 0), not when editing an existing one where the type is already on record. Happy to test a patch against this staging environment if useful.

Credit

Root cause traced and workaround verified by Claude (Anthropic), via live debug-mode stack trace analysis and reproduction across multiple menu item types on a real Joomla 5.4.8 install, during an AI-assisted development session.

avatar dmlwebal dmlwebal - open - 11 Sep 2026
avatar joomla-cms-bot joomla-cms-bot - change - 11 Sep 2026
Labels Added: No Code Attached Yet
avatar joomla-cms-bot joomla-cms-bot - labeled - 11 Sep 2026
avatar dmlwebal
dmlwebal - comment - 11 Sep 2026

Patch — verified live against a real Joomla 5.4.8 install

Tested the fix below against the reproducing environment from the issue: uploaded the patched ItemModel.php over the live core file on staging, retried the exact PATCH that previously 500'd ({"params":{"pageclass_sfx":"..."}}, no type field), confirmed 200 + persisted correctly via an independent re-fetch, then reverted the core file back to stock and confirmed the 500 reproduces again unmodified. So this isn't just lint-clean, it's round-tripped through a real failing → passing → failing-again cycle on the actual bug.

The fix falls back to the item's own already-loaded DB row ($table, loaded a few lines above in the same method for client_id) when both getUserState() and the request input come back empty and a primary key is present — i.e. it only changes behavior for the edit-existing-item case the original comment never accounted for, leaving the create-new-item path (where $table isn't loaded and $pk is falsy) untouched.

--- a/administrator/components/com_menus/src/Model/ItemModel.php
+++ b/administrator/components/com_menus/src/Model/ItemModel.php
@@ -1011,7 +1011,18 @@
             /**
              * Note: a new menu item will have no field type.
              * The field is required so the user has to change it.
+             *
+             * When editing an EXISTING item (pk is set) over the stateless Web Services
+             * API, neither the user state (never populated outside the backend edit
+             * screen) nor the request input (only present if the caller explicitly
+             * echoes "type" back) will have this value, even though it is already known
+             * — it's on the row we just loaded above. Fall back to that before giving up,
+             * so an API PATCH that only intends to change an unrelated field (e.g.
+             * pageclass_sfx) doesn't have to know to resend "type" as well.
              */
+            if (!$type && $pk && isset($table) && $table->type) {
+                $type = $table->type;
+            }
         }
 
         $this->setState('item.type', $type);

Happy to open this as a PR against 5.4-dev if that's preferred over a diff in the issue thread — let me know the convention this repo wants (didn't want to assume without checking CONTRIBUTING.md first).

— Claude

avatar alikon alikon - change - 13 Sep 2026
Labels Added: Webservices
avatar alikon alikon - labeled - 13 Sep 2026

Add a Comment

Login with GitHub to post a comment