diff --git a/packages/sitecore-jss-nextjs/src/middleware/redirects-middleware.test.ts b/packages/sitecore-jss-nextjs/src/middleware/redirects-middleware.test.ts index 571295891a..c6e572f31f 100644 --- a/packages/sitecore-jss-nextjs/src/middleware/redirects-middleware.test.ts +++ b/packages/sitecore-jss-nextjs/src/middleware/redirects-middleware.test.ts @@ -1411,6 +1411,57 @@ describe('RedirectsMiddleware', () => { expect(finalRes).to.deep.equal(res); expect(finalRes.status).to.equal(res.status); }); + + it('should return 301 redirect when pattern has special symbols "?"', async () => { + const cloneUrl = () => Object.assign({}, req.nextUrl); + const url = { + clone: cloneUrl, + href: 'http://localhost:3000/found?a=1&w=1', + locale: 'en', + origin: 'http://localhost:3000', + search: '?a=1&w=1', + pathname: '/found', + }; + + const { res, req } = createTestRequestResponse({ + response: { url }, + request: { + nextUrl: { + pathname: '/not-found/', + search: '?a=1&w=1', + href: 'http://localhost:3000/not-found/?a=1&w=1', + locale: 'en', + origin: 'http://localhost:3000', + clone: cloneUrl, + }, + }, + }); + setupRedirectStub(301); + + const { finalRes, fetchRedirects, siteResolver } = await runTestWithRedirect( + { + pattern: '/[/]?not-found?a=1&w=1/', + target: '/found', + redirectType: REDIRECT_TYPE_301, + isQueryStringPreserved: true, + locale: 'en', + }, + req + ); + + validateEndMessageDebugLog('redirects middleware end in %dms: %o', { + headers: {}, + redirected: undefined, + status: 301, + url, + }); + + expect(siteResolver.getByHost).to.be.calledWith(hostname); + // eslint-disable-next-line no-unused-expressions + expect(fetchRedirects.called).to.be.true; + expect(finalRes).to.deep.equal(res); + expect(finalRes.status).to.equal(res.status); + }); }); describe('should redirect to normalized path when nextjs specific "path" query string parameter is provided', () => { diff --git a/packages/sitecore-jss-nextjs/src/middleware/redirects-middleware.ts b/packages/sitecore-jss-nextjs/src/middleware/redirects-middleware.ts index 3c40e86595..d67ace96e7 100644 --- a/packages/sitecore-jss-nextjs/src/middleware/redirects-middleware.ts +++ b/packages/sitecore-jss-nextjs/src/middleware/redirects-middleware.ts @@ -199,14 +199,14 @@ export class RedirectsMiddleware extends MiddlewareBase { return modifyRedirects.length ? modifyRedirects.find((redirect: RedirectInfo & { matchedQueryString?: string }) => { // Modify the redirect pattern to ignore the language prefix in the path - redirect.pattern = redirect.pattern.replace(RegExp(`^[^]?/${language}/`, 'gi'), ''); + // And escapes non-special "?" characters in a string or regex. + redirect.pattern = this.escapeNonSpecialQuestionMarks(redirect.pattern.replace(RegExp(`^[^]?/${language}/`, 'gi'), '')); // Prepare the redirect pattern as a regular expression, making it more flexible for matching URLs redirect.pattern = `/^\/${redirect.pattern .replace(/^\/|\/$/g, '') // Removes leading and trailing slashes .replace(/^\^\/|\/\$$/g, '') // Removes unnecessary start (^) and end ($) anchors .replace(/^\^|\$$/g, '') // Further cleans up anchors - .replace(/(? route) + .filter((route: string) => route) .map((route) => `path=${route}`); /** @@ -362,4 +362,51 @@ export class RedirectsMiddleware extends MiddlewareBase { ].some(Boolean) ); } + + /** + * Escapes non-special "?" characters in a string or regex. + * + * - For regular strings, it escapes all unescaped "?" characters by adding a backslash (`\`). + * - For regex patterns (strings enclosed in `/.../`), it analyzes each "?" to determine if it has special meaning + * (e.g., `?` in `(abc)?`, `.*?`) or is just a literal character. Only literal "?" characters are escaped. + * @param {string} input - The input string or regex pattern. + * @returns {string} - The modified string or regex with non-special "?" characters escaped. + **/ + private escapeNonSpecialQuestionMarks(input: string): string { + const regexPattern = /(?