[SOLVED] Infinite redirect loop (ERR_TOO_MANY_REDIRECTS) on secondary locales with restful_urls enabled

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!

Hi @Oleksandr_Radkevych,

At a glance, this may be the same issue as reported here:

Can you confirm?

Regards,
Alec Smecher
Public Knowledge Project Team

Hi @asmecher,

I can confirm this is exactly the same issue I’ve encountered on OJS 3.5.0.3 with restful_urls enabled.

Secondary locales trigger an infinite redirect loop. I temporarily patched PKPRequest::redirectUrl to abort the redirect if the target path matches the current URI (ignoring index.php). This immediately restored the 200 OK status and fixed our Google indexing issues.