Skip to content

Commit 1281fa8

Browse files
committed
fixup! fix(@angular/ssr): resolve query parameter placeholders in redirect targets
1 parent dff1cab commit 1281fa8

3 files changed

Lines changed: 48 additions & 21 deletions

File tree

‎packages/angular/ssr/src/utils/url.ts‎

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -220,37 +220,56 @@ export function buildPathWithParams(toPath: string, fromPath: string): string {
220220
* Resolves `:name` placeholders in the query of a URL, like the Angular router does for redirects.
221221
*
222222
* Each query parameter whose value is a `:name` placeholder gets the values of the `name`
223-
* query parameter of `fromParams`, and is removed if `fromParams` has no such parameter.
223+
* query parameter of `fromParams`, or `undefined` if `fromParams` has no such parameter.
224+
* Like in the Angular router, placeholders of repeated query parameters are not resolved.
224225
*
225226
* @param toUrl - A URL whose query may contain `:name` placeholders.
226227
* @param fromParams - The query parameters that provide the values for the placeholders.
227228
* @returns The URL with its query placeholders resolved.
228229
*
229230
* @example
230231
* ```typescript
231-
* buildQueryWithParams('/search?q=:term', new URLSearchParams('term=cats')); // returns '/search?q=cats'
232+
* buildQueryWithParams('/find?q=:x', new URLSearchParams('x=cats')); // returns '/find?q=cats'
232233
* ```
233234
*/
234235
export function buildQueryWithParams(toUrl: string, fromParams: URLSearchParams): string {
235236
const [, path, query, fragment] = /^([^?#]*)\?([^#]*)(.*)$/.exec(toUrl) ?? [];
236-
if (query === undefined || !query.includes('=:')) {
237+
if (query === undefined) {
237238
return toUrl;
238239
}
239240

240-
const resolvedParams = new URLSearchParams();
241-
for (const [name, value] of new URLSearchParams(query)) {
242-
if (value[0] === ':') {
243-
for (const sourceValue of fromParams.getAll(value.slice(1))) {
244-
resolvedParams.append(name, sourceValue);
241+
const params = query.split('&');
242+
const names = params.map((param) => param.split('=', 1)[0]);
243+
const resolvedQuery = params
244+
.map((param, index) => {
245+
const name = names[index];
246+
const value = param.slice(name.length + 1);
247+
if (value[0] !== ':' || names.indexOf(name) !== names.lastIndexOf(name)) {
248+
return param;
245249
}
246-
} else {
247-
resolvedParams.append(name, value);
248-
}
249-
}
250250

251-
const resolvedQuery = resolvedParams.toString();
251+
const sourceValues = fromParams.getAll(value.slice(1));
252+
253+
return (sourceValues.length ? sourceValues : ['undefined'])
254+
.map((sourceValue) => `${name}=${encodeQueryValue(sourceValue)}`)
255+
.join('&');
256+
})
257+
.join('&');
258+
259+
return `${path}?${resolvedQuery}${fragment}`;
260+
}
252261

253-
return path + (resolvedQuery ? `?${resolvedQuery}` : '') + fragment;
262+
/**
263+
* Encodes a query parameter value like the Angular router (`DefaultUrlSerializer`), which keeps
264+
* `@`, `:`, `$`, `,` and `;` as is.
265+
*
266+
* @param value - The query parameter value to encode.
267+
* @returns The encoded query parameter value.
268+
*/
269+
function encodeQueryValue(value: string): string {
270+
return encodeURIComponent(value).replace(/%(40|3A|24|2C|3B)/g, (match) =>
271+
decodeURIComponent(match),
272+
);
254273
}
255274

256275
const MATRIX_PARAMS_REGEX = /;[^/]+/g;

‎packages/angular/ssr/test/app_spec.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,9 @@ describe('AngularServerApp', () => {
276276
const response = await app.handle(
277277
new Request('http://localhost/redirect-query-placeholder?term=cats&term=dogs'),
278278
);
279-
expect(response?.headers.get('location')).toBe('/home?q=cats&q=dogs&static=value');
279+
expect(response?.headers.get('location')).toBe(
280+
'/home?q=cats&q=dogs&id=undefined&static=value',
281+
);
280282
expect(response?.status).toBe(302);
281283
});
282284

‎packages/angular/ssr/test/utils/url_spec.ts‎

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -207,16 +207,22 @@ describe('URL Utils', () => {
207207

208208
describe('buildQueryWithParams', () => {
209209
it('should replace query placeholders with the values of the source parameters', () => {
210-
const fromParams = new URLSearchParams('term=cats&term=dogs&page=2');
211-
expect(buildQueryWithParams('/search?q=:term&p=:page&static=1#top', fromParams)).toBe(
212-
'/search?q=cats&q=dogs&p=2&static=1#top',
210+
const fromParams = new URLSearchParams('term=big cats&term=a:b@c&page=2');
211+
expect(buildQueryWithParams('/search?q=:term&p=:page&static=a=:b#top', fromParams)).toBe(
212+
'/search?q=big%20cats&q=a:b@c&p=2&static=a=:b#top',
213213
);
214214
});
215215

216-
it('should remove query parameters whose source parameter is missing', () => {
216+
it('should set query parameters whose source parameter is missing to undefined', () => {
217217
const fromParams = new URLSearchParams();
218-
expect(buildQueryWithParams('/search?q=:term&static=1', fromParams)).toBe('/search?static=1');
219-
expect(buildQueryWithParams('/search?q=:term', fromParams)).toBe('/search');
218+
expect(buildQueryWithParams('/search?q=:term&static=1', fromParams)).toBe(
219+
'/search?q=undefined&static=1',
220+
);
221+
});
222+
223+
it('should not replace placeholders of repeated query parameters', () => {
224+
const fromParams = new URLSearchParams('a=1&b=2');
225+
expect(buildQueryWithParams('/search?q=:a&q=:b', fromParams)).toBe('/search?q=:a&q=:b');
220226
});
221227
});
222228

0 commit comments

Comments
 (0)