RTC bug RMDQ PR-5.4-dev Pending

User tests: Successful: Unsuccessful:

avatar hikashop-nicolas
hikashop-nicolas
22 Apr 2026

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 # .

  • I read the Generative AI policy and my contribution is either not created with the help of AI or is compatible with the policy and GNU/GPL 2 or later.

Summary of Changes

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.

Testing Instructions

  1. Install a Joomla 5.x or 6.x site.
  2. From a CLI shell, run the repro script below against your installed site. It boots the API application the same way 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";
}
  1. Run without the patch: observe the ApiRouter::build() crash.
  2. Apply the patch and run again: the URL is built. The path prefix is doubled under the CLI SAPI because 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.

Actual result BEFORE applying this Pull Request

 FAIL: Error - Call to undefined method Joomla\CMS\Router\ApiRouter::build()

Expected result AFTER applying this Pull Request

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.

Link to documentations

Please select:

  • No documentation changes for guide.joomla.org needed
  • No documentation changes for manual.joomla.org needed
avatar hikashop-nicolas hikashop-nicolas - open - 22 Apr 2026
avatar hikashop-nicolas hikashop-nicolas - change - 22 Apr 2026
Status New ⇒ Pending
avatar joomla-cms-bot joomla-cms-bot - change - 22 Apr 2026
Category ⇒ Libraries
avatar hikashop-nicolas hikashop-nicolas - change - 22 Apr 2026
The description was changed
avatar hikashop-nicolas hikashop-nicolas - edited - 22 Apr 2026
avatar exlemor
exlemor - comment - 27 Apr 2026

@hikashop-nicolas - Salut Nicolas, might be my setup but I couldn't get the BEFORE condition to match, I'll let others test it...

avatar hikashop-nicolas
hikashop-nicolas - comment - 14 Jul 2026

@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().

avatar ThomasFinnern ThomasFinnern - test_item - 16 Jul 2026 - Tested successfully
avatar ThomasFinnern
ThomasFinnern - comment - 16 Jul 2026

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


This comment was created with the J!Tracker Application at issues.joomla.org/tracker/joomla-cms/47662.

avatar ThomasFinnern
ThomasFinnern - comment - 16 Jul 2026

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


This comment was created with the J!Tracker Application at issues.joomla.org/tracker/joomla-cms/47662.

avatar ThomasFinnern
ThomasFinnern - comment - 16 Jul 2026

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


This comment was created with the J!Tracker Application at issues.joomla.org/tracker/joomla-cms/47662.

avatar ThomasFinnern
ThomasFinnern - comment - 16 Jul 2026

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


This comment was created with the J!Tracker Application at issues.joomla.org/tracker/joomla-cms/47662.

avatar hikashop-nicolas hikashop-nicolas - change - 26 Jul 2026
Labels Added: PR-5.4-dev
avatar richard67
richard67 - comment - 26 Jul 2026

@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.

avatar richard67 richard67 - alter_testresult - 26 Jul 2026 - ThomasFinnern: Tested successfully
avatar muhme
muhme - comment - 5 Sep 2026

@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?

avatar hikashop-nicolas
hikashop-nicolas - comment - 5 Sep 2026

@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.

avatar hikashop-nicolas hikashop-nicolas - change - 5 Sep 2026
The description was changed
avatar hikashop-nicolas hikashop-nicolas - edited - 5 Sep 2026
avatar muhme
muhme - comment - 6 Sep 2026

I am updating the testing instructions in the PR description with the corrected script

@hikashop-nicolas thank you 👍 the updated script is working

avatar muhme muhme - test_item - 6 Sep 2026 - Tested successfully
avatar muhme
muhme - comment - 6 Sep 2026

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>
avatar muhme
muhme - comment - 6 Sep 2026

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>
avatar muhme
muhme - comment - 6 Sep 2026

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

This comment was created with the J!Tracker Application at issues.joomla.org/tracker/joomla-cms/47662.
avatar muhme muhme - change - 6 Sep 2026
Status Pending ⇒ Ready to Commit
avatar muhme
muhme - comment - 6 Sep 2026

RTC


This comment was created with the J!Tracker Application at issues.joomla.org/tracker/joomla-cms/47662.

avatar muhme
muhme - comment - 6 Sep 2026

RTC


This comment was created with the J!Tracker Application at issues.joomla.org/tracker/joomla-cms/47662.

avatar richard67 richard67 - change - 6 Sep 2026
Labels Added: RTC bug
avatar HLeithner HLeithner - change - 9 Sep 2026
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
avatar HLeithner HLeithner - close - 9 Sep 2026
avatar HLeithner
HLeithner - comment - 9 Sep 2026

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.

Add a Comment

Login with GitHub to post a comment