microsoft / reverse-proxy

A toolkit for developing high-performance HTTP reverse proxy applications.

Home Page:https://microsoft.github.io/reverse-proxy

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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 :)