Skip to content
Merged
12 changes: 10 additions & 2 deletions packages/open-next/src/core/routing/matcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,10 +200,18 @@ export function handleRewrites<T extends RewriteDefinition>(
);
// We need to use a localized path if the rewrite is not locale specific
const pathToUse = rewrite.locale === false ? rawPath : localizedRawPath;
// We need to encode the "+" character with its UTF-8 equivalent "%20" to avoid 2 issues:
// 1. The compile function from path-to-regexp will throw an error if it finds a "+" character.
// https://github.com/pillarjs/path-to-regexp?tab=readme-ov-file#unexpected--or-
// 2. The convertToQueryString function will replace the "+" character with %2B instead of %20.
// %2B does not get interpreted as a space in the URL thus breaking the query string.
const encodePlusQueryString = queryString.replaceAll("+", "%20");
debug("urlParts", { pathname, protocol, hostname, queryString });
const toDestinationPath = compile(escapeRegex(pathname ?? "") ?? "");
const toDestinationHost = compile(escapeRegex(hostname ?? "") ?? "");
const toDestinationQuery = compile(escapeRegex(queryString ?? "") ?? "");
const toDestinationQuery = compile(
escapeRegex(encodePlusQueryString ?? "") ?? "",
);
let params = {
// params for the source
...getParamsFromSource(match(escapeRegex(rewrite?.source) ?? ""))(
Expand All @@ -219,7 +227,7 @@ export function handleRewrites<T extends RewriteDefinition>(
}, {}),
};
const isUsingParams = Object.keys(params).length > 0;
let rewrittenQuery = queryString;
let rewrittenQuery = encodePlusQueryString;
let rewrittenHost = hostname;
let rewrittenPath = pathname;
if (isUsingParams) {
Expand Down
7 changes: 5 additions & 2 deletions packages/open-next/src/core/routing/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,15 @@ export function convertRes(res: OpenNextNodeResponse): InternalResult {
* @__PURE__
*/
export function convertToQueryString(query: Record<string, string | string[]>) {
// URLSearchParams is a representation of the PARSED query.
// So we must decode the value before appending it to the URLSearchParams.
// https://stackoverflow.com/a/45516812
const urlQuery = new URLSearchParams();
Object.entries(query).forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach((entry) => urlQuery.append(key, entry));
value.forEach((entry) => urlQuery.append(key, decodeURIComponent(entry)));
} else {
urlQuery.append(key, value);
urlQuery.append(key, decodeURIComponent(value));
}
});
const queryString = urlQuery.toString();
Expand Down
21 changes: 21 additions & 0 deletions packages/tests-unit/tests/core/routing/matcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,27 @@ describe("handleRedirects", () => {

expect(result).toBeUndefined();
});

it("should redirect with + character and query string", () => {
const event = createEvent({
url: "/foo",
});

const result = handleRedirects(event, [
{
source: "/foo",
destination: "/search?bar=hello+world&baz=new%2C+earth",
locale: false,
statusCode: 308,
regex: "^(?!/_next)/foo(?:/)?$",
},
]);

expect(result.statusCode).toEqual(308);
expect(result.headers.Location).toEqual(
"/search?bar=hello+world&baz=new%2C+earth",
);
});
});

describe("handleRewrites", () => {
Expand Down
Loading