aspnetcore icon indicating copy to clipboard operation
aspnetcore copied to clipboard

AngularCliMiddleware is not working with the new Angular CLI's build system

Open PeteAtWSA opened this issue 2 years ago • 10 comments

Is there an existing issue for this?

  • [X] I have searched the existing issues

Describe the bug

Serving an Angular 17 SPA build with the new Angular CLI's build system. Angular.io guide for esbuild

UseAngularCliServer(npmScript: start) is running into a timeout.

For example, consider the following code in Startup.cs:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
  app.UseSpa(
      spa =>
      {
          spa.Options.SourcePath = "ClientApp";app.UseSpa(
      spa =>
      {
          spa.Options.SourcePath = "ClientApp";
          spa.UseAngularCliServer("start");
      });
          spa.UseAngularCliServer("start");
      });
}

After a timeout the following error is thrown: "The Angular CLI process did not start listening for requests within the timeout period of 120 seconds. Check the log output for error information."

So it does not support the new Angular build system.

Expected Behavior

The ASP .NET core Backend and the Angular SPI is served.

Steps To Reproduce

Create a new "Angular and ASP.NET Core" project from Visual Studio 2022 project templates. Change the angular.json of the newly created project as following: Using the browser-esbuild builder

The following is what you would typically find in angular.json for an application:

"architect": {
  "build": {
    "builder": "@angular-devkit/build-angular:browser",

Changing the builder field is the only change you will need to make.

"architect": {
  "build": {
    "builder": "@angular-devkit/build-angular:browser-esbuild",

Exceptions (if any)

TimeoutException: The Angular CLI process did not start listening for requests within the timeout period of 120 seconds. Check the log output for error information. Microsoft.AspNetCore.SpaServices.Extensions.Util.TaskTimeoutExtensions.WithTimeout<T>(Task<T> task, TimeSpan timeoutDelay, string message) Microsoft.AspNetCore.SpaServices.Extensions.Proxy.SpaProxy.PerformProxyRequest(HttpContext context, HttpClient httpClient, Task<Uri> baseUriTask, CancellationToken applicationStoppingToken, bool proxy404s) Microsoft.AspNetCore.Builder.SpaProxyingExtensions+<>c__DisplayClass2_0+<<UseProxyToSpaDevelopmentServer>b__0>d.MoveNext() Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context) Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)

.NET Version

8.0.101

Anything else?

In Microsoft.AspNetCore.SpaServices.AngularCli.AngularCliMiddleware.cs is a condition for waiting until the output of the ng serve command is matching a specific regex pattern:

source code

openBrowserLine = await scriptRunner.StdOut.WaitForMatch(
    new Regex("open your browser on (http\\S+)", RegexOptions.None, RegexMatchTimeout));

But the output has changed due to the new Angular build system.

Old build system: 2024-01-11_16h23_02

New build system: 2024-01-11_16h23_28

So the regex pattern does not match any more. This pattern has to be fitted!

PeteAtWSA avatar Jan 11 '24 15:01 PeteAtWSA

my suggestion is to change the regex pattern to "(open your browser on (http\S+)|Local: http\S+)"

PeteAtWSA avatar Jan 12 '24 12:01 PeteAtWSA

Is there any plan to fix this or to allow the use of a custom regex as suggested in this #52325 issue?

pchriste24 avatar Feb 27 '24 16:02 pchriste24

Hi, I have the same problem. I changed to the new build system application and now I receive the timeout after 120 seconds. I tried a lot of things but I couldn't. And I didn't find updated posts talking about this problem.

Any help or anyone got a solution?

Thanks!

SoyDiego avatar Mar 04 '24 13:03 SoyDiego

@PeteAtWSA did you find another solution? Please if you have any help, I appreaciate

SoyDiego avatar Mar 04 '24 15:03 SoyDiego

We are printing the expected text before the serve command:

"start": "echo open your browser on https://localhost:4200/ && ng serve"

PeteAtWSA avatar Mar 05 '24 07:03 PeteAtWSA

We are printing the expected text before the serve command:

"start": "echo open your browser on https://localhost:4200/ && ng serve"

Thanks again, I added to to my package.json

 "start": "echo 'open your browser on https://localhost:4200/' && ng serve",

But I continue failing in the timeout. Any idea why is continue failing?

image

Thanks again @PeteAtWSA

SoyDiego avatar Mar 05 '24 07:03 SoyDiego

Finally I solved my problem. I don't know if the best solution and my knowledge in .NET is 0. I had this code before I changed the build system:

app.UseSpa(spa =>
    {
        spa.Options.SourcePath = "../../MyProject";

        if (environment.IsDevelopment())
        {
            spa.UseAngularCliServer(npmScript: "start");
        }

        spa.Options.DefaultPageStaticFileOptions = new StaticFileOptions
        {
            OnPrepareResponse = ctx =>
            {
                var headers = ctx.Context.Response.GetTypedHeaders();
                headers.CacheControl = new CacheControlHeaderValue
                {
                    NoCache = true,
                    NoStore = true,
                    MustRevalidate = true,
                    MaxAge = TimeSpan.Zero
                };
            }
        };
    });

And I realized that if I run my frontend and backend separately (by executing npm start from my frontend), everything was working perfectly. So I was researching how to execute my process directly from .NET without spa.UseAngularCliServer(npmScript: "start") and I did using ProcessStartInfo.

The final code looks like this:

app.UseSpa(spa =>
    {
        spa.Options.SourcePath = "../../MyProject";

        if (environment.IsDevelopment())
        {
            string angularProjectPath = spa.Options.SourcePath;

            ProcessStartInfo psi = new ProcessStartInfo
            {
                FileName = "npm",
                Arguments = "start",
                WorkingDirectory = angularProjectPath,
                UseShellExecute = true,
            };

            Process process = Process.Start(psi);

            spa.UseProxyToSpaDevelopmentServer("http://localhost:4200");
        }
    });

This code works perfectly, and I didn't need to use any other possible solutions involving modified NuGet packages.

I hope the solution helps others with the same problem!

SoyDiego avatar Mar 07 '24 07:03 SoyDiego

I've found AspNetCore.SpaYarp by Bernd Hirschmann, and after only a few days of experimenting with it, it seems like a better approach than both the legacy and new SPA Proxy methods available with the old and new templates. Moving the YARP proxy to the back-end fixes CORS issues when using back-end authentication middleware, and his approach to monitoring for the start of the SPA side seems much cleaner.

awdorrin avatar Sep 09 '24 12:09 awdorrin

my suggestion is to change the regex pattern to "(open your browser on (http\S+)|Local: http\S+)"

@javiercn @jiayac, can you try to make sure some fix or official workaround is available for this? The development-time experience for the Microsoft-provided ASP.NET Angular template is not only obsolete, but broken with Angular 17+ projects (Angular 19 due next month) that use the new standard Vite/Esbuild. This is a significant issue.

jitterbox avatar Oct 14 '24 23:10 jitterbox

If the regex could be updated to allow for the new output format, that would be really appreciated. In the other linked issue it was mentioned that the package may be deprecated in the future, but it isn't yet, so if the fix for the regex could be put in in a minor release that would be a big help, I think we along with many others depend on this working and need it working with Angular 17, 18, and 19 and the new build system.

mcardoalm avatar Oct 17 '24 14:10 mcardoalm

Pity this issue has not been addressed :(

I used @SoyDiego 's manual process approach.

For it to work with the vite/caching stuff I added the following:

            app.Map("/@fs", applications =>
            {
                applications.UseSpa(spa =>
                {
                    if (Environment.IsDevelopment())
                    {
                        spa.UseProxyToSpaDevelopmentServer("http://localhost:4200");
                    }
                });
            });

Terribly hacky, but will only be for dev. Prebuilt static files seems to have no issues though, except for not being dev friendly.

Update for Angular 19: You want to turn off hmr in the dev-server config due to the way CSS is loaded.

leppie avatar Nov 07 '24 09:11 leppie

Finally I solved my problem. I don't know if the best solution and my knowledge in .NET is 0. I had this code before I changed the build system:

app.UseSpa(spa => { spa.Options.SourcePath = "../../MyProject";

    if (environment.IsDevelopment())
    {
        spa.UseAngularCliServer(npmScript: "start");
    }

    spa.Options.DefaultPageStaticFileOptions = new StaticFileOptions
    {
        OnPrepareResponse = ctx =>
        {
            var headers = ctx.Context.Response.GetTypedHeaders();
            headers.CacheControl = new CacheControlHeaderValue
            {
                NoCache = true,
                NoStore = true,
                MustRevalidate = true,
                MaxAge = TimeSpan.Zero
            };
        }
    };
});

And I realized that if I run my frontend and backend separately (by executing npm start from my frontend), everything was working perfectly. So I was researching how to execute my process directly from .NET without spa.UseAngularCliServer(npmScript: "start") and I did using ProcessStartInfo.

The final code looks like this:

app.UseSpa(spa => { spa.Options.SourcePath = "../../MyProject";

    if (environment.IsDevelopment())
    {
        string angularProjectPath = spa.Options.SourcePath;

        ProcessStartInfo psi = new ProcessStartInfo
        {
            FileName = "npm",
            Arguments = "start",
            WorkingDirectory = angularProjectPath,
            UseShellExecute = true,
        };

        Process process = Process.Start(psi);

        spa.UseProxyToSpaDevelopmentServer("http://localhost:4200");
    }
});

This code works perfectly, and I didn't need to use any other possible solutions involving modified NuGet packages.

I hope the solution helps others with the same problem!

Thanks for this solution! I use it and it works like a charm. If you want to hide the angular process terminal window you can also add WindowStyle = ProcessWindowStyle.Hidden like this:

ProcessStartInfo psi = new ProcessStartInfo
{
  FileName = "npm",
  Arguments = "start",
  WorkingDirectory = angularProjectPath,
  UseShellExecute = true,
  WindowStyle = ProcessWindowStyle.Hidden // <--
};

manguoes avatar Nov 27 '24 12:11 manguoes

There has been an update to Microsoft.AspNetCore.SpaServices.Extensions package and the fix to this issue not in there. Any update on this would be appreciated.

aghazanchyan avatar Jan 18 '25 03:01 aghazanchyan

It seems necessary to specify a port for DevServerPort, set to 4200 for SpaOptions. Otherwise, this change will not work in the package.json 'start' script, which includes 'echo open your browser on https://localhost:4200/ && ng serve'

If you do not specify a port, it will assign a random port Image

mm-ryo avatar Feb 27 '25 02:02 mm-ryo

This is still a problem.

I ran into this exact issue after updating angular CLI from 16 to 19.

Running on .net 8.0.4

Can we please get a formal resolution?

PhilStantonCoding avatar Mar 07 '25 01:03 PhilStantonCoding

@philDaprogrammer I switched to SpaProxy, it removes the need to have any dotnet middleware involved. Http requests that are intended for the dotnet backend can be proxied and it has a better startup experience. https://learn.microsoft.com/en-us/aspnet/core/client-side/spa/intro?view=aspnetcore-7.0&preserve-view=true#developing-single-page-apps

dotjoe avatar Mar 07 '25 01:03 dotjoe