1

I'm trying to get my ReactJS app (on an AWS S3 machine) PUT request working with my Server API (on an AWS Windows EC2 machine). Seems I am being tripped up by the preflight message that is being sent out. I've been searching on how to handle this and came across these two stackoverflow posts:

Enable OPTIONS header for CORS on .NET Core Web API

How to handle OPTION header in dot net core web api

I've ensured IIS accepts the OPTIONS verb and have added the middleware described. I can see the OPTIONS preflight handling being called through the logging but for some reason I am still getting the CORS error. Listed the main sections of the code below, any help would be really appreciated.

ReactJS PUT request

    var myHeaders = new Headers();
    myHeaders.append('Accept', 'application/json');
    myHeaders.append('Content-Type', 'application/json-patch+json');

    var rawObject = {
      Name: this.state.recipeEdit.name,
      Type: this.state.recipeTypeEdit,
      Description: this.state.recipeEdit.description,
      Ingredients: this.state.recipeIngredients,
      Steps: this.state.recipeSteps,
    };

    var requestOptions = {
      method: 'PUT',
      headers: myHeaders,
      body: JSON.stringify(rawObject),
      redirect: 'follow',
    };

    fetch(this.state.url, requestOptions)
      .then((response) => response.json())
      .then((data) => {
        this.setState({ recipeDetail: data });
      });

Middleware Class

    public class OptionsMiddleware
    {
        private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
        private readonly RequestDelegate _next;

        public OptionsMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public Task Invoke(HttpContext context)
        {
            return BeginInvoke(context);
        }

        private Task BeginInvoke(HttpContext context)
        {
            if (context.Request.Method == "OPTIONS")
            {
                log.Error("Handling the OPTIONS preflight message");

                context.Response.Headers.Add("Access-Control-Allow-Origin", new[] { (string)context.Request.Headers["Origin"] });
                context.Response.Headers.Add("Access-Control-Allow-Headers", new[] { "Origin, X-Requested-With, Content-Type, Accept" });
                context.Response.Headers.Add("Access-Control-Allow-Methods", new[] { "GET, POST, PUT, DELETE, OPTIONS" });
                context.Response.Headers.Add("Access-Control-Allow-Credentials", new[] { "true" });
                context.Response.StatusCode = 200;
                return context.Response.WriteAsync("OK");
            }

            log.Error("Invoking message");
            return _next.Invoke(context);
        }
    }

    public static class OptionsMiddlewareExtentions
    {
        public static IApplicationBuilder UseOptions(this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<OptionsMiddleware>();
        }
    }

CORS Configuration in Startup.cs

        public void ConfigureServices(IServiceCollection services)
        {
            log.Error("Entered ConfigureServices");

            try
            {
#if DEBUG
                services.AddCors();
#else
                services.AddCors(o => o.AddPolicy("MyCorsPolicy", builder =>
                {
                    builder.SetIsOriginAllowed((host) => true)
                           .AllowAnyMethod()
                           .AllowAnyHeader()
                           .AllowCredentials();
                }));
#endif

                services.AddControllersWithViews().AddNewtonsoftJson();

                services.AddControllersWithViews(options =>
                {
                    options.InputFormatters.Insert(0, GetJsonPatchInputFormatter());
                });

                services.AddMvc(options => options.EnableEndpointRouting = false).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
                services.AddMvc(options => options.Filters.Add(typeof(homebakeExceptionFilter)));

#if USE_SQLITE
                log.Error("Using SQLITE");
                services.AddDbContext<SqliteDbContext>(options =>
                {
                    options.UseSqlite("Data Source=./homebake.db");
                });
#else
            services.AddDbContext<AppDbContext>(options =>
            {
                options.UseInMemoryDatabase("homebakeapp-api-in-memory");
            });
#endif
                log.Error("Adding services");
                services.AddScoped<IIngredientRepository, IngredientRepository>();
                services.AddScoped<IRecipeStepRepository, RecipeStepRepository>();
                services.AddScoped<IRecipeRepository, RecipeRepository>();
                services.AddScoped<IIngredientService, IngredientService>();
                services.AddScoped<IRecipeStepService, RecipeStepService>();
                services.AddScoped<IRecipeService, RecipeService>();
                services.AddScoped<IUnitOfWork, UnitOfWork>();

                log.Error("Adding auto mapper");
                services.AddAutoMapper(typeof(Startup));
            }
            catch (System.Exception ex)
            {
                log.Error(ex.Message);
                if (ex.InnerException != null )
                    log.Error(ex.InnerException);
            }
        }

        private static NewtonsoftJsonPatchInputFormatter GetJsonPatchInputFormatter()
        {
            var builder = new ServiceCollection()
                .AddLogging()
                .AddMvc()
                .AddNewtonsoftJson()
                .Services.BuildServiceProvider();

            return builder
                .GetRequiredService<IOptions<MvcOptions>>()
                .Value
                .InputFormatters
                .OfType<NewtonsoftJsonPatchInputFormatter>()
                .First();

        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddLog4Net();

            log.Error("Entered Configure");
            app.UseOptions();

#if DEBUG
            app.UseCors(options => options.WithOrigins("http://localhost:3000").AllowAnyMethod().AllowAnyHeader());
#else
            log.Error("Using cors policy");
            app.UseCors("MyCorsPolicy");
#endif

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
                app.UseHsts();
            }
            //app.use
            app.UseHttpsRedirection();

            log.Error("Using MVC");
            app.UseMvc();
        }
Hobospy
  • 51
  • 7

2 Answers2

0

I have seen this error when the server-side CORS settings are set using both the web.config and in the code, like in your middleware, which at runtime results in duplicates and cause this type of behavior. Also, you may want to add the following to your web.config and see if it helps. This will ensure your CORS settings are only set by the code.

<httpProtocol>
    <customHeaders>
        <remove name="Access-Control-Allow-Headers" />
        <remove name="Access-Control-Allow-Methods" />
        <remove name="Access-Control-Allow-Origin" />        
    </customHeaders>
</httpProtocol>
Masoud Safi
  • 102
  • 1
  • 6
  • Thanks for that, it didn't look like I had duplicated the CORS settings but I added the code to web.config. I've added further logging to my middleware to output what headers have been added and it looks to be just the headers configured in the middleware code. Is there anyway to determine what part of the response is causing the failure? – Hobospy Jun 08 '20 at 03:55
  • For deeper debugging I would add ``config.EnableSystemDiagnosticsTracing();`` in the ``WebApiConfig.cs`` file's ``Register`` method. More on this feature here: https://learn.microsoft.com/en-us/aspnet/web-api/overview/testing-and-debugging/tracing-in-aspnet-web-api As for the CORS issue, if not already done, I would also add ``config.EnableCors();`` in the same method ``Register``. – Masoud Safi Jun 09 '20 at 05:09
  • Thanks again Masoud. Can you clarify, are these calls based on ASP.Net or .Net Core? I'm using .Net Core and I don't have any Register method. – Hobospy Jun 09 '20 at 11:10
  • OK, so I've been looking at this again tonight. The error I am receiving is that there is no Access-Control-Allow-Origin header present on the requested resource, however if I add to my web.config file I get an error stating that I can't have multiple entries in the Access-Control-Allow-Origin header. Is it me or is something else happening? – Hobospy Jun 09 '20 at 14:11
  • My input is based on ASP.NET, but I suspect the root cause may be the same since the error you are seeing is similar to what I have seen. The fact that it is complaining about "multiple entries" suggests there maybe multiple of one ore more of these CORS settings. – Masoud Safi Jun 09 '20 at 22:45
0

In the end the issue was to do with IIS configuration. After more searching I found the solution here:

How do I enable HTTP PUT and DELETE for ASP.NET MVC in IIS?

Essentially I had to update the ExtensionlessUrlHandler-Integrated-4.0 setting to accept the PUT and DELETE verbs (access it from the Handler Mappings option in IIS) and also disable both the WebDav module and handler. After that the requests went through and were processed correctly. I'm also still running the Middleware code as detailed above in case anyone else ever comes across this issue.

What made me look at the IIS configuration was that I got the multiple entries for Access-Control-Allow-Origin if I added it to my web.config file, if so, how could it be missing when not included there. Big thanks to @Masoud Safi for al the help he gave on this too.

Hobospy
  • 51
  • 7