fix: retry trailing-slash redirect when URL has a query string#31843
fix: retry trailing-slash redirect when URL has a query string#31843helio-cf wants to merge 2 commits into
Conversation
The worker's trailing-slash retry ran `request.url.replace(/([^/])$/, "$1/")` against the full URL string. For requests with a query string, that regex appended the slash at the end of the query (e.g. `?test=whatever/`) rather than to the pathname, so a rule like `/foo/* /bar/:splat 301` was never matched and the request 404'd. The retry now: - Skips entirely when pathname already ends in `/` (avoids a redundant evaluator call on every non-redirected request). - Appends `/` to the pathname only, so the retry actually forces a trailing slash on paths that carry a query string. - Reuses the URL parsed at the top of `fetch` instead of allocating another one. Adds two regression tests: one exercising a query string on a source path that already has a trailing slash (baseline for the library), and one on a source path that requires the trailing-slash retry to fire — the case the fix restores.
Review✅ No issues found in commit Code ReviewThis code review is in beta and may not always be helpful — use your judgment. No code review issues found. ConventionsChecks PR title, description, and redirect checklist. No convention issues found. Style Guide ReviewNo style-guide issues found. RedirectsNo missing redirect entries found. CommandsOnly codeowners can run commands. Post a comment with the command to trigger it.
|
| return isMarkdownRequest | ||
| ? rewriteRedirectForMarkdown(redirect, url) | ||
| : redirect; | ||
| if (!pathname.endsWith("/")) { |
There was a problem hiding this comment.
pathname is not new, declared at around line 80 of this function
| if (!pathname.endsWith("/")) { | ||
| try { | ||
| const redirect = await redirectsEvaluator( | ||
| new Request(`${url.origin}${pathname}/${url.search}`, request), |
For requests like /old/index.md the pathname doesn't end in '/', so the retry would evaluate /old/index.md/. The first pass already covers these by stripping /index.md and evaluating the base path, and if the retry had happened to match anything, rewriteRedirectForMarkdown would then have re-appended index.md, producing paths like /new/index.md/index.md if the destination itself ended with /index.md. Gate the retry on !isMarkdownRequest as well, and drop the now-dead isMarkdownRequest branch inside it. Adds a regression test asserting that /docs/index.md resolves through the base path in the first pass.
| try { | ||
| const forceTrailingSlashURL = new URL( | ||
| request.url.replace(/([^/])$/, "$1/"), | ||
| request.url, | ||
| ); | ||
| const redirect = await redirectsEvaluator( | ||
| new Request(forceTrailingSlashURL, request), | ||
| this.env.ASSETS, | ||
| ); | ||
| if (redirect) { | ||
| return isMarkdownRequest | ||
| ? rewriteRedirectForMarkdown(redirect, url) | ||
| : redirect; |
There was a problem hiding this comment.
the new code does away with the regex but keeps the rest up until the ternary, i dont believe we need rewriteRedirectForMarkdown(redirect, url) at this point since it would have already been covered by the first redirect check above... or am i reading this wrong?


Summary
The worker's trailing-slash retry ran
request.url.replace(/([^/])$/, "$1/")against the full URL string. For requests with a query string, that regex appended the slash at the end of the query (e.g.?test=whatever/) rather than to the pathname, so a rule like/foo/* /bar/:splat 301was never matched and the request 404'd.TLDR: https://developers.cloudflare.com/cloudflare-one/policies/data-loss-prevention?x=y 404s but should redirect the same way as https://developers.cloudflare.com/cloudflare-one/policies/data-loss-prevention/?x=y
The retry now:
/(avoids a redundant evaluator call on every non-redirected request)./to the pathname only, so the retry actually forces a trailing slash on paths that carry a query string.fetchinstead of allocating another one.Adds two regression tests: one exercising a query string on a source path that already has a trailing slash (baseline for the library), and one on a source path that requires the trailing-slash retry to fire — the case the fix restores.