Skip to content

Commit 77d87e7

Browse files
Fix redirect when containing "+" and decode values for URLSearchParams (#603)
* encode "+" character in query string * Decode query string for URLSearchParams * add test case for + character and query string * fix format with prettier * fix up comment * internal redirect expects result to be undefined * Revert "internal redirect expects result to be undefined" This reverts commit ea90c3c. * internal redirect expects result to be undefined * Revert "internal redirect expects result to be undefined" This reverts commit a6f5ccb. * remove internal and set locale to false * Create big-grapes-hammer.md --------- Co-authored-by: conico974 <nicodorseuil@yahoo.fr>
1 parent 5839217 commit 77d87e7

File tree

4 files changed

+41
-4
lines changed

4 files changed

+41
-4
lines changed

.changeset/big-grapes-hammer.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@opennextjs/aws": patch
3+
---
4+
5+
Fix redirect when containing "+" and decode values for URLSearchParams

packages/open-next/src/core/routing/matcher.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,10 +200,18 @@ export function handleRewrites<T extends RewriteDefinition>(
200200
);
201201
// We need to use a localized path if the rewrite is not locale specific
202202
const pathToUse = rewrite.locale === false ? rawPath : localizedRawPath;
203+
// We need to encode the "+" character with its UTF-8 equivalent "%20" to avoid 2 issues:
204+
// 1. The compile function from path-to-regexp will throw an error if it finds a "+" character.
205+
// https://github.com/pillarjs/path-to-regexp?tab=readme-ov-file#unexpected--or-
206+
// 2. The convertToQueryString function will replace the "+" character with %2B instead of %20.
207+
// %2B does not get interpreted as a space in the URL thus breaking the query string.
208+
const encodePlusQueryString = queryString.replaceAll("+", "%20");
203209
debug("urlParts", { pathname, protocol, hostname, queryString });
204210
const toDestinationPath = compile(escapeRegex(pathname ?? "") ?? "");
205211
const toDestinationHost = compile(escapeRegex(hostname ?? "") ?? "");
206-
const toDestinationQuery = compile(escapeRegex(queryString ?? "") ?? "");
212+
const toDestinationQuery = compile(
213+
escapeRegex(encodePlusQueryString ?? "") ?? "",
214+
);
207215
let params = {
208216
// params for the source
209217
...getParamsFromSource(match(escapeRegex(rewrite?.source) ?? ""))(
@@ -219,7 +227,7 @@ export function handleRewrites<T extends RewriteDefinition>(
219227
}, {}),
220228
};
221229
const isUsingParams = Object.keys(params).length > 0;
222-
let rewrittenQuery = queryString;
230+
let rewrittenQuery = encodePlusQueryString;
223231
let rewrittenHost = hostname;
224232
let rewrittenPath = pathname;
225233
if (isUsingParams) {

packages/open-next/src/core/routing/util.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,15 @@ export function convertRes(res: OpenNextNodeResponse): InternalResult {
9898
* @__PURE__
9999
*/
100100
export function convertToQueryString(query: Record<string, string | string[]>) {
101+
// URLSearchParams is a representation of the PARSED query.
102+
// So we must decode the value before appending it to the URLSearchParams.
103+
// https://stackoverflow.com/a/45516812
101104
const urlQuery = new URLSearchParams();
102105
Object.entries(query).forEach(([key, value]) => {
103106
if (Array.isArray(value)) {
104-
value.forEach((entry) => urlQuery.append(key, entry));
107+
value.forEach((entry) => urlQuery.append(key, decodeURIComponent(entry)));
105108
} else {
106-
urlQuery.append(key, value);
109+
urlQuery.append(key, decodeURIComponent(value));
107110
}
108111
});
109112
const queryString = urlQuery.toString();

packages/tests-unit/tests/core/routing/matcher.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,27 @@ describe("handleRedirects", () => {
284284

285285
expect(result).toBeUndefined();
286286
});
287+
288+
it("should redirect with + character and query string", () => {
289+
const event = createEvent({
290+
url: "/foo",
291+
});
292+
293+
const result = handleRedirects(event, [
294+
{
295+
source: "/foo",
296+
destination: "/search?bar=hello+world&baz=new%2C+earth",
297+
locale: false,
298+
statusCode: 308,
299+
regex: "^(?!/_next)/foo(?:/)?$",
300+
},
301+
]);
302+
303+
expect(result.statusCode).toEqual(308);
304+
expect(result.headers.Location).toEqual(
305+
"/search?bar=hello+world&baz=new%2C+earth",
306+
);
307+
});
287308
});
288309

289310
describe("handleRewrites", () => {

0 commit comments

Comments
 (0)