Opened 8 months ago
Last modified 7 days ago
#64376 new defect (bug)
redirect_canonical() causes unnecessary 301 redirects for query string encoding variants (+ vs %20)
| Reported by: | robbertvancaem | Owned by: | |
|---|---|---|---|
| Priority: | normal | Milestone: | Future Release |
| Component: | Canonical | Version: | |
| Severity: | normal | Keywords: | has-patch has-unit-tests needs-testing has-test-info |
| Cc: | Focuses: | performance |
Description
Summary
redirect_canonical()issues 301 redirects when query strings contain+characters, redirecting them to%20encoded equivalents. Since both are functionally identical in query strings (RFC 3986), these redirects serve no purpose and
cause real-world problems.
Environment
- WordPress version: 6.8.3
- PHP version: 8.3
- Hosting: Kinsta (with page caching)
- Relevant setting: Static front page enabled
Steps to Reproduce
- Set up a WordPress site with a static front page
- Visit:
https://example.com/?utm_content=Hello+World - WordPress redirects 301 to:
https://example.com/?utm_content=Hello%20World
Root Cause
In
wp-includes/canonical.php(lines 576-594), query strings are rebuilt usingrawurlencode_deep(), which converts+to%20. The function then compares the rebuilt URL to the original and issues a redirect, even though both URLs
resolve to identical content.
Real-World Impact
This caused a severe production incident on our site:
- Marketing campaign URLs with
+in UTM parameters triggered 301 redirects - Our page cache stored the 301 response
- All subsequent visitors received cached 301 redirects, creating redirect loops
- Campaign success rate dropped from 100% to 2% over several days
- Affected thousands of users before root cause was identified
This affects any site using:
- Page caching (Kinsta, WP Super Cache, W3 Total Cache, etc.)
- Marketing campaigns with spaces in UTM parameters
- Any query strings containing
+characters
Proposed Fix
Before returning a redirect, compare decoded URLs:
// In redirect_canonical(), before the final redirect: if ( $redirect_url && urldecode( $redirect_url ) === urldecode( $requested_url ) ) { return false; }
This preserves all meaningful redirects (www normalization, trailing slashes, pretty permalinks) while preventing encoding-only redirects that provide no SEO or functional benefit.
Current Workaround
add_filter( 'redirect_canonical', function( $redirect_url, $requested_url ) { if ( $redirect_url && urldecode( $redirect_url ) === urldecode( $requested_url ) ) { return false; } return $redirect_url; }, 10, 2 );
Why This Should Be Fixed
- No benefit: Both
+and%20represent spaces in query strings; redirecting between them serves no SEO or functional purpose - Breaks caching: 301 responses get cached, causing redirect loops
- Breaks analytics: Redirect strips original referrer data
- Violates user expectations: UTM parameters are designed for variation; users don't expect redirects based on encoding
Attachments (2)
Change History (16)
#1
@
8 months ago
- Keywords has-patch added
When redirect_canonical() rebuilt URLs, it sometimes triggered 301 redirects
only because the query string used an encoded variant (e.g. " " vs "%20" or "+").
This change compares decoded URLs and suppresses redirects when decoded forms
are identical, avoiding harmful encoding-only 301s that cause caching issues
and break tracking parameters. See Trac #64376.
#2
@
8 months ago
- Focuses performance added
- Milestone Awaiting Review → 7.0
I was able to reproduce the issue when accessing a static homepage.
I was not able to reproduce the issue when appending ?utm_content=Hello+World to a post permalink, however.
Tagging this as a performance defect because the redirect needlessly slows down page loads.
#3
@
8 months ago
- Keywords needs-patch added; has-patch removed
@iflairwebtechnologies The patch in 64376.diff does not apply. I also do not see how it would fix the problem, since it is specifically targeting attachment URLs when the issue is specifically for the static front page. This is not the first time I've seen a patch supplied like this from you. See previous example. Please do not waste other contributors' time by submitting patches that clearly do not apply and which have not been tested.
#5
@
8 months ago
@westonruter
Thank you for your feedback.
We appreciate you pointing out the issues with the patch. We apologize for the oversight, we will ensure that all future patches are properly reviewed, tested, and correctly aligned with the reported issue before submission.
This ticket was mentioned in PR #10724 on WordPress/wordpress-develop by @sanket.parmar.
7 months ago
#6
- Keywords has-patch has-unit-tests added; needs-patch removed
This PR fixes unnecessary 301 redirect triggered by redirect_canonical() when a query string contains a + (plus sign) instead of %20.
While both + and %20 are valid representations of a space in a query string, WordPress's redirect_canonical() logic uses rawurlencode_deep(), which strictly follows RFC 3986 (using %20). When comparing the requested URL to the canonical candidate, the mismatch in encoding triggers a redirect. This can lead to redirect loops, increased server load, and cache poisoning in environments with edge caching.
Changes
- Updated
redirect_canonical()inwp-includes/canonical.phpto normalize query string encoding before comparison. - Implemented a check that treats
+and%20as equivalent within the query string portion of the URL. - Ensures that if the only difference between the current URL and the redirect candidate is the space encoding, the redirect is suppressed.
Testing Instructions
- Set up a WordPress site with pretty permalinks enabled.
- Navigate to a URL with a plus sign in a query parameter, for example:
example.com/?s=hello+world. - Before Patch: Observe a 301 redirect to
example.com/?s=hello%20world. - After Patch: The page should load directly without a 301 redirect.
- Verify that search results and other query-dependent pages still function correctly.
Trac ticket
@sanket.parmar commented on PR #10724:
6 months ago
#7
@westonruter Just wanted to confirm whether we only need the urlencode for the URLs with + characters, or make it generic? The Copilot feedback suggests only for + character.
@westonruter commented on PR #10724:
6 months ago
#8
@sanketio it does seem that this more targeted replacement is valid feedback, especially since the original issue was reported about spaces in the query string.
This ticket was mentioned in Slack in #core-performance by mukeshpanchal27. View the logs.
6 months ago
#10
@
6 months ago
- Keywords needs-testing added
- Milestone 7.0 → Future Release
Moving this to Future Release as there has been no progress on the ticket for the past few weeks. Feel free to move it back to the milestone once it’s ready for merge.
@sanket.parmar commented on PR #10724:
6 months ago
#11
@westonruter Thank you for your reply, and apologies for delay here. I have pushed some updates.
Note: A couple of test cases are failing here, but they are not related to the changes in this PR.
#12
@
4 months ago
Just wanted to add my 2c here. I struggled with this for the whole of Easter weekend until I figured out what was happening. Upon fixing the issue I discovered this thread.
@sanket.parmar commented on PR #10724:
3 weeks ago
#13
Thanks for the automated re-review. Addressing the two low-confidence notes from the latest pass:
Normalization ordering / hex-octet casing — Good observation, but this only affects URLs that differ by +/%20 *and* hex-octet casing at the same time (e.g. %C3%A9 vs %c3%a9), which is rare since canonical URLs and browsers emit uppercase octets per RFC 3986. It's also not a regression: that combined case simply falls through and redirects exactly as it did before this PR. The change only *adds* a suppression path for the pure space-encoding scenario the ticket describes. I'm happy to handle combined octet-case normalization as a follow-up if that's preferred.
Test cleanup deleting show_on_front — WP_UnitTestCase runs each test inside a DB transaction that's rolled back in tear_down (and flushes the object cache between tests), so deleting show_on_front here doesn't leak into later tests — it's reverted regardless of delete vs. restore. Keeping the explicit cleanup for readability.
#14
@
7 days ago
- Keywords has-test-info added
Test Report
Description
This report validates that the patch resolves the unnecessary canonical redirect caused by + versus %20 query-string space encoding.
Patch tested: https://github.com/WordPress/wordpress-develop/pull/10724
Environment
- WordPress: 7.2-alpha-63166-src
- PHP: 8.3.30
- Server: nginx/1.31.1
- Database: MySQL
- OS: Windows 11 with Docker Desktop
- Multisite: No
Steps to Test
- Configure a static front page.
- Visit
/?utm_content=Hello+World. - Inspect the HTTP status and
Locationheader. - Apply the PR and repeat the request.
Actual Results
- Before patch: Returned
301 Moved Permanentlyand redirected to/?utm_content=Hello%20World. - After patch: Returned
200 OKwith no redirect. - The
%20variant also returned200 OK. - Existing canonical PHPUnit suite passed: 1,052 tests, 1,131 assertions.
- Ticket-specific tests passed: 1 test, 4 assertions.
The patch works as expected.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)
canonical: avoid unnecessary redirects for encoding-only differences (fix #64376)