I must be doing something wrong here but I can't figure it out; it seems to be a CORS issue from what I can tell. I need to expose Access-Control-Expose-Headers: * to any origin but dotnet core 2.1 isn't doing what I expect.
Relevant Startup.cs code:
public void ConfigureServices(IServiceCollection services)
{
//Mapping settings to POCO and registering with container
var settings = new AppSettings.ReportStorageAccountSettings();
Configuration.Bind(nameof(AppSettings.ReportStorageAccountSettings), settings);
services.AddCors(options =>
{
options.AddPolicy("AllowAll",
builder =>
{
builder
.AllowAnyHeader()
.AllowAnyMethod()
.AllowAnyOrigin()
.AllowCredentials();
});
});
services.AddSingleton(settings);
services.AddApiVersioning();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
// 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();
}
app.UseCors("AllowAll");
app.UseHttpsRedirection();
app.UseMvc();
}
This application is hosted in Azure and I have added a * entry to the CORS settings in Azure just for good measure. Now, whenever the client application (which is also hosted in Azure) makes a post request, the headers are not accessible via JS and Access-Control-Expose-Headers: * is not present in the response. However, I can see the headers when I inspect the network response and when using Fiddler. I have tried Axios and Jquery for accessing the headers to rule out any issues with the JS. What am I doing wrong here?
In the controller I respond with:
Response.Headers.Add("Location", $"api/someLocation");
return StatusCode(StatusCodes.Status202Accepted);
The CorsPolicyBuilder's AllowAnyHeader method configures the Access-Control-Allow-Headers response header, which is used only for preflighted requests. The Access-Control-Expose-Headers response header is what's needed, which is configured using WithExposedHeaders.
Here's a complete example:
services.AddCors(options =>
{
options.AddPolicy("AllowAll", builder =>
{
builder.AllowAnyHeader()
.AllowAnyMethod()
.AllowAnyOrigin()
.AllowCredentials()
.WithExposedHeaders("Location"); // params string[]
});
});
As Kirk mentioned .WithExposedHeaders() method is what is needed.
Another variation to Kirk's answer is:
// in Startup.cs
// at the end of ConfigureServices() add:
services.AddCors();
// at the top of Configure() add:
app.UseCors(x => x.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader().WithExposedHeaders("*"));
Related
Please look at my long answer at the end about how I resolved this. I had gotten too frustrated and after another day with a fresh perspective and more sleep, I got to a solution.
I did this in 5.0 with no issues in the Startup.Configure method.
Basically I created a header for the request on a protected route. I'm using React as the front end. I'm finding when I place everything in Program.cs the dependency injection, authorization doesn't work right so I split up into separate Program and Startup files.
But I can't use the following signature in 6.0 like I did in 5.0:
example that worked in 5.0:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IAntiforgery antiforgery)
{
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("antiforgery/token", context =>
{
var tokens = antiforgery.GetAndStoreTokens(context);
context.Response.Headers.Append("XYZ", tokens.RequestToken!);
return Task.FromResult(StatusCodes.Status200OK);
});
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
});
}
Program.cs (my attempt to split up program and startup - 6.0)
var startup = new dolpassword.Startup(builder.Configuration);
startup.ConfigureServices(builder.Services);
var app = builder.Build();
startup.Configure(app,app.Environment);
Saw this example on Microsoft website:
app.UseRouting();
app.UseAuthorization();
// app.Services syntax error in Configure for 6.0
var antiforgery = **app.Services.GetRequiredService<IAntiforgery>();**
app.Use((context, next) =>
{
var requestPath = context.Request.Path.Value;
if (string.Equals(requestPath, "/",
StringComparison.OrdinalIgnoreCase)
|| string.Equals(requestPath, "/index.html",
StringComparison.OrdinalIgnoreCase))
{
var tokenSet = antiforgery.GetAndStoreTokens(context);
context.Response.Cookies.Append("XSRF-TOKEN",
tokenSet.RequestToken!,
new CookieOptions { HttpOnly = false });
}
return next(context);
});
I was able to successfully do this in 6.0 so I will share some of the code and how I resolved it. I also had Windows authentication baked in with a policy-based authorization. The reason I'm putting all the authentication/authorization wireup in this post is because the entire solution relies on authentication, authorization and antiforgery.
First I set up my services. I get IAntiforgery by default by adding ControllersWithViews but I want to use my own header name, which is X-XSRF-TOKEN instead of the .AspNet.Antiforgery.xxxx or whatever the default is. I also needed options.DefaultAuthenticateScheme = NegotiateDefaults.AuthenticationScheme; to get Windows auth working.
string CorsPolicy = "CorsPolicy";
//===================================formerly Configure Services
WebApplicationBuilder? builder = WebApplication.CreateBuilder(args);
ConfigurationManager _configuration = builder.Configuration;
// Add services to the container.
**IServiceCollection? services = builder.Services;
services.AddAntiforgery(options => { options.HeaderName = "X-XSRF-TOKEN";
options.Cookie.HttpOnly = false; });**
services.AddTransient<IActiveDirectoryUserService, ActiveDirectoryUserService>();
services.AddControllersWithViews();
services.AddAuthentication(options => {//needed for Windows authentication
options.DefaultAuthenticateScheme = NegotiateDefaults.AuthenticationScheme;
});
Adding more...
I'm using Windows auth so I'm using the Negotiate provider. Then I set up my Authorization. I insert my own authorization policy and also add my claims transformers to get the authenticated user into a claim. The fallback policy in Authorization was causing an Authentication exception.
services.AddAuthorization(options =>
{
// options.FallbackPolicy = options.DefaultPolicy;//authorization bombs if you include this line
options.AddPolicy("AuthenticatedOnly", policy => {
policy.Requirements.Add(new AuthenticatedRequirement(true));
});
});
services.AddTransient<IClaimsTransformation, MyClaimsTransformer>();
services.AddTransient<IAuthorizationHandler, AppUserRoleHandler>();
services.AddTransient<IAuthorizationHandler, AuthenticatedRoleHandler>();
services.AddCors(options =>
{
options.AddPolicy(CorsPolicy,
builder => builder
.WithOrigins("https://localhost:7021","https://localhost:44414")
//Note: The URL must be specified without a trailing slash (/).
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
});
Now I'm in the middleware territory...as you know, order matters in your middleware! In 5.0 you could add IAntiforgery to your constructor and DI would handle the rest. In program.cs you don't have that luxury. Fortunately you can just grab it out of your services collection and you see that in the following code.
//==============formerly Startup.Configure=====================
WebApplication app = builder.Build();
app.UseAuthentication();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseCookiePolicy();
app.UseCors(CorsPolicy);
IAntiforgery? antiforgery = app.Services.GetRequiredService<IAntiforgery>();
Now when I'm setting up my endpoint routing. Found out that UseRouting and Use.Endpoints are married at the hip and need to be paired.
I also create a protected route "/auth" (protected by my authorization policy) to grab the antiforgery request token generated when we added it in the services collection. So this header won't be persisted from request to request like a cookie would. The minimal API allows me to create a route without creating the controller and action in a separate controller class.
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/auth", context =>
{
var tokens = antiforgery.GetAndStoreTokens(context);
context.Response.Headers.Append("XYZ", tokens.RequestToken!);
return Task.FromResult(Results.Ok());
}).RequireAuthorization("AuthenticatedOnly");
endpoints.MapControllerRoute(
name: "default"
pattern: "{controller}/{action=Index}/{id?}");
});
My React front end will use a fetch get request to get the token from the headers collection and then stick into a second post request and voila it works.
BTW, React doesn't provide Antiforgery functionality out of the box like Angular, in step with it's minimalist API ethos.
The action I'm posting to looks like this:
[HttpPost]
[Authorize(Policy="AuthenticatedOnly")]
[ValidateAntiForgeryToken]
public string Update()
I fully realize there are other ways to do this.
I have a full-stack app in ASP.NET Core 5. The front-end is React and the back-end is OData.
I need to use app.UseStatusCodePagesWithReExecute("/"); in my Configure() method to redirect any unknown requests to index.html, as routing is handled by the client-side code.
The problem is that in OData standard, when a key in a GET request is invalid, it returns a 404 error. This error will cause a redirect to index.html as well.
My Question: How can I exclude any request that starts with /odata.svc from UseStatusCodePagesWithReExecute()?
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// generated swagger json and swagger ui middleware
// You can access the swagger ui at /swagger/index.html
app.UseSwagger();
app.UseSwaggerUI(x => x.SwaggerEndpoint("/swagger/v1/swagger.json", "ASP.NET Core Sign-up and Verification API"));
//app.UseCors("CorsPolicy");
// global cors policy
app.UseCors(x => x
.SetIsOriginAllowed(origin => true)
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.All
});
app.UseRouting();
// global error handler
app.UseMiddleware<ErrorHandlerMiddleware>();
// custom jwt auth middleware
app.UseMiddleware<JwtMiddleware>();
app.UseEndpoints(endpoints =>
{
endpoints.Select().Expand().Filter().OrderBy().Count().MaxTop(30);
// add an endpoint for an actual domain model
// registered this endpoint with name odata (first parameter)
// and also with the same prefix (second parameter)
// in this route, we are returning an EDM Data Model
endpoints.MapODataRoute("odata.svc", "odata.svc", GetEdmModel(app.ApplicationServices));
endpoints.EnableDependencyInjection();
endpoints.MapControllers();
// enable serving static files
endpoints.MapDefaultControllerRoute();
});
// Redirects any unknown requests to index.html
app.UseStatusCodePagesWithReExecute("/");
app.UseHttpsRedirection();
// Serve default documents (i.e. index.html)
app.UseDefaultFiles();
//Set HTTP response headers
const string cacheMaxAge = "1";
// Serve static files
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
// using Microsoft.AspNetCore.Http;
ctx.Context.Response.Headers.Append(
"Cache-Control", $"public, max-age={cacheMaxAge}");
}
});
}
This seems like an ideal use case for the UseWhen() extension method. This functionality is (under)documented by Microsoft, though #Paul Hiles has a more comprehensive write-up about it on his DevTrends blog. Basically, this allows you to conditionally inject middleware into your execution pipeline.
So, to conditionally exclude UseStatusCodePagesWithReExecute() if your request path starts with /odata.svc, you would simply wrap your UseStatusCodePagesWithReExecute() call with a UseWhen() condition, as follows:
app.UseWhen(
context => context.Request.Path.StartsWithSegments("/odata.svc"),
appBuilder =>
{
appBuilder.UseStatusCodePagesWithReExecute("/");
}
);
I'm having some troubles setting up the correct Cors settings on my application.
NetCore application with Angular6 hosted on IIS, the angular app is outside the .Net project and compiled and inserted inside the wwwroot when publishing.
Now the problem is that when I'm coding the angular part, I'd like to call directly the release server to test some functionality.
I tried any kind of approach to have this work out but it seems like I'm always hitting a problem with the Cors setup but only for the POST calls, GET works fine. So here is my startup code:
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<ILoggerManager, LoggerManager>();
services.AddSingleton<IDbContext, MongoDbContext>();
services.AddSingleton<IIdGenerator, IdGenerator>();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info { Title = "My API", Version = "v1" });
c.AddSecurityDefinition("Bearer", new ApiKeyScheme(){In = "header",Description = "Please enter JWT with Bearer into field", Name = "Authorization", Type = "apiKey"});
c.AddSecurityRequirement(new Dictionary<string, IEnumerable<string>>
{
{"Bearer", Enumerable.Empty<string>()}
});
});
services.AddCustomRepositories();
services.AddCustomServices();
services.AddJwtService();
//Add cors
services.AddCors();
// Add framework services.
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.Configure<IISOptions>(options =>
{
options.ForwardClientCertificate = false;
});
services.Configure<Settings>(Configuration.GetSection("SystemSettings"));
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
//app.UseCors("AllowSpecificOrigin");
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseCors(builder => builder
.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod());
app.UseDefaultFiles();
// this will serve js, css, images etc.
app.UseStaticFiles();
app.Use(async (context, next) =>
{
if (context.Request.Path.HasValue &&
!context.Request.Path.Value.StartsWith("/api/") &&
!context.Request.Path.Value.StartsWith("/swagger"))
{
context.Response.ContentType = "text/html";
await context.Response.SendFileAsync(
env.ContentRootFileProvider.GetFileInfo("wwwroot/index.html")
);
return;
}
await next();
});
//app.UseHttpsRedirection();
app.UseSwagger();
app.UseAuthentication();
app.UseMiddleware(typeof(ErrorHandlingMiddleware));
app.UseMvc();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
c.DocExpansion(DocExpansion.None);
});
}
Since I want this enabled only for development purpose I'd like to enable it globally.
i think you need to set the origin manually
app.UseCors(builder =>
builder.AllowAnyOrigin().AllowAnyMethod());
as shown in here
This's the necessary part to add cors in your web API.
I'm at wit's end on this one. I've already researched other answers to similar questions on SO w/o any luck.
I'm fairly certain I've got CORS enabled correctly to allow incoming requests (in this case, POST requests) from all origins, but I'm seeing the error below:
Failed to load http://localhost:5000/expenses: No
'Access-Control-Allow-Origin' header is present on the requested
resource. Origin 'http://localhost:4200' is therefore not allowed
access. The response had HTTP status code 500.
Here's how I've enabled CORS in my webAPI project:
relevant methods in Startup.cs
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddCors();
services.AddMvc();
services.AddDbContext<ExpensesDbContext>(options =>
options.UseMySQL(Configuration.GetConnectionString("DefaultConnection")));
services.AddTransient<IBaseDa<Accounts>, AccountsDataAccess>();
services.AddTransient<IExpensesDa, ExpensesDa>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
env.EnvironmentName = "Development";
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseCors(builder => builder
.AllowAnyHeader()
.AllowAnyMethod()
.AllowAnyOrigin()
.AllowCredentials());
app.UseMvc();
}
If i'm using .AllowAnyOrigin() and .AllowAnyMethod(), why am I seeing the error above?
Was scratching my head on this situation here for a while. I had CORS enabled properly, but some calls were still returning the Access-Control-Allow-Origin error. I found the problem... the sneaky sneaky problem...
Our problem was caused by how we were using app.UseExceptionHandler. Specifically, here's the code we were using, except our original code didn't have the context.Response.Headers.Add("Access-Control-Allow-Origin", "*"); line.
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
var errorFeature = context.Features.Get<IExceptionHandlerFeature>();
var exception = errorFeature.Error;
var problemDetails = new ProblemDetails
{
Title = R.ErrorUnexpected,
Status = status,
Detail =
$"{exception.Message} {exception.InnerException?.Message}"
};
context.Response.Headers.Add("Access-Control-Allow-Origin", "*");
context.Response.StatusCode = problemDetails.Status.GetValueOrDefault();
context.Response.WriteJson(problemDetails, "application/problem+json");
await Task.CompletedTask;
});
});
app.UseExceptionHandler is a much lower level function than controller actions, and thus does not take part in anything related to CORS natively. Adding context.Response.Headers.Add("Access-Control-Allow-Origin", "*"); fixed the problem.
The combination of netCore2.0 (http://localhost:5000/) + Angular (http://localhost:4200) + chrome = Access-Control-Allow-Origin. I have had this issue before and it took me 3 days to realize that chrome will always throw this error. I think it is because chrome views localhost as the origin disregarding the port even tho the middleware explicitly tells it not too especially on POST requests.
I would try and define a policy in your startup.cs Configure services:
public void ConfigureServices(IServiceCollection services)
{
//add cors service
services.AddCors(options => options.AddPolicy("Cors",
builder =>
{
builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
}));
then in your Configure method I would add that:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
//authentication added
app.UseAuthentication();
app.UseCors("Cors");
app.UseMvc();
}
... This most likely wont work and but try it any who.... This drove me mad and I needed the satisfaction of seeing if the request even attempted to hit the asp.netCore backend:
I used
If you really want to see I would clear your cache and cookies then add
IHttpContextAccessor to get low level control of whats going on in the request.
In my dilema with the same problem I needed angular to send an image. I was getting the annyoing
Origin error then through exprimenting I got the Image by injecting IHttpContextAccessor into my controller and
debugging
public void ConfigureServices(IServiceCollection services)
{
//add cors service
services.AddCors(options => options.AddPolicy("Cors",
builder =>
{
builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
}));
services.AddMvc();
// register an IHttpContextAccessor so we can access the current
// HttpContext in services by injecting it
//---we use to this pull out the contents of the request
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
}
you want to inject this into whatever controller u are
using to retrieve the json object of the POST request. Im going to use the example as Home controller:
public class HomeController : Controller
{
// make a read only field
private readonly IHttpContextAccessor _httpContextAccessor;
//create ctor for controller and inject it
public UserService(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
// now in your post method use this to see what the if anything came in through the request:
public async Task<IActionResult> Picload(IFormFile file){//---always null
// in my problem I was loading and image from.
var file = _httpContextAccessor.HttpContext.Request.Form.Files[0];
}
Using this it gave me access to the image chrome was giving me an Origin error about.
My crystal ball tells me that somewhere in your C# code an error appear in time of execution and this way the "return" statement of the service is never executed. So debug your code and fix the error so the response is returned to the browser.
I'm totally stuck on implementing Windows authentication for one of my .NET Core apps that uses Aurelia for client side.
The Aurelia application is hosted on port:9000 and the .NET WebAPI is hosted on port:9001.
The idea is to serve static pages from my .NET app once the app is published but now in development I use port:9000 because of the BrowserSync provided by Aurelia.
When I use port:9000 it's all fine and dandy and I have no issues posting or getting.
If I switch to port:9001 I can still get but not post. Posting results in 401 Unauthorized.
If we look at the headers for port:9000 requests..
Get(success):
Post(failed):
You can see that there are multiple headers missing in the post for some reasons, most importantly the authentication cookie..
Base-Repo.js
import {inject} from 'aurelia-framework';
import {HttpClient, json} from 'aurelia-fetch-client';
import {AppSettings} from '../infrastructure/app-settings';
#inject(HttpClient, AppSettings)
export class BaseRepo {
constructor(http, appSettings) {
http.configure(config => {
config
.withDefaults({
credentials: 'include',
headers: {
'Accept': 'application/json'
}
})
.withInterceptor({
request(request) {
console.log(`Requesting ${request.method} ${request.url}`);
return request;
},
response(response) {
console.log(`Received ${response.status} ${response.url}`);
return response;
}
})
});
this.http = http;
this.baseUrl = appSettings.api;
}
get(url) {
console.log('BaseRepo(get): ' + url);
return this.http.fetch(this.baseUrl + url)
.then(response => { return response.json(); })
.then(data => { return data; });
}
post(url, data) {
console.log('BaseRepo(post): ' + url, data);
return this.http.fetch(this.baseUrl + url, {
method: 'post',
body: json(data)
})
.then(response => response.json())
.then(data => { return data; });
}
}
Why is GET working but not POST when using BrowserSync port?
Edit 1
Post(success) for port:9001:
Edit 2
Console message post error:
OPTIONS http://localhost:9001/api/MYURLS 401 (Unauthorized)
Fetch API cannot load
http://localhost:9001/api/MYURLS.
Response to preflight request doesn't pass access control check: No
'Access-Control-Allow-Origin' header is present on the requested
resource. Origin 'http://localhost:9000' is therefore not allowed
access. The response had HTTP status code 401. If an opaque response
serves your needs, set the request's mode to 'no-cors' to fetch the
resource with CORS disabled.
Edit 3
Startup.cs
public class Startup
{
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
env.ConfigureNLog("nlog.config");
}
public IConfigurationRoot Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy",
builder => builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
});
services.AddMemoryCache();
services.AddMvc();
services.InjectWebServices();
services.AddOptions();
//call this in case you need aspnet-user-authtype/aspnet-user-identity
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddSingleton<IConfiguration>(Configuration);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.UseCors("CorsPolicy");
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.UseMvc();
app.UseDefaultFiles();
app.UseStaticFiles();
//add NLog to ASP.NET Core
loggerFactory.AddNLog();
//add NLog.Web
app.AddNLogWeb();
}
}
I enabled "Enable Anonymous Authentication" in project properties and voila...
Before I only had "Enable Windows Authenticaiton" enabled, now both ports work!
When application is deployed this wont be enabled anyway because by then I will use the real IIS.
Update 1
After upgrading to .net core 2.0 I was no longer able to enable both Windows Authentication and Anonymous Authentication.
After some research I found out you have to add:
services.AddAuthentication(IISDefaults.AuthenticationScheme);
in your startup.cs in order for it to work.
More info can be found in comment section and docs.
Update 2
You need Microsoft.AspNetCore.Authentication package for authentication builder.
You will need to enable CORS in your ASP.NET Core project. There's information on how to do this here: https://learn.microsoft.com/en-us/aspnet/core/security/cors.
You need to call AddCors in ConfigureServices:
services.AddCors();
And then UseCors in Configure:
// Shows UseCors with CorsPolicyBuilder.
app.UseCors(builder => builder.WithOrigins("http://example.com"));
When you're using port 9000, you're on a different origin to the API, but with 9001, you're on the same origin and therefore CORS will not apply.
The OPTIONS requests are known as "preflighting". There's more information on those here: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Preflighted_requests.