5

I noticed that the "cors" check takes longer than I expected. This has happened at different speeds from localhost, qa and production.

I am using axios (^0.18.0) that is using mobx/MST/reactjs and asp.net core api 2.2

I can have preflight options that range from a 20 milliseconds to 10 seconds and it will randomly change.

For instance I have

https://localhost:44391/api/Countries

This is a get request and it can take 20 milliseconds 9 times in a row (me ctrl + F5) but on the 10th time it decides to take seconds (I don't get seconds really on localhost but sometimes a second).

enter image description here

So this test, the 204 (cors request) takes 215ms where the actual request that brings back the data takes half the time. This seems backwards.

enter image description here

enter image description here

This is my ajax request

 const axiosInstance = axios.create({
      baseURL: 'https://localhost:44391/api/Countries',
      timeout: 120000,
      headers: {
        contentType: 'application/json',
      }})

      axiosInstance.get();

Here is my startup. I made cors all open and wanted to refine it after I get this issue solved.

public class Startup
    {
        public IHostingEnvironment HostingEnvironment { get; }
        private readonly ILogger<Startup> logger;
        public Startup(IConfiguration configuration, IHostingEnvironment env, ILogger<Startup> logger)
        {
            Configuration = configuration;
            HostingEnvironment = env;
            this.logger = logger;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddCors();

            services.AddDbContext<ApplicationDbContext>(options =>
            {
                options.UseLazyLoadingProxies();
                options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));

            });

            services.AddIdentity<Employee, IdentityRole>(opts =>
            {
                opts.Password.RequireDigit = false;
                opts.Password.RequireLowercase = false;
                opts.Password.RequireUppercase = false;
                opts.Password.RequireNonAlphanumeric = false;
                opts.Password.RequiredLength = 4;
                opts.User.RequireUniqueEmail = true;

            }).AddEntityFrameworkStores<ApplicationDbContext>().AddDefaultTokenProviders();

            services.AddAuthentication(opts =>
            {
                opts.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
                opts.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                opts.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            }).AddJwtBearer(cfg =>
            {
                cfg.RequireHttpsMetadata = false;
                cfg.SaveToken = true;
                cfg.TokenValidationParameters = new TokenValidationParameters()
                {
                    // standard configuration
                    ValidIssuer = Configuration["Auth:Jwt:Issuer"],
                    ValidAudience = Configuration["Auth:Jwt:Audience"],
                    IssuerSigningKey = new SymmetricSecurityKey(
                    Encoding.UTF8.GetBytes(Configuration["Auth:Jwt:Key"])),
                    ClockSkew = TimeSpan.Zero,

                    // security switches
                    RequireExpirationTime = true,
                    ValidateIssuer = true,
                    ValidateIssuerSigningKey = true,
                    ValidateAudience = true
                };
            });

            services.AddAuthorization(options =>
            {
                options.AddPolicy("CanManageCompany", policyBuilder =>
                {
                    policyBuilder.RequireRole(DbSeeder.CompanyAdminRole, DbSeeder.SlAdminRole);
                });

                options.AddPolicy("CanViewInventory", policyBuilder =>
                {
                    policyBuilder.RequireRole(DbSeeder.CompanyAdminRole, DbSeeder.SlAdminRole, DbSeeder.GeneralUserRole);
                });

                options.AddPolicy("AdminArea", policyBuilder =>
                {
                    policyBuilder.RequireRole(DbSeeder.SlAdminRole);
                });
            });


            // do di injection about 30 of these here
            services.AddTransient<IService, MyService>();


            services.AddSingleton(HostingEnvironment);

            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);


            services.AddTransient<IValidator<CompanyDto>, CompanyDtoValidator> ();
            services.AddTransient<IValidator<BranchDto>, BranchDtoValidator>();
            services.AddTransient<IValidator<RegistrationDto>, RegistrationDtoValidator>();

            JsonConvert.DefaultSettings = () => {
                return new JsonSerializerSettings()
                {
                    NullValueHandling = NullValueHandling.Ignore,
                    MissingMemberHandling = MissingMemberHandling.Ignore,
                    ContractResolver = new CamelCasePropertyNamesContractResolver()
                };
            };

        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseHsts();
            }

            //TODO: Change this.
            app.UseCors(builder => builder
                .AllowAnyOrigin()
                .AllowAnyMethod()
                .AllowAnyHeader()
                .AllowCredentials());


            app.UseHttpsRedirection();
            app.UseAuthentication();
            app.UseMvc();

        }
    }

I don't know if this is a valid test, but it does mimic what I am seeing in qa/production at random times.

I changed the axios request to ...

 const axiosInstance = axios.create({
      baseURL: 'https://localhost:44391/api/Countries/get',
      timeout: 120000,
      headers: {
        contentType: 'application/json',
      }})

      axiosInstance.get();

basically I put /get, which causes a 404

enter image description here

enter image description here

enter image description here

Yet, when I refresh my page with the exact same scenario it is completed in milliseconds again (though still slower than the 404)

Edit

I made a hosted site nearly identical to my real site. The only difference is this one is only using http and not https.

http://52.183.76.195:82/

It is not as slow as my real site, but the preflight right now can take 40ms while the real request takes 50ms.

I am testing it in latest version of chrome and you will have to load up the network tab and load/click the button (no visual output is displayed).

enter image description here

JohnH
  • 1,920
  • 4
  • 25
  • 32
chobo2
  • 83,322
  • 195
  • 530
  • 832

2 Answers2

2

I'm not sure if you are using a load balancer or some other proxy service, but one quick fix would be to move the CORS response headers to the load balancer level. This will minimize the overhead that your app may add at any given time. How to setup nginx as a cors proxy service can be found in How to enable CORS in Nginx proxy server?

Ahmet
  • 906
  • 1
  • 8
  • 22
  • No I am not using any load balancer and I did try a proxy service but it did not seem to do anything. Though a balancer would help but no one is using the site right now but me and I am getting it, so I am not sure why the load would be already so much. – chobo2 Jun 05 '19 at 19:25
  • I understand that no one is using the site right now, but I think to create a future proof architecture, it wouldn't be such a bad idea to add it as a layer early on. Most cloud providers allow you to add it with just a simple click of a button. However, for local development as you said, a proxy could be easier. I had success with nginx proxy service. Something like https://enable-cors.org/server_nginx.html So essentially. You disable CORS from your own app, and make the proxy service/load balancer return the required headers. – Ahmet Jun 05 '19 at 19:28
  • What is ngix proxy service? I not really sure what I am looking at in your link. – chobo2 Jun 05 '19 at 20:44
  • Sorry. You are right, that link didn't give the complete picture. In essence, you can start a nginx server locally, and make it run as a proxy. So that when it forwards the incoming requests to your app. And when returning the responses, it can add the CORS headers that you seek. https://stackoverflow.com/a/45994114/1588156 Seems like it provides a more complete example. You can change all of the headers as you prefer. And since CORS requests will be handled by nginx, it will have the lowest overhead hence faster and consistent performance you desire. – Ahmet Jun 05 '19 at 20:51
  • ok, I will look more into this proxy server thought he link you pointed to seems to be for node.js where I am using iis and such(though I did see something about iis 7 on the first link). would this be the same thing as https://cors-anywhere.herokuapp.com/ ? – chobo2 Jun 05 '19 at 21:19
  • What do you mean change all of the headers? – chobo2 Jun 05 '19 at 21:20
  • Also will be using azure for the final production release so if there are suggestions for that. – chobo2 Jun 05 '19 at 21:24
  • When you use `services.AddCors();`, you tell your app to return the CORS headers where needed, so cross-domain ajax calls work. So when a browser tries to make an ajax call to a domain other than the one on the address bar, it first sends an `OPTIONS` request, to make sure it is allowed by the site. Essentially, your web server must be able to respond with the expected response headers. By setting up your load balancer or a proxy server, you intercept these `OPTIONS` requests and respond with the CORS headers without ever reaching your app. – Ahmet Jun 05 '19 at 21:39
  • That herokuapp link, and any other setup that returns the CORS headers will be the same in essance. In this link you can see how to setup directly in Azure https://learn.microsoft.com/en-us/azure/app-service/app-service-web-tutorial-rest-api#enable-cors You can enable them through the Azure on the server side, or in your app. They all do the same. But since your app may be under heavy load, returning the headers before reaching the app will give you lower latency. – Ahmet Jun 05 '19 at 21:44
  • What I mean by "change all of the headers" is that, in the example link, there were some example headers, by changing them, you can make your CORS be more restrictive or more liberal. – Ahmet Jun 05 '19 at 21:47
  • so are you saying herokuapp does the same thing as your suggesting? As I tried using it and I did not really notice any difference with my problem. – chobo2 Jun 05 '19 at 21:55
  • Well if I done it in my statups.cs why would i have to run it in azure again? – chobo2 Jun 05 '19 at 21:56
  • Exactly my point, you don't have to have it in startups.cs. You can handle CORS requests on the server layer without ever reaching your app. So if you setup your load balancer or proxy server or azure correctly, you will not need `services.AddCors();` on your startup.cs, and because of this, the ajax request will have a shorter round trip, and will help you achieve the consistent performance you seek. I suggest you remove it from startup.cs, and get it to work before it reaches your app. – Ahmet Jun 05 '19 at 22:03
  • As you can see from https://learn.microsoft.com/en-us/azure/app-service/app-service-web-tutorial-rest-api#app-service-cors-vs-your-cors once you setup Azure CORS, your app CORS will not have any effect. Think of this like installing your SSL certificate on the load balancer or proxy server or azure server, rather than your app directly. You essentially offload the work required to another layer. – Ahmet Jun 05 '19 at 22:04
  • ok so what could I do for localhost/qa server(what is not azure) if I would do it at the web.config level(not sure if this will work with core, but would hope it would) https://enable-cors.org/server_iis7.html or do I have to go ngix proxy service? – chobo2 Jun 05 '19 at 22:08
  • For iis7, you can add your custom response headers as described here https://learn.microsoft.com/en-us/iis/configuration/system.webserver/httpprotocol/customheaders/#how-to and the headers that you want are `Access-Control-Allow-Origin: *`, `Access-Control-Allow-Headers: *`, `Access-Control-Allow-Methods: *` – Ahmet Jun 05 '19 at 22:17
  • I am using iis 10 I think, is it the same thing? Can I not do it via the web.config? Also would this speed up things vs doing it through code? – chobo2 Jun 05 '19 at 22:58
  • I'm not sure how to use web.config with dotnet core, but if you know how, you can use it of course. Or if you prefer a GUI, you can set it from iis 10 control panel. The settings should be pretty much the same. As long as you set the CORS headers on the server side, it shouldn't matter which method you adopt to set them. – Ahmet Jun 05 '19 at 23:10
  • ok I will try it out for now, though I do want to not use all (*) wildcard in the end but for now just see if it makes any difference. Then I can disable the check I got in my cors code correct? – chobo2 Jun 05 '19 at 23:14
  • I just wrote `*`, because I didn't know your domain names. You can change them to whatever you actually want. I would suggest add the CORS headers to iis, and remove cors from startup.cs. And give it a try. Hopefully it will work as you desired. – Ahmet Jun 05 '19 at 23:17
  • Though looking at every example they always do cors for asp.net core via the startup file, but I will try it to just see what happens. – chobo2 Jun 05 '19 at 23:17
  • Examples usually don't show how to setup server side settings for simplicity sake. Since your question was about performance, my answer is about how to make it work more performant. – Ahmet Jun 05 '19 at 23:19
0

Recently I tried to use Azure Front Door as a load balancer to solve this issue of CORS. You can read about it here:

https://www.devcompost.com/post/using-azure-front-door-for-eliminating-preflight-calls-cors

Although I used Azure Front Door but you can use any load balancer which can repurposed as an ingress controller (like ng-inx) to solve the same problem.

The basic premise is that I am load balancing the UI hosted domain and the APIs under a same domain, thereby tricking the Browser into thinking it's a same origin call. Hence the OPTIONS request is not made anymore.

Pratik Bhattacharya
  • 3,596
  • 2
  • 32
  • 60