visual studio – WebApplicationBuilder: What triggers launchUrl to actually launch the browser?


After upgrading to .NET 10, launchUrl and launchBrowser in appsettings.json stopped working under Kestrel. Upgrading to .NET 10 is a big change because IStartup needs to be replaced and I’m reasonably certain something broke launchUrl in the conversion.

To be clear:

This works:

    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },

This does not:

    "http": {
      "commandName": "Project",
      "launchBrowser": true,
      "launchUrl": "http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },

The failure mode: the application is running and is listening, but the browser never opens. If the browser is manually opened and pointed at the web application, everything works.

The startup code:

        System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
        //Snip irrelevant
        var config = CreateSettings(out var sources);
        //Snip irrelevant
        var builder = WebApplication.CreateEmptyBuilder(new WebApplicationOptions { ApplicationName = AppName, Args = args });
        builder.Configuration.SetBasePath(Directory.GetCurrentDirectory());
        builder.Configuration.Sources.AddRange(sources);
        builder.Configuration.AddEnvironmentVariables();

        builder.WebHost
            .UseKestrel(options =>
            {
                // Snip loading production port config
                // Looks essentially like this:
                // if (port > 0)
                //      options.Listen(System.Net.IPAddress.Loopback, port);
                // but port is 0 in developer environment
                options.AllowSynchronousIO = true;
            })
            .UseContentRoot(Directory.GetCurrentDirectory())
            .ConfigureLogging((logging) =>
            {
                logging.SetMinimumLevel(config.LogLevel);
                logging.ClearProviders();
                logging.AddConsole();
#if DEBUG
                logging.AddDebug();
#endif
            })
            .UseIISIntegration()
            .UseIIS() // Enables use of IIS in the case of IIS launch; does not force IIS.
            .ConfigureServices((services) =>
            {
                Startup.ConfigureServices(services, config, builder.Environment);
            });
        
        builder.WebHost
            .UseSetting(WebHostDefaults.ApplicationKey, AppName);

        var app = builder.Build();
        Startup.Configure(app, app.Environment, config);
        app.Run();

The contents of Startup.Configure are the same as the .NET 9 code using IStartup.

Virtually identical working startup code:

        var builder = WebApplication.CreateEmptyBuilder(new WebApplicationOptions { ApplicationName = AppName, Args = args });
        builder.Configuration.SetBasePath(Directory.GetCurrentDirectory());
        builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
        builder.Configuration.AddJsonFile("devsettings.json", optional: true, reloadOnChange: true);
        builder.Configuration.AddEnvironmentVariables();
        builder.WebHost
            .UseKestrel(options =>
            {
                // Trim rading config file.
                System.Net.IPAddress bindto;
                Action<Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions> https =
                    listenOptions => {
                        if (!string.IsNullOrEmpty(certificate))
                            listenOptions.UseHttps(certificate, certificatepw);
                        // force http 1 to avoid the http2 cipher suite black list
                        // https://github.com/aspnet/AspNetCore/issues/14350
                        // https://tools.ietf.org/html/rfc7540#appendix-A
                        if (forcehttp1)
                            listenOptions.Protocols = Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols.Http1;
                    };
                options.Listen(bindto, httpsport, https);
            })
            .ConfigureLogging((logging) =>
            {
                logging.ClearProviders();
                logging.AddConsole();
#if DEBUG
                logging.AddDebug();
#endif
            })
            .UseIISIntegration()
            .UseIIS() // Enables use of IIS in the case of IIS launch; does not force IIS.
            .ConfigureServices((services) =>
            {
                // It looks like you can do this but you can't.
                // services.AddCors(options => options.AddPolicy("AllowAll", p => p.AllowAnyOrigin().AllowCredentials()));
                // The CORS protocol does not allow specifying a wildcard (any) origin and credentials at the same time. Configure the CORS policy by listing individual origins if credentials needs to be supported.
                // But you can say it the long way.
                services.AddCors((options) =>
                    options.AddPolicy("Enable", (builder) =>
                        builder.SetIsOriginAllowed(_ => true)
                            .WithMethods("GET")
                            .AllowAnyHeader()
                            .AllowCredentials())
                );
                services.AddRouting();
            })
            .UseSetting(WebHostDefaults.ApplicationKey, AppName);

        var app = builder.Build();
        app.UseCors();
        // Trim MapGet()
        app.Run();

Hypotheses:

  • explicitly calling listen matters. Result: false
  • calling AddJsonFile matters. Result: false
  • logging.SetMinimumLevel matters. Result: false

Leave a Reply

Your email address will not be published. Required fields are marked *