Skip to content

Fix: Canonical redirects hijacking matched routes (#13)#53

Open
Levdbas wants to merge 2 commits into
masterfrom
canolical-attachment-redirect-fix
Open

Fix: Canonical redirects hijacking matched routes (#13)#53
Levdbas wants to merge 2 commits into
masterfrom
canolical-attachment-redirect-fix

Conversation

@Levdbas

@Levdbas Levdbas commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Problem

When a mapped route's slug happened to match the slug of a media attachment (e.g. an image), WordPress would redirect the request straight to the raw upload file instead of rendering the route.

Root cause: WordPress's redirect_canonical() runs on template_redirect and issues a 301 to the raw attachment file whenever the resolved main query is_attachment() is true — this is expected behavior since attachment pages are disabled by default (wp_attachment_pages_enabled). The problem is that this redirect fires (and exits) before the template_include filter that Routes::load() registers ever runs, so a matched route gets silently overridden.

Fix

Routes::load() now cancels WordPress's canonical redirect via the documented redirect_canonical filter (add_filter('redirect_canonical', '__return_false')) whenever it successfully sets up a template. Once a route has matched and is rendering a response, it should always take full control — WordPress's own canonical/attachment redirect logic should no longer have a say.

This only applies when a route actually resolves to a template via Routes::load(); requests that don't match any route, or that match a route which never calls Routes::load(), are completely unaffected.

Tests

  • testRouteSurvivesAttachmentRedirect — reproduces the original bug: creates a colliding attachment, maps a route to the same slug, forces the main query to resolve as that attachment, and asserts the response is 200 (not a 301 to the raw file). Confirmed this test fails without the fix.
  • testUnmatchedRouteDoesNotAffectCanonicalRedirects — sanity check that regular WordPress canonical redirects (e.g. legacy ?p=123 links) still work when no route matches at all.
  • testMatchedRouteWithoutLoadDoesNotAffectCanonicalRedirects — sanity check that a route matching and running its callback without calling Routes::load() doesn't affect canonical redirects either.

All 25 tests pass, verified stable across multiple random test orderings.

Files changed

  • Routes.php — the fix
  • tests/RoutesTest.php — regression + sanity tests

Fixes #13

Levdbas added 2 commits July 7, 2026 22:56
WordPress's redirect_canonical() runs on template_redirect and can send a 301 to the raw upload file whenever the resolved query is_attachment(), since attachment pages are disabled by default. This fires before template_include, so a route whose slug collides with a media attachment's slug gets silently overridden.

Routes::load() now cancels the canonical redirect (via the documented redirect_canonical filter) whenever it successfully sets up a template, since a matched route should always take full control of the response.

Fixes #13
- testRouteSurvivesAttachmentRedirect reproduces the original bug and verifies the route wins instead of being redirected to the attachment.
- testUnmatchedRouteDoesNotAffectCanonicalRedirects and testMatchedRouteWithoutLoadDoesNotAffectCanonicalRedirects confirm regular WordPress canonical redirects are untouched when Routes isn't actively rendering a response.
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.

Image instead of the page

1 participant