User tests: Successful: Unsuccessful:
Route::() retrieves the router for the current application and calls build() on it. The ApiRouter and the (unregistered) CLI router extend Joomla\Router\Router, which is parse-only and does not implement build(), so any Route::() call reached from an API endpoint or a console command crashes with "Call to undefined method Joomla\CMS\Router\ApiRouter::build()".
In practice this is hit whenever a component generates a frontend URL from code reached via webservice plugins or scheduled CLI tasks (emails with article links, notifications, etc.).
Detect those two clients in Route::_() and build the URL through the site router instead. Route::link('api', ...) / Route::link('cli', ...) is left untouched, so explicit calls continue to fail exactly as they do today and the fix stays scoped to the convenience dispatcher.
Pull Request resolves # .
Route::_() builds a URL through the router of the currently-active application. The ApiApplication and the ConsoleApplication both use routers that extend Joomla\Router\Router (the framework router), which is parse-only and does not implement build(). As a result, any Route::_() call reached from an API endpoint or a CLI command crashes with:
Error: Call to undefined method Joomla\CMS\Router\ApiRouter::build()
at libraries/src/Router/Route.php:150
In practice this breaks every extension that generates frontend URLs from code reachable via a webservice plugin or a scheduled console task. A common symptom is emails (order notifications, contact forms, etc.) that embed Route::_()-built links in their body.
The fix detects the api and cli clients inside Route::_() and routes through the site router instead. Route::link('api', ...) / Route::link('cli', ...) are left unchanged: an explicit call to a parse-only router still fails exactly as before, so the convenience dispatcher gets a sensible fallback while explicit requests keep their current semantics.
api/index.php does and then calls Route::_() on a standard com_content URL.<?php
// reproduce.php — run with: php reproduce.php
$joomlaBase = str_replace('/', DIRECTORY_SEPARATOR, '/path/to/your/joomla');
define('_JEXEC', 1);
define('JPATH_BASE', $joomlaBase . DIRECTORY_SEPARATOR . 'api');
define('JDEBUG', false);
require_once JPATH_BASE . '/includes/defines.php';
require_once JPATH_BASE . '/includes/framework.php';
$container = Joomla\CMS\Factory::getContainer();
$container->alias('session', 'session.cli')
->alias('JSession', 'session.cli')
->alias(Joomla\CMS\Session\Session::class, 'session.cli')
->alias(Joomla\Session\Session::class, 'session.cli')
->alias(Joomla\Session\SessionInterface::class, 'session.cli');
$app = $container->get(Joomla\CMS\Application\ApiApplication::class);
Joomla\CMS\Factory::$application = $app;
$app->bootComponent('com_content');
try {
$url = Joomla\CMS\Router\Route::_('index.php?option=com_content&view=article&id=1');
echo "OK: $url\n";
} catch (Throwable $e) {
echo "FAIL: " . get_class($e) . ' - ' . $e->getMessage() . "\n";
}ApiRouter::build() crash.Route::_() returns a real site URL (e.g. /index.php/component/content/article/1).Tested on Joomla 5.2.2 and 6.1.0.
FAIL: Error - Call to undefined method Joomla\CMS\Router\ApiRouter::build()
OK: /index.php/component/content/article/1
Please select:
| Status | New | ⇒ | Pending |
| Category | ⇒ | Libraries |