Skip to content

fix: retry trailing-slash redirect when URL has a query string#31843

Open
helio-cf wants to merge 2 commits into
cloudflare:productionfrom
helio-cf:fix/redirect-retry-with-query-string
Open

fix: retry trailing-slash redirect when URL has a query string#31843
helio-cf wants to merge 2 commits into
cloudflare:productionfrom
helio-cf:fix/redirect-retry-with-query-string

Conversation

@helio-cf

@helio-cf helio-cf commented Jul 1, 2026

Copy link
Copy Markdown

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 301 was 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:

  • 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.

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.
@helio-cf helio-cf requested review from a team and kodster28 as code owners July 1, 2026 22:47
@github-actions github-actions Bot added the size/s label Jul 1, 2026
@cloudflare-docs-bot

cloudflare-docs-bot Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review

✅ No issues found in commit 4f067c6.

Code Review

This code review is in beta and may not always be helpful — use your judgment.

No code review issues found.

Conventions

Checks PR title, description, and redirect checklist.

No convention issues found.

Style Guide Review

No style-guide issues found.

Redirects

No missing redirect entries found.

Commands

Only codeowners can run commands. Post a comment with the command to trigger it.

Command Description
/review Runs a review now. Incremental if a prior review exists, full if not.
/full-review Re-reviews the entire PR diff from scratch, ignoring incremental history. Useful after a rebase, when you want a fresh review, or if the bot gets out of sync and reports issues that no longer exist.
/ignore-review-limit Permanently lifts the 2-review automatic limit for this PR. Future pushes will trigger reviews as normal.
/disable-auto-review Stops automatic reviews from triggering on future pushes to this PR. Codeowners can still run /review or /full-review manually.

Comment thread worker/index.ts Outdated
return isMarkdownRequest
? rewriteRedirectForMarkdown(redirect, url)
: redirect;
if (!pathname.endsWith("/")) {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pathname is not new, declared at around line 80 of this function

Comment thread worker/index.ts
if (!pathname.endsWith("/")) {
try {
const redirect = await redirectsEvaluator(
new Request(`${url.origin}${pathname}/${url.search}`, request),

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image

joining these should be safe, pathname will always have a leading slash

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

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.
Comment thread worker/index.ts
Comment on lines -168 to -180
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;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants