Skip to content

refactor(@angular/ssr): parse route paths and redirect URLs into structured segments and query params - #34502

Open
alan-agius4 wants to merge 2 commits into
angular:mainfrom
alan-agius4:refactor/ssr-structured-url-parsing
Open

alan-agius4 wants to merge 2 commits into
angular:mainfrom
alan-agius4:refactor/ssr-structured-url-parsing

Conversation

@alan-agius4

@alan-agius4 alan-agius4 commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Follow-up to #34467 to parse route paths and redirect URLs up front into structured PathSegment and QueryParam discriminated unions (static, param, wildcard), decoupling resolveRedirectTo and buildUrlWithParams from string-splitting offsets while preserving fast paths for static redirects and reusing a single UrlTree shell when serializing dynamic query parameters.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request refactors URL and route parsing in Angular SSR by introducing structured types (PathSegment, QueryParam, ParsedUrl) and robust parsing/serialization functions to handle static, parameter, and wildcard segments. It also updates route redirection logic and switches positional wildcards to 0-based indexing. Feedback on these changes suggests optimizing performance by parsing path segments outside of the resolveParam callback to avoid repeated execution, and improving robustness by wrapping the serialization of the shared mutable serializationTree singleton in a try...finally block to ensure its state is always reset.

Comment thread packages/angular/ssr/src/routes/ng-routes.ts
Comment thread packages/angular/ssr/src/utils/url.ts
@alan-agius4 alan-agius4 added the action: review The PR is still awaiting reviews from at least one requested reviewer label Oct 23, 2026
@alan-agius4
alan-agius4 requested a review from dgp1130 October 23, 2026 06:39
@alan-agius4 alan-agius4 added the target: minor This PR is targeted for the next minor release label Oct 23, 2026

@dgp1130 dgp1130 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for looking into this! Hopefully makes manipulating these paths a little cleaner.

* - `static`: A literal query parameter value (single string or repeated `string[]`).
* - `param`: A named query parameter placeholder (`?q=:term` -> `name: 'q'`, `param: 'term'`).
*/
export type QueryParam =

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Consider: Would it be simpler to treat this like a Map<Name, Value> rather than Array<{name: string, value: string}? Does the order matter for query parameters?

* - `param`: A named query parameter placeholder (`?q=:term` -> `name: 'q'`, `param: 'term'`).
*/
export type QueryParam =
| { readonly type: 'static'; readonly name: string; readonly value: string | string[] }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Question: What does the string[] case represent? Are you allowed to do ?q=foo&q=bar?

readonly segments: readonly PathSegment[];
readonly queryParams: readonly QueryParam[];
readonly fragment: string | null;
readonly rawSuffix: string;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Question: Why do we need this? IIUC, it seems like this contains the ?foo or #bar text, which feels like it's just ignoring the parsing behavior we're intending to use here?


describe('parseUrl and serializePathSegments', () => {
it('should parse path segments and query parameters into discriminated unions', () => {
const parsed = parseUrl('/user/:id/*/details/*0/**?q=:term&q2=:a&q2=:b&static=1#section');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

TIL: *0 is a thing we accept.

if (POSITIONAL_WILDCARD_REGEX.test(segment)) {
return { type: 'wildcard', index: Number(segment.slice(1)) };
}
} else if (segment[0] === ':' && segment.length > 1) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Consider: I think this will parse /:/ as a static segment with the text :. Is that the behavior we want?

*/
export function serializePathSegments(segments: readonly PathSegment[]): string {
let result = '';
for (const segment of segments) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggestion: Might be simpler with segments.map(serializePathSegment).join('/')?

/**
* Extracts raw non-empty segment strings from a URL pathname without allocating empty strings.
*/
function extractPathSegments(path: string): string[] {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Question: How is this different from parsePathSegment above? It looks pretty similar.

const segments = parsePathSegments(path);
const hasDynamicSegments = segments.some((segment) => segment.type !== 'static');

if (rawSuffix[0] === '?' && rawSuffix.includes('=:')) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Bug: What about /?foo=myfavoriteemojiistheonethatlookslikeguilefromstreetfighter=:isntthatsocool

Comment on lines +329 to +332
const path = suffixIndex === -1 ? url : url.slice(0, suffixIndex);
const rawSuffix = suffixIndex === -1 ? '' : url.slice(suffixIndex);
const segments = parsePathSegments(path);
const hasDynamicSegments = segments.some((segment) => segment.type !== 'static');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Consider: Can we just use new Url(url, 'localhost') and read path / query parameters / fragment off that object? I know we've had issues with inconsistent parsing behavior before.

expect(serializePathSegments(parsed.segments)).toBe('/user/:id/*/details/*0/**');
});

it('should normalize leading, trailing, and consecutive slashes in parsePathSegments', () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Question: Do we need to care about backslashes \?

@alan-agius4 alan-agius4 added action: merge The PR is ready for merge by the caretaker and removed action: review The PR is still awaiting reviews from at least one requested reviewer labels Oct 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

action: merge The PR is ready for merge by the caretaker area: @angular/ssr target: minor This PR is targeted for the next minor release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants