JavaScriptServices cannot host multiple SPAs #213

Closed
opened 2025-08-09 17:15:26 +00:00 by fergalmoran · 0 comments
Owner

Originally created by @DarthRainbows on 1/24/2018

Functional impact

I am attempting to host multiple SPA's by making multiple calls to app.UseSpa, but whichever SPA is registered first intercepts all incoming requests.

Minimal repro steps

These steps will use Angular with Angular's i18n tools to generate multiple apps, but any scenario with multiple apps should produce the same results.

  1. install Microsoft.DotNet.Web.Spa.ProjectTemplates::2.0.0-rc1-final
  2. create a new app: dotnet new angular
  3. navigate to ClientApp
  4. run npm install
  5. Create a second SPA. I don't think it matters how you do this, I found the issue by using Angular's i18n tools to compile multiple localized versions of the app, but for simplicity, I duplicated it by copying ./ClientApp to ./ClientApp2, and adjusting the build/serve scripts in the package.json files to deploy to different paths:
    1. ./ClientApp/package.json scripts:
      "start": "ng serve --extract-css --deploy-url=/app1/ --base-href=/app1/",
      "build": "ng build --extract-css --deploy-url=/app1/ --base-href=/app1/",
      
    2. ./ClientApp2/package.json scripts:
      "start": "ng serve --extract-css --deploy-url=/app2/ --base-href=/app2/",
      "build": "ng build --extract-css --deploy-url=/app2/ --base-href=/app2/",
      
    3. ./ClientApp2/src/app/app.component.html add an identifier so we can differentiate the apps:
      <div class='container-fluid'>
        <div class='row'>
          <div class='col-sm-3'>
            <app-nav-menu></app-nav-menu>
          </div>
          <div class='col-sm-9 body-content'>
            <h2>App 2</h2>
            <router-outlet></router-outlet>
          </div>
        </div>
      </div>
      
    4. ./Startup.cs configure two SPAs:
      app.UseSpa(spa =>
      {
          // To learn more about options for serving an Angular SPA from ASP.NET Core,
          // see https://go.microsoft.com/fwlink/?linkid=864501
      
          spa.Options.SourcePath = "ClientApp";
          spa.Options.DefaultPage = "/app1/index.html";
      
          if (env.IsDevelopment())
          {
              spa.UseAngularCliServer(npmScript: "start");
          }
      });
      
      app.UseSpa(spa =>
      {
          // To learn more about options for serving an Angular SPA from ASP.NET Core,
          // see https://go.microsoft.com/fwlink/?linkid=864501
      
          spa.Options.SourcePath = "ClientApp2";
          spa.Options.DefaultPage = "/app2/index.html";
      
          if (env.IsDevelopment())
          {
              spa.UseAngularCliServer(npmScript: "start");
          }
      });
      
  6. Start the server with dotnet run
  7. Navigate to http://localhost:{port}/app1; app1 loads as expected
  8. Navigate to http://localhost:{port}/app2; app1 is loaded, and you are redirected to /app1

If we disable the first call to app.UseSpa or reverse the order, app2 loads instead of app1.

Expected result

Navigating to the base path of an SPA should route to that specific SPA.

Actual result

Whichever SPA is configured first handles all requests.

Further technical details

While I am a newcomer to .Net Core MVC/Javascript services, from what I can tell so far, it looks like the SPA configuration needs an option to specify a route to handle requests for.

Microsoft.AspNetCore.SpaServices.SpaDefaultPageMiddleware.Attach rewrites all requests to point to options.DefaultPage, which is then either handled by the middleware attached by app.UseSpaStaticFilesInternal or caught by an error handler. In my example, a request coming in to /app2 gets pointed to /app1/index.html, which is served up by the static file middleware for app1, and so never gets to the middleware configured by the second call to app.UseSpa.

Overriding the default file provider (as suggested in code comments) doesn't help, because the request path is still being overwritten, so we can't distinguish the apps at the file provider. Even if we could, the request would be caught by the error handler before it could get to the middleware configured for the second SPA.

The path rewrite and the error handler need to be aware of the SPA base path, so requests can pass to the appropriate apps. Perhaps something akin to the way routing is handled in .Net Core MVC?

*Originally created by @DarthRainbows on 1/24/2018* ### Functional impact I am attempting to host multiple SPA's by making multiple calls to `app.UseSpa`, but whichever SPA is registered first intercepts all incoming requests. ### Minimal repro steps These steps will use Angular with Angular's i18n tools to generate multiple apps, but any scenario with multiple apps should produce the same results. 1. install Microsoft.DotNet.Web.Spa.ProjectTemplates::2.0.0-rc1-final 1. create a new app: `dotnet new angular` 1. navigate to `ClientApp` 1. run `npm install` 1. Create a second SPA. I don't think it matters how you do this, I found the issue by using Angular's i18n tools to compile multiple localized versions of the app, but for simplicity, I duplicated it by copying `./ClientApp` to `./ClientApp2`, and adjusting the build/serve scripts in the `package.json` files to deploy to different paths: 1. `./ClientApp/package.json` scripts: ```json "start": "ng serve --extract-css --deploy-url=/app1/ --base-href=/app1/", "build": "ng build --extract-css --deploy-url=/app1/ --base-href=/app1/", ``` 1. `./ClientApp2/package.json` scripts: ```json "start": "ng serve --extract-css --deploy-url=/app2/ --base-href=/app2/", "build": "ng build --extract-css --deploy-url=/app2/ --base-href=/app2/", ``` 1. `./ClientApp2/src/app/app.component.html` add an identifier so we can differentiate the apps: ```html <div class='container-fluid'> <div class='row'> <div class='col-sm-3'> <app-nav-menu></app-nav-menu> </div> <div class='col-sm-9 body-content'> <h2>App 2</h2> <router-outlet></router-outlet> </div> </div> </div> ``` 1. `./Startup.cs` configure two SPAs: ```C# app.UseSpa(spa => { // To learn more about options for serving an Angular SPA from ASP.NET Core, // see https://go.microsoft.com/fwlink/?linkid=864501 spa.Options.SourcePath = "ClientApp"; spa.Options.DefaultPage = "/app1/index.html"; if (env.IsDevelopment()) { spa.UseAngularCliServer(npmScript: "start"); } }); app.UseSpa(spa => { // To learn more about options for serving an Angular SPA from ASP.NET Core, // see https://go.microsoft.com/fwlink/?linkid=864501 spa.Options.SourcePath = "ClientApp2"; spa.Options.DefaultPage = "/app2/index.html"; if (env.IsDevelopment()) { spa.UseAngularCliServer(npmScript: "start"); } }); ``` 1. Start the server with `dotnet run` 1. Navigate to http://localhost:{port}/app1; app1 loads as expected 1. Navigate to http://localhost:{port}/app2; app1 is loaded, and you are redirected to /app1 If we disable the first call to `app.UseSpa` or reverse the order, app2 loads instead of app1. ### Expected result Navigating to the base path of an SPA should route to that specific SPA. ### Actual result Whichever SPA is configured first handles all requests. ### Further technical details While I am a newcomer to .Net Core MVC/Javascript services, from what I can tell so far, it looks like the SPA configuration needs an option to specify a route to handle requests for. [`Microsoft.AspNetCore.SpaServices.SpaDefaultPageMiddleware.Attach`](https://github.com/aspnet/JavaScriptServices/blob/0b53b92bc6cb40122e08b116508e4c2dde806011/src/Microsoft.AspNetCore.SpaServices.Extensions/SpaDefaultPageMiddleware.cs#L13) rewrites all requests to point to `options.DefaultPage`, which is then either handled by the middleware attached by `app.UseSpaStaticFilesInternal` or caught by an [error handler](https://github.com/aspnet/JavaScriptServices/blob/0b53b92bc6cb40122e08b116508e4c2dde806011/src/Microsoft.AspNetCore.SpaServices.Extensions/SpaDefaultPageMiddleware.cs#L39). In my example, a request coming in to /app2 gets pointed to `/app1/index.html`, which is served up by the static file middleware for app1, and so never gets to the middleware configured by the second call to `app.UseSpa`. Overriding the default file provider (as suggested in [code comments](https://github.com/aspnet/JavaScriptServices/blob/0b53b92bc6cb40122e08b116508e4c2dde806011/src/Microsoft.AspNetCore.SpaServices.Extensions/SpaDefaultPageMiddleware.cs#L31)) doesn't help, because the request path is still being overwritten, so we can't distinguish the apps at the file provider. Even if we could, the request would be caught by the error handler before it could get to the middleware configured for the second SPA. The path rewrite and the error handler need to be aware of the SPA base path, so requests can pass to the appropriate apps. Perhaps something akin to the way routing is handled in .Net Core MVC?
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github/JavaScriptServices#213
No description provided.