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');
// Under CLI, $_SERVER has no request context, so the ApiApplication constructor
// (Uri::getInstance) throws "Could not parse the requested URI". Fake a request.
$_SERVER['HTTP_HOST'] = 'localhost';
$_SERVER['REQUEST_URI'] = '/api/index.php';
$_SERVER['SCRIPT_NAME'] = '/api/index.php';
$_SERVER['PHP_SELF'] = '/api/index.php';
$_SERVER['REQUEST_METHOD'] = 'GET';
define('_JEXEC', 1);
define('JPATH_BASE', $joomlaBase . DIRECTORY_SEPARATOR . 'api');
if (!defined('JDEBUG')) 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 = Joomla\CMS\Factory::$application = $container->get(Joomla\CMS\Application\ApiApplication::class);
// A real request registers the extension namespaces in CMSApplication::execute(),
// which this script never calls. Without this line any component boot fails with
// "Class Joomla\Component\Content\Administrator\Helper\AssociationsHelper not found",
// with or without this patch.
$app->createExtensionNamespaceMap();
// getName() === 'api', so Route::_() resolves the 'api' client to ApiRouter,
// which extends the parse-only framework Router and has no build().
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.Uri::root() has no real request to work from; a real API request does not have that.Tested on Joomla 5.2.2, 5.4.5 and 6.1.0.
FAIL: Error - Call to undefined method Joomla\CMS\Router\ApiRouter::build()
OK: /index.php/index.php/component/content/article/1
The exact path depends on the SEF settings of the site. The doubled prefix comes from Uri::root() having no real request to work from under the CLI SAPI; an API request served over HTTP builds the same URL as the site application does.
Please select:
| Status | New | ⇒ | Pending |
| Category | ⇒ | Libraries |
@exlemor Thanks for testing. The issue is the repro script, not the bug: under a plain CLI run $_SERVER has no request context, so the ApiApplication constructor (Uri::getInstance) throws Could not parse the requested URI http:///.../reproduce.php and the script dies before it ever reaches the Route::_() call. Depending on the PHP CLI setup you can also hit an autoload error on the bootComponent() line. Either way you never get to the failing line, so the BEFORE result looks like it "does not match".
Here is a hardened version that fakes a request context so the API app boots under CLI, and drops the bootComponent() call which is not needed to trigger the crash:
<?php
// reproduce.php — run with: php reproduce.php
$joomlaBase = str_replace('/', DIRECTORY_SEPARATOR, '/path/to/your/joomla');
// Under CLI, $_SERVER has no request context, so the ApiApplication constructor
// (Uri::getInstance) throws "Could not parse the requested URI". Fake a request.
$_SERVER['HTTP_HOST'] = 'localhost';
$_SERVER['REQUEST_URI'] = '/api/index.php';
$_SERVER['SCRIPT_NAME'] = '/api/index.php';
$_SERVER['PHP_SELF'] = '/api/index.php';
$_SERVER['REQUEST_METHOD'] = 'GET';
define('_JEXEC', 1);
define('JPATH_BASE', $joomlaBase . DIRECTORY_SEPARATOR . 'api');
if (!defined('JDEBUG')) 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;
// getName() === 'api', so Route::_() resolves the 'api' client to ApiRouter,
// which extends the parse-only framework Router and has no build().
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";
}Set $joomlaBase to your install path and run php reproduce.php.
BEFORE the patch:
FAIL: Error - Call to undefined method Joomla\CMS\Router\ApiRouter::build()
AFTER the patch:
OK: /index.php/component/content/article/1
I just re-confirmed the BEFORE crash on a clean Joomla 6.1 install with this script.
For reference, the crash is deterministic: Route::link('api', ...) resolves the container service ApiRouter, which extends Joomla\Router\Router (parse-only, no build()), so the build() call is always undefined from the api/cli clients. The patch reroutes those two clients through the site router, which does implement build().
I have tested this item ✅ successfully on 3357705
I had the same error "Call to undefined method Joomla\CMS\Router\ApiRouter::build()" when creating a API for component JoomGalley. (Still a PR there). After applyiong the PR this error was gone
I have tested this item ✅ successfully on 3357705
I had the same error "Call to undefined method Joomla\CMS\Router\ApiRouter::build()" when creating a API for component JoomGalley. (Still a PR there). After applyiong the PR this error was gone
I have tested this item ✅ successfully on 3357705
I had the same error "Call to undefined method Joomla\CMS\Router\ApiRouter::build()" when creating a API for component JoomGalley. (Still a PR there). After applyiong the PR this error was gone. Did test with an API call
I have tested this item ✅ successfully on 3357705
I had the same error "Call to undefined method Joomla\CMS\Router\ApiRouter::build()" when creating a API for component JoomGalley. (Still a PR there). After applying the PR this error was gone. Did test with an API call
| Labels |
Added:
PR-5.4-dev
|
||
@hikashop-nicolas Please don't update your branch if not really necessary e.g. due to conflicts. The branch update has invalidated the human test counter in our issue tracker, and I have to restore @ThomasFinnern 's test now so it is counted again.
@hikashop-nicolas I tried to give this PR a second test. Used JBT inside jbt-54 container with the second hardened source code version and configured:
$joomlaBase = str_replace('/', DIRECTORY_SEPARATOR, '/var/www/html');
$_SERVER['HTTP_HOST'] = 'host.docker.internal:7054';
Before PR I am getting the
FAIL: Error - Call to undefined method Joomla\CMS\Router\ApiRouter::build()
But, after applying the PR I am getting
FAIL: Error - Class "Joomla\Component\Content\Administrator\Helper\AssociationsHelper" not found
It is a current 5.4-dev branch installation and file ./administrator/components/com_content/src/Helper/AssociationsHelper.php exists.
Tried to delete and recreated administrator/cache/autoload_psr4.phpwithout success.
Also tried to call from Docker host system (outside container) with same result. Can you give advice how to get a successful test? Or if to complicated, should I try with local Joomla installation?
@muhme Thanks for testing. This one is the repro script, not the patch: I reproduced your exact output on a Joomla 5.4.5 install and tracked it down.
Extension namespaces are registered by createExtensionNamespaceMap(), which is called from CMSApplication::execute(). The script only constructs the ApiApplication, it never executes it, so no extension namespace is registered at all. Before the patch you crash in ApiRouter::build() before anything else can happen. With the patch you get past that, the site router boots com_content, and administrator/components/com_content/services/provider.php does new AssociationsHelper(), which PHP then cannot autoload. You get the very same class error without the patch if you add $app->bootComponent('com_content'); to the script (the line my first version had, which is also what @exlemor hit in April), so it is independent of the change.
Two ways to get a successful test.
1. One more line in the script, right after Joomla\CMS\Factory::$application = $app;:
$app->createExtensionNamespaceMap();On 5.4.5 that gives FAIL: Error - Call to undefined method Joomla\CMS\Router\ApiRouter::build() before the patch and OK: ... after it. The URL you get from the script carries a doubled path prefix (/index.php/index.php/...); that is Uri::root() under the CLI SAPI with a faked $_SERVER, not something the patch does. A real request does not have it, which is why I would rather you run the second test.
2. Through a real API request. Create plugins/system/routetest/routetest.php:
<?php
defined('_JEXEC') or die;
class PlgSystemRoutetest extends \Joomla\CMS\Plugin\CMSPlugin
{
public function onAfterInitialise()
{
$app = \Joomla\CMS\Factory::getApplication();
$line = date('c') . ' client=' . $app->getName() . ' ';
try {
$line .= 'OK: ' . \Joomla\CMS\Router\Route::_('index.php?option=com_content&view=article&id=1');
} catch (\Throwable $e) {
$line .= 'FAIL: ' . get_class($e) . ' - ' . $e->getMessage();
}
file_put_contents(JPATH_ROOT . '/routetest.log', $line . "\n", FILE_APPEND);
}
}with a minimal routetest.xml:
<?xml version="1.0" encoding="utf-8"?>
<extension type="plugin" group="system" method="upgrade">
<name>plg_system_routetest</name>
<version>1.0.0</version>
<files>
<filename plugin="routetest">routetest.php</filename>
</files>
</extension>Install and enable it, then call any API URL and one frontend URL. The 401 of an unauthenticated API call is fine, the plugin runs before authentication:
curl -s -o /dev/null http://your-site/api/index.php/v1/content/articles
curl -s -o /dev/null http://your-site/
tail -2 routetest.log
Here on 5.4.5, without the patch:
client=api FAIL: Error - Call to undefined method Joomla\CMS\Router\ApiRouter::build()
client=site OK: /joomla5_route/index.php/pricedis4-test
and with the patch:
client=api OK: /joomla5_route/index.php/pricedis4-test
client=site OK: /joomla5_route/index.php/pricedis4-test
So an API request now produces exactly the URL the site application produces, which is the point of the change.
For the console half: before the patch Route::_() returns an empty string there (the RuntimeException for the missing cli router is swallowed inside Route::_()), after it you get the correct URL.
I am updating the testing instructions in the PR description with the corrected script so nobody else runs into this.
I am updating the testing instructions in the PR description with the corrected script
@hikashop-nicolas thank you 👍 the updated script is working
I have tested this item ✅ successfully on c9ee1b3
Tested with JBT, current 5.4-dev branch, PHP 8.5 and the updated repro script adopted by:
$joomlaBase = str_replace('/', DIRECTORY_SEPARATOR, '/var/www/html');
$_SERVER['HTTP_HOST'] = 'host.docker.internal:7054';
Before PR, I am getting the
FAIL: Error - Call to undefined method Joomla\CMS\Router\ApiRouter::build()
After applying the PR, I am getting
OK: /index.php/index.php/component/content/article/1
There is one PHP warning, but not related to changes from this PR:
Warning: Constant JDEBUG already defined, this will be an error in PHP 9 in /var/www/html/api/includes/framework.php on line 80
```<hr /><sub>This comment was created with the <a href="https://github.com/joomla/jissues">J!Tracker Application</a> at <a href="https://issues.joomla.org/tracker/joomla-cms/47662">issues.joomla.org/tracker/joomla-cms/47662</a>.</sub>
I have tested this item ✅ successfully on c9ee1b3
Tested with JBT, current 5.4-dev branch, PHP 8.5 and the updated repro script adopted by:
$joomlaBase = str_replace('/', DIRECTORY_SEPARATOR, '/var/www/html');
$_SERVER['HTTP_HOST'] = 'host.docker.internal:7054';
Before PR, I am getting the
FAIL: Error - Call to undefined method Joomla\CMS\Router\ApiRouter::build()
After applying the PR, I am getting
OK: /index.php/index.php/component/content/article/1
There is one PHP warning, but not related to changes from this PR:
Warning: Constant JDEBUG already defined, this will be an error in PHP 9 in /var/www/html/api/includes/framework.php on line 80
```<hr /><sub>This comment was created with the <a href="https://github.com/joomla/jissues">J!Tracker Application</a> at <a href="https://issues.joomla.org/tracker/joomla-cms/47662">issues.joomla.org/tracker/joomla-cms/47662</a>.</sub>
I have tested this item ✅ successfully on c9ee1b3
Tested with JBT, current 5.4-dev branch, PHP 8.5 and the updated repro script adopted by:
$joomlaBase = str_replace('/', DIRECTORY_SEPARATOR, '/var/www/html');
$_SERVER['HTTP_HOST'] = 'host.docker.internal:7054';
Before PR, I am getting the
FAIL: Error - Call to undefined method Joomla\CMS\Router\ApiRouter::build()
After applying the PR, I am getting
OK: /index.php/index.php/component/content/article/1
There is one PHP warning, but not related to changes from this PR:
Warning: Constant JDEBUG already defined, this will be an error in PHP 9 in /var/www/html/api/includes/framework.php on line 80
| Status | Pending | ⇒ | Ready to Commit |
RTC
RTC
| Labels |
Added:
RTC
bug
|
||
| Status | Ready to Commit | ⇒ | Closed |
| Closed_Date | 0000-00-00 00:00:00 | ⇒ | 2026-09-09 17:06:24 |
| Closed_By | ⇒ | HLeithner | |
| Labels |
Added:
RMDQ
|
||
Thanks for your contribution sadly I'm closing this, please the proper function Route::link('site', ....) if you need to generate a frontend route. thanks.
@hikashop-nicolas - Salut Nicolas, might be my setup but I couldn't get the BEFORE condition to match, I'll let others test it...