CORS HTTP 405 Status Code
Seany84 opened this issue · comments
Describe the bug
I have created an API gateway with YARP and have a couple of downstream APIs.
The problem I am facing is that when running locally, my web client app calls the API gateway, I am getting:
https://localhost:64540/xxx/v1/yyy/zzz 405 (Method Not Allowed)
I have researched this as much as possible and have found a handful or blogs e.g. https://blog.agchapman.com/bypassing-cors-with-yarp-proxy/ but I cannot seem to rectify this CORS issue.
Output from API gateway HTTP logging:
[21:28:06 DBG] Connection id "0HN331AO8SB31" accepted.
[21:28:06 DBG] Connection id "0HN331AO8SB31" started.
[21:28:06 DBG] Connection 0HN331AO8SB31 established using the following protocol: Tls12
[21:28:06 INF] Request starting HTTP/1.1 OPTIONS https://localhost:64540/REDACTED/v1/xxx/yyy - null null
[21:28:06 DBG] 1 candidate(s) found for the request path '/REDACTED/v1/xxx/yyy'
[21:28:06 DBG] Endpoint 'REDACTED' with route pattern 'REDACTED/{**catch-all}' is valid for the request path '/REDACTED/v1/xxx/yyy'
[21:28:06 DBG] Request matched endpoint 'REDACTED'
[21:28:06 DBG] The request has an origin header: 'https://localhost:5173'.
[21:28:06 INF] CORS policy execution successful.
[21:28:06 DBG] The request is a preflight request.
[21:28:06 DBG] Connection id "0HN331AO8SB31" completed keep alive response.
[21:28:06 INF] Request finished HTTP/1.1 OPTIONS https://localhost:64540/REDACTED/v1/xxx/yyy - 204 null null 14.9071ms
[21:28:06 INF] Request starting HTTP/1.1 GET https://localhost:64540/REDACTED/v1/xxx/yyy - null null
[21:28:06 DBG] 1 candidate(s) found for the request path '/REDACTED/v1/xxx/yyy'
[21:28:06 DBG] Endpoint 'REDACTED' with route pattern 'REDACTED/{**catch-all}' is valid for the request path '/REDACTED/v1/xxx/yyy'
[21:28:06 DBG] Request matched endpoint 'REDACTED'
[21:28:06 DBG] The request has an origin header: 'https://localhost:5173'.
[21:28:06 INF] CORS policy execution successful.
[21:28:06 DBG] Static files was skipped as the request already matched an endpoint.
[21:28:06 INF] Executing endpoint 'REDACTED'
[21:28:06 INF] Proxying to https://localhost:64660/v1/xxx/yyy HTTP/2 RequestVersionOrLower
[21:28:06 INF] Received HTTP/2.0 response 405.
[21:28:06 INF] Executed endpoint 'REDACTED'
[21:28:06 DBG] Connection id "0HN331AO8SB31" completed keep alive response.
[21:28:06 INF] Request finished HTTP/1.1 GET https://localhost:64540/REDACTED/v1/xxx/yyy - 405 0 null 105.5603ms
[21:28:07 INF] Start processing HTTP request POST http://localhost:4318/v1/traces
[21:28:07 INF] Sending HTTP request POST http://localhost:4318/v1/traces
[21:28:12 INF] Received HTTP response headers after 4077.6925ms - 502
[21:28:12 INF] End processing HTTP request after 4078.9004ms - 502
I have the following code configuration implemented on the API gateway project:
Program.cs
var builder = CommonApi.CreateWebApplicationBuilder(args);
var services = builder.Services;
services.AddHttpLogging(logging =>
{
logging.LoggingFields = HttpLoggingFields.All;
logging.RequestBodyLogLimit = 4096;
logging.ResponseBodyLogLimit = 4096;
});
services.AddApplicationDependencies(builder.Configuration, builder.Environment);
services.AddYarp(builder.Configuration);
services.AddCors(opt =>
{
opt.AddPolicy("corsGatewayPolicy", policyBuilder =>
{
policyBuilder
.AllowAnyOrigin()
.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod();
});
});
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
HttpClient.DefaultProxy = new WebProxy();
}
app.MapReverseProxy();
app.UseCors("corsGatewayPolicy");
app.UseSwagger();
app.UseSwaggerUI(options =>
{
var config = app.Services.GetRequiredService<IOptionsMonitor<ReverseProxyDocumentFilterConfig>>().CurrentValue;
foreach (var cluster in config.Clusters)
{
options.SwaggerEndpoint($"/swagger/{cluster.Key}/swagger.json", cluster.Key);
}
});
app.Run();
Extension method to add YARP to servicecollection:
public static IServiceCollection AddYarp(this IServiceCollection services, IConfiguration config)
{
services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
var endpointSection = new EndpointSection();
config.Bind(endpointSection);
services
.AddReverseProxy()
.LoadFromMemory(YarpRouteService.Routes(endpointSection.Endpoints.BoundImplementations),
YarpRouteService.Clusters(endpointSection.Endpoints.BoundImplementations))
.AddSwagger(YarpRouteService.GetSwaggerConfig(endpointSection.Endpoints.BoundImplementations));
services.Configure((Action<ReverseProxyDocumentFilterConfig>) (overriddenConfig =>
{
overriddenConfig.Swagger = YarpRouteService.GetSwaggerConfig(endpointSection.Endpoints.BoundImplementations).Swagger;
}));
return services;
}
Custom YarpService class
public static class YarpRouteService
{
public static List<RouteConfig> Routes(List<IEndpoint> endpoints)
{
var routes = new List<RouteConfig>();
if (routes == null) throw new ArgumentNullException(nameof(routes));
foreach (var endpoint in endpoints)
{
routes.Add(new RouteConfig
{
RouteId = $"{endpoint.RouteName}",
ClusterId = $"{endpoint.RouteName}-module",
Order = endpoint.Order,
CorsPolicy = "corsGatewayPolicy",
Match = new RouteMatch { Path = $"{endpoint.RouteName}/{{**catch-all}}" },
Transforms = new []
{
new Dictionary<string, string>
{
{"PathPattern", "{**catch-all}"}
},
new Dictionary<string, string>
{
{"ResponseHeader", "Access-Control-Allow-Origin"},
{"Set", "*"}
}
}
});
}
return routes;
}
public static List<ClusterConfig> Clusters(List<IEndpoint> endpoints)
{
var clusters = new List<ClusterConfig>();
if (clusters == null) throw new ArgumentNullException(nameof(clusters));
clusters.AddRange(endpoints.Select(endpoint =>
new ClusterConfig { ClusterId = $"{endpoint.RouteName}-module",
Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase) {
{
"destination1", new DestinationConfig() { Address = endpoint.Uri }
}
}
}));
return clusters;
}
public static ReverseProxyDocumentFilterConfig GetSwaggerConfig(List<IEndpoint> endpoints)
{
var cluster = new Dictionary<string, ReverseProxyDocumentFilterConfig.Cluster>();
foreach (var endpoint in endpoints)
{
cluster.Add($"{endpoint.RouteName}-module", new ReverseProxyDocumentFilterConfig.Cluster
{
Destinations = new Dictionary<string, ReverseProxyDocumentFilterConfig.Cluster.Destination>
{
{
"destination1", new ReverseProxyDocumentFilterConfig.Cluster.Destination
{
Address = endpoint.Uri,
Swaggers = new[]
{
new ReverseProxyDocumentFilterConfig.Cluster.Destination.Swagger
{
PrefixPath = $"/{endpoint.RouteName}",
Paths = new[] {"/swagger/v1/swagger.json"}
}
}
}
}
}
});
}
return new ReverseProxyDocumentFilterConfig
{
Swagger = new ReverseProxyDocumentFilterConfig.SwaggerConfig
{
CommonDocumentName = "YARP", IsCommonDocument = false
},
Routes = Routes(endpoints).ToDictionary(_ => _.RouteId, _ => _),
Clusters = cluster
};
}
To Reproduce
Further technical details
- Include the version of the packages you are using
- The platform (Linux/macOS/Windows)
Why do you believe this is a CORS issue? The client did make the request to the gateway.
Judging by the logs, YARP forwarded the GET request to the backend service (https://localhost:64660/v1/xxx/yyy
), and it responded with a 405. Are you sure that the client is using the correct method for this request?
I would recommend looking at the logs in the backend service to see why it decided to reject the request.
Why do you believe this is a CORS issue? The client did make the request to the gateway.
Judging by the logs, YARP forwarded the GET request to the backend service (
https://localhost:64660/v1/xxx/yyy
), and it responded with a 405. Are you sure that the client is using the correct method for this request? I would recommend looking at the logs in the backend service to see why it decided to reject the request.
You were completely correct, it ended up being a HTTP verb change that was missed when I replaced the legacy API gateway.
Many thanks for the second pair of eyes :)