Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Since 19.1: SSR Apps with urlMatcher in route config are breaking due to commit 6edb908 #29384

Closed
1 task done
denisyilmaz opened this issue Jan 17, 2025 · 16 comments
Closed
1 task done
Labels
needs: more info Reporter must clarify the issue

Comments

@denisyilmaz
Copy link

Command

serve

Is this a regression?

  • Yes, this behavior used to work in the previous version

The previous version in which this bug was not present was

19.0

Description

With 19.0 it was possible to use UrlMatcher functions within SSR applications. 19.1 introduced a commit 6edb908 which was meant to fix a incompatibality of UrlMatcher with SSR applications. I wasn't able to find any documentation that indicated UrlMatcher and SSR are not working together. Was this a issue that needed to be solved? The only issue i was able to find on the web was in combination with the @angular-architects/module-federation library (https://dev.to/michaeljota/how-to-add-module-federation-into-your-angular-micro-frontend-apps-jmh)

Before we start refactoring our project (which requires SSR and also UrlMatcher) or downgrading to 19.0 I would like to know what the implications were for this "fix". Happy for any insights.

Minimal Reproduction

  1. create project with ssr enabled with 19.1+
  2. add UrlMatcher function to route
  3. open project with ng serve

Exception or Error

Error: Error(s) occurred while extracting routes:
- The route 'en/example' uses a route matcher which is not supported.
- The route 'en/example' uses a route matcher which is not supported.
- The route 'en/example-2' uses a route matcher which is not supported.
- The route 'en/example-2' uses a route matcher which is not supported.
- The route 'example' uses a route matcher which is not supported.
- The route 'example' uses a route matcher which is not supported.
- The route 'example-2' uses a route matcher which is not supported.
- The route 'example-2' uses a route matcher which is not supported.
    at eval (/../angular/.angular/cache/19.1.1/main/vite/deps_ssr/chunk-FOZQYQAA.js:10655:15)
    at _ZoneDelegate.invoke (/../angular/.angular/vite-root/main/node_modules/zone.js/fesm2015/zone-node.js:369:28)
    at ZoneImpl.run (/../angular/.angular/vite-root/main/node_modules/zone.js/fesm2015/zone-node.js:111:43)
    at eval (/../angular/.angular/vite-root/main/node_modules/zone.js/fesm2015/zone-node.js:1221:40)
    at _ZoneDelegate.invokeTask (/../angular/.angular/vite-root/main/node_modules/zone.js/fesm2015/zone-node.js:402:33)
    at ZoneImpl.runTask (/../angular/.angular/vite-root/main/node_modules/zone.js/fesm2015/zone-node.js:159:47)
    at drainMicroTaskQueue (/../angular/.angular/vite-root/main/node_modules/zone.js/fesm2015/zone-node.js:581:35)
    at invokeTask (/../angular/.angular/vite-root/main/node_modules/zone.js/fesm2015/zone-node.js:487:21)
    at Timeout.ZoneTask.invoke (/../angular/.angular/vite-root/main/node_modules/zone.js/fesm2015/zone-node.js:472:48)
    at Timeout.data.args.<computed> (/../angular/.angular/vite-root/main/node_modules/zone.js/fesm2015/zone-node.js:2260:32)

Your Environment

_                      _                 ____ _     ___
    / \   _ __   __ _ _   _| | __ _ _ __     / ___| |   |_ _|
   / △ \ | '_ \ / _` | | | | |/ _` | '__|   | |   | |    | |
  / ___ \| | | | (_| | |_| | | (_| | |      | |___| |___ | |
 /_/   \_\_| |_|\__, |\__,_|_|\__,_|_|       \____|_____|___|
                |___/
    

Angular CLI: 19.1.1
Node: 20.18.1
Package Manager: npm 10.8.2
OS: darwin arm64

Angular: 19.1.1
... animations, cli, common, compiler, compiler-cli, core, forms
... platform-browser, platform-browser-dynamic, platform-server
... router, ssr

Package                         Version
---------------------------------------------------------
@angular-devkit/architect       0.1901.1
@angular-devkit/build-angular   19.1.1
@angular-devkit/core            19.1.1 (cli-only)
@angular-devkit/schematics      19.1.1
@angular/cdk                    19.1.0
@schematics/angular             19.1.1
ng-packagr                      19.0.1
rxjs                            7.8.1
typescript                      5.6.3
zone.js                         0.15.0

Anything else relevant?

No response

@alan-agius4
Copy link
Collaborator

alan-agius4 commented Jan 17, 2025

Before this change, the URL matchers did not function correctly at runtime. With this update, an error is shown during build time instead. For more details, refer to: #29284.

I’m interested in understanding the specific use cases where route matchers are used. Could you share yours, along with a code snippet demonstrating your implementation?

@denisyilmaz
Copy link
Author

denisyilmaz commented Jan 17, 2025

@alan-agius4 thanks for the input. You are right, on local development the matcher is not working. But on our staging server with a production build the routes with urlMatcher are rendered correctly on the server.

happy to share our use case:

We have a route that lists subpages of various levels in a chronical order. The pages can have related events to it which will have a nested permalink

for simplicity here are the path created by the backend:

/overview
/overview/page-1
/overview/page-1/page-2/page-3
/overview/page-1/events/event-1
/overview/page-1/page-2/events/event-2

as you can see the "events" urlSegments indicate that the last element is a event. As Angulars router config does not allow wildcards before url segements like **/events/:slug the only way to get this to work was using the UrlMatcher function. As mentioned above, on the production build it was working without any issues.

Current implementation (which was working in 19.0):

export default [
  {
    path: '',
    resolve: {
      nav: navigationResolver,
    },
    children: [
      {
        path: '',
        loadComponent: () =>
          import('./overview/overview.component'),
        resolve: {
          page: overviewResolver,
        },
      },
      {
        path: 'past',
        loadComponent: () => import('./archive/archive.component'),
        resolve: {
          page: archiveResolver,
        },
      },
      {
        matcher: relatedEvent,
        loadComponent: () =>
          import('../events/event-detail/event-detail.component'),
      },
      {
        matcher: defaultPage,
        loadComponent: () => import('./default-page/default-page.component'),
        resolve: {
            page: defaultPageResolver,
        },
      },
    ],
  },
] satisfies Route[];

relatedEvent matcher function

export function relatedEvent(url: UrlSegment[]): UrlMatchResult | null {
  const isEventDetail = url.length > 2 && url.at(-2)?.path === 'events';
  const slug = url.at(-1);
  return isEventDetail && slug ? { consumed: url, posParams: { slug } } : null;
}

defaultPage matcher function

export function defaultPage(url: UrlSegment[]): UrlMatchResult | null {
  if (url.length >= 1) {
    const isNotEvent = !(
      url.length >= 2 && url.at(-2)?.path === 'events'
    );
    const slug = url.at(-1);
    return isNotEvent && slug ? { consumed: url, posParams: { slug } } : null;
  }
  return null;
}

Ideal implementation if Angular route config would allow wildcards (not working in any version of Angular):

export default [
  {
    path: '',
    resolve: {
      nav: navigationResolver,
    },
    children: [
      {
        path: '',
        loadComponent: () =>
          import('./overview/overview.component'),
        resolve: {
          page: overviewResolver,
        },
      },
      {
        path: 'past',
        loadComponent: () => import('./archive/archive.component'),
        resolve: {
          page: archiveResolver,
        },
      },
      {
        path: '**/:slug',
        loadComponent: () => import('./default-page/default-page.component'),
        resolve: {
            page: defaultPageResolver,
        },
        children: [
            {
                path: 'events/:event-slug',
                loadComponent: () => import('../events/event-detail/event-detail.component'),
            }
        ]
      },
    ],
  },
] satisfies Route[];

Maybe there is another way to have n-wildcard-urlSegments before a routerParameter is defined, but i'm not aware of such a solution. The urlMatcher function was giving me exactly this feature. Happy for any help here.

@alan-agius4
Copy link
Collaborator

Based on the provided URL structure, it seems unnecessary to create a custom matcher function. Instead, you can use Angular's built-in matcher and define nested routes with multiple levels or parameters as required.

For example:

import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: 'overview',
    children: [
      {
        path: '',
        loadComponent: () => import('./overview/overview.component'),
      },
      {
        path: 'past',
        loadComponent: () => import('./past/past.component'),
      },
      {
        path: ':event',
        children: [
          {
            path: '',
            loadComponent: () => import('./events/events.component'),
          },
          {
            path: 'past',
            loadComponent: () => import('./past/past.component'),
          },
          {
            path: ':event-detail',
            loadComponent: () =>
              import('./events-details/events-details.component'),
          },
        ],
      },
    ],
  },
];

@denisyilmaz
Copy link
Author

denisyilmaz commented Jan 17, 2025

@alan-agius4 true, but this would mean i need to create several child routes and cannot guarantee a "n-deep" hierachy.

import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: 'overview',
    children: [
      {
        path: '',
        loadComponent: () => import('./overview/overview.component'),
      },
      {
        path: 'past',
        loadComponent: () => import('./past/past.component'),
      },
      {
        path: ':event',
        children: [
          {
            path: '',
            loadComponent: () => import('./events/events.component'),
          },
          {
            path: ':sub-event',
            children: [
              {
                path: '',
                loadComponent: () => import('./../events/events.component'),
              },
              {
                path: ':sub-sub-event',
                children: [
                  {
                    path: '',
                    loadComponent: () => import('./../../events/events.component'),
                  },
                  {
                      children: [
                         ... // this would require manual repitition depending on the allowed levels (currently there should not be any limit to the editors)
                      ]
                  },
                  {
                    path: 'events/:event-detail',
                    loadComponent: () =>
                      import('./../../events-details/events-details.component'),
                  },
                ],
              },
              {
                path: 'events/:event-detail',
                loadComponent: () =>
                  import('./../events-details/events-details.component'),
              },
            ],
          },
          {
            path: 'events/:event-detail',
            loadComponent: () =>
              import('./events-details/events-details.component'),
          },
        ],
      },
    ],
  },
];

@alan-agius4
Copy link
Collaborator

Oh I missed that sorry

@denisyilmaz
Copy link
Author

denisyilmaz commented Jan 17, 2025

@alan-agius4 do you think the urlMatcher will work together with SSR anytime soon? until the new project is launched (we are in the alpha phase) we could disable SSR so we can still update to 19.1

@denisyilmaz
Copy link
Author

denisyilmaz commented Jan 18, 2025

@alan-agius4 I'm just a bit confused as i understood that the matcherfunction is overwriting the defaultUrlMatcher which is working with SSR, isnt it?

const matcher = route.matcher || defaultUrlMatcher;

https://github.com/angular/angular/blob/16b08853bcd3719aec1781949e8adcf0e558a033/packages/router/src/utils/config_matching.ts#L80

@magnolo
Copy link

magnolo commented Jan 20, 2025

We have no issues here with SSR and custom route matcher on version 19.0. No 404s, no errors. Set everything to RenderMode.Server. hmm...

@alan-agius4
Copy link
Collaborator

alan-agius4 commented Jan 20, 2025

During SSR, two routers are in use: the traditional Angular router and an additional Angular SSR router (Which is currently in developer preview). The latter, found here, requires routes to be serializable. While route matchers could function possible this was due to a catch-all route being present in the Angular router, which tricked the SSR Router to ignore the matchers.

That said, the Angular SSR router, which is currently in developer preview, does not support custom route matchers. This feature is being tracked in #29284

@denisyilmaz
Copy link
Author

@alan-agius4 thanks for the clarification.

My first guess was that switchting from AngularNodeAppEngine back to CommonEngine would resolve this. But i get the same error with the CommonEngine implementation.

So my question: is there a way to update to Angular 19.1 without using the new SSR Router?

@alan-agius4
Copy link
Collaborator

alan-agius4 commented Jan 20, 2025

To switch back to the CommonEngine, you'd also need to remove outputMode from your angular.json.

@denisyilmaz
Copy link
Author

denisyilmaz commented Jan 20, 2025

hm, even with usage of CommonEngine, setting outputMode to static (in angular.json) and the removal of provideServerRoutesConfig()/renderMode the app (with 19.1) is crashing because of the matcher functions.

@alan-agius4
Copy link
Collaborator

You need to remove outputMode completely.

@celilunver
Copy link

celilunver commented Jan 21, 2025

how to catch routes like /[text]--[id]. without urlMatcher

@denisyilmaz
Copy link
Author

denisyilmaz commented Jan 21, 2025

@celilunver i guess until UrlMatcher is supported by the new AngularNodeAppEngine we need to stick to the old CommonEngine implementation. There is a bug though in 19.1.3 which was already fixed and i hope it will be released soon. See #29420

@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Feb 22, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
needs: more info Reporter must clarify the issue
Projects
None yet
Development

No branches or pull requests

4 participants