Skip to content

Fix #148: resolve redirects from the 404 handler instead of a catch-all URL rule - #150

Open
nikolenko-dmitriy wants to merge 1 commit into
Dolphiq:masterfrom
rbrv:upstream-pr/fix-148
Open

Fix #148: resolve redirects from the 404 handler instead of a catch-all URL rule#150
nikolenko-dmitriy wants to merge 1 commit into
Dolphiq:masterfrom
rbrv:upstream-pr/fix-148

Conversation

@nikolenko-dmitriy

Copy link
Copy Markdown

Fixes #148.

Problem

On a multi-site install where a site's base URL carries a URI prefix (e.g. https://example.com/en), redirects stored on that site never fire, and template pages there 404. The issue reports the template 404s; the dead redirects are the same root cause and arguably the bigger half — the plugin does nothing at all on a prefixed site.

Cause

craft\web\UrlManager::_getRequestRoute() evaluates, in order:

token -> element -> URL rules -> .well-known -> template

The '<all:.+>' catch-all registered on EVENT_REGISTER_SITE_URL_RULES sits in URL rules, so it is reached before .well-known and before template routing. The comment on the rule — "Real pages and entries resolve first; only a URL that would otherwise 404 reaches our controller" — holds for entries, but templates resolve last. So:

  1. Redirects never fire on a prefixed site. RedirectController passed getFullPath() to resolveForUri(), which keeps the URI prefix (en/foo), while source URLs are stored site-relative (foo) and matchUri() compares them directly. Nothing matches.
  2. Template pages 404 on a prefixed site, because the controller re-implements the template lookup with the same getFullPath().
  3. A configured .well-known route would be shadowed.

Fix

Rather than patch getFullPath()getPathInfo() in the controller, this stops pre-empting Craft's router altogether: resolution hooks ErrorHandler::EVENT_BEFORE_HANDLE_EXCEPTION and acts only on a genuine NotFoundHttpException. Craft routes entries, .well-known and templates natively, and only a real 404 reaches the plugin — which is what the 1.x/2.x line did in effect, via per-source rules, and what #148 suggests.

RedirectController is removed (nothing routed to it but the catch-all). EVENT_BEFORE_CATCHALL and FILE_EXTENSIONS move to RedirectPlugin. An unmatched 404 falls through to Craft's own 404 response unless catchAllTemplate is set; that template's request payload is unchanged.

Verification

Craft 5.10, three-site install (one at the domain root, two under URI prefixes), 5.1.1 vs this change, cold opcache:

Request Before After
prefixed site, redirect stored on it 404 301
prefixed site, template page 404 200
root site, template page 200 200
root site, redirect 301 301
unknown path (root + prefixed) 404 404
entries (root + prefixed) 200 200

Catch-all logging exercised separately with catchAllActive on: hits log per-site against the site-relative URI, and asset extensions stay excluded.

Note on set-password

#148 also reports set-password breaking. That is not caused by this rulecraft\web\Request resolves setPasswordPath via _specialPaths against the prefix-stripped path, making it an action request that never reaches the URL rules. It behaved identically before and after here, so that symptom has another cause.

Depends on

Needs the extra.schemaVersion fix to be useful on an existing install, otherwise resolveForUri() hits the missing priority column.

🤖 Generated with Claude Code

…all rule

On a multi-site install where a site's base URL carries a URI prefix
(e.g. https://example.com/en), redirects stored on that site never fire,
and template pages there 404. Both follow from the '<all:.+>' catch-all
rule registered on EVENT_REGISTER_SITE_URL_RULES.

craft\web\UrlManager::_getRequestRoute() evaluates, in order:

  token -> element -> URL rules -> .well-known -> template

so the catch-all is reached before .well-known and before template
routing. The comment on the rule ("Real pages and entries resolve first;
only a URL that would otherwise 404 reaches our controller") holds for
entries, but not for templates: those resolve last. Consequences:

1. Redirects never fire on a prefixed site. RedirectController passed
   getFullPath() to resolveForUri(), which keeps the site's URI prefix
   ("en/foo"), while source URLs are stored site-relative ("foo") and
   matchUri() compares them directly. Nothing matches.
2. Template pages 404 on a prefixed site, because the controller
   re-implements the template lookup with the same getFullPath().
3. A configured .well-known route would be shadowed.

Rather than patch getFullPath() -> getPathInfo() in the controller, this
stops pre-empting Craft's router: resolution now hooks
ErrorHandler::EVENT_BEFORE_HANDLE_EXCEPTION and acts only on a genuine
NotFoundHttpException. Craft routes entries, .well-known and templates
natively, and only a real 404 reaches the plugin. That is also what the
1.x/2.x line did in effect, via per-source rules.

RedirectController is removed (nothing routed to it but the catch-all).
EVENT_BEFORE_CATCHALL and FILE_EXTENSIONS move to RedirectPlugin. An
unmatched 404 falls through to Craft's own 404 response unless
catchAllTemplate is set; the template's `request` payload is unchanged.

Verified on Craft 5.10 with a three-site install — one site at the domain
root, two under URI prefixes — comparing 5.1.1 against this change with a
cold opcache:

  prefixed site, redirect stored on it   404 -> 301
  prefixed site, template page           404 -> 200
  root site, template page               200 -> 200
  root site, redirect                    301 -> 301
  unknown path, root and prefixed        404 -> 404
  entries, root and prefixed             200 -> 200

Catch-all logging was exercised separately with catchAllActive on: hits
log per-site against the site-relative URI, and asset extensions stay
excluded.

Note: Dolphiq#148 also reports set-password breaking. That is not caused by this
rule — craft\web\Request resolves setPasswordPath via _specialPaths
against the prefix-stripped path, making it an action request that never
reaches the URL rules. It behaved identically before and after here.

This change needs the schemaVersion fix to be useful on an existing
install, otherwise resolveForUri() hits a missing `priority` column.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Catch-all uses getFullPath() instead of getPathInfo() - 404s template pages on URI-prefixed sites and breaks set-password

2 participants