Hi everyone,
OJS 3.5.0.3
I wanted to share a solution to a very frustrating routing bug that I recently encountered and managed to fix. I hope this helps other journal administrators who are struggling with multilingual setups and clean URLs.
We run a bilingual OJS 3 journal. We successfully enabled restful_urls = On in config.inc.php and removed index.php via .htaccess. The primary locale (/uk/) worked perfectly.
However, whenever a user (or Googlebot) tried to access the secondary locale (e.g., domain.com/1/en/article/view/123), the browser threw an ERR_TOO_MANY_REDIRECTS error.
This not only ruined the user experience but also caused massive “Page with redirect” errors in Google Search Console, preventing our English articles from being indexed.
Standard fixes like adjusting base_url[index] mapping, toggling force_canonical_hostname, or setting disable_path_info = On did not resolve this specific loop in our server environment.
The issue lies deep within the OJS Front Controller (PKPRequest::redirectUrl). When OJS detects a request for a secondary locale, it tries to enforce the canonical URL. However, because index.php is stripped by the server, the internal router sees a mismatch between the requested URI and its generated canonical URI. It issues a Location: redirect to fix it, but generates the exact same logical path. The system ends up infinitely redirecting to itself.
To fix this, we need to add a simple “loop prevention” check inside the redirect function. It intercepts the redirect, compares the target path with the current path, and if they are logically identical, it aborts the redirect and renders the page normally (HTTP 200 OK).
we need to edit: lib/pkp/classes/core/PKPRequest.php
Find the redirectUrl(string $url) method
public function redirectUrl(string $url): void
{
if (Hook::call('Request::redirect', [&$url])) {
return;
}
// sent out the cookie as header
Application::get()->getRequest()->getSessionGuard()->sendCookies();
header("Location: {$url}");
exit;
}
and replace it entirely with this patched version:
public function redirectUrl(string $url): void
{
if (Hook::call('Request::redirect', [&$url])) {
return;
}
// sent out the cookie as header
Application::get()->getRequest()->getSessionGuard()->sendCookies();
// --- ANTI-LOOP PATCH FOR OJS 3 START ---
// Prevent infinite redirects on secondary locales when restful_urls is On
$targetPath = parse_url($url, PHP_URL_PATH);
$currentPath = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
if ($targetPath !== null && $currentPath !== null) {
// Clean paths for accurate comparison (ignoring index.php presence)
$target = trim(str_replace('index.php/', '', $targetPath), '/');
$current = trim(str_replace('index.php/', '', $currentPath), '/');
// Abort redirect if we are already heading to the exact same logical path
if ($target !== '' && $target === $current) {
return; // Render the page with 200 OK instead of looping
}
}
// --- ANTI-LOOP PATCH END ---
header("Location: {$url}");
exit;
}
As soon as the file is saved (and .php files in the /cache/ folder are cleared), the secondary locale pages load instantly with a strict 200 OK status. Cross-journal and HTTP->HTTPS redirects continue to work flawlessly, as the patch only aborts exact-match loops.
Note: Always backup your PKPRequest.php file before making core edits!
I hope the PKP dev team can take a look at this routing logic for future releases, but until then, I hope this patch saves someone else a few sleepless nights!
