Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CORS HTTP 405 Status Code #2480

Closed
Seany84 opened this issue Apr 22, 2024 · 2 comments
Closed

CORS HTTP 405 Status Code #2480

Seany84 opened this issue Apr 22, 2024 · 2 comments
Labels
Type: Bug Something isn't working

Comments

@Seany84
Copy link

Seany84 commented Apr 22, 2024

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)
@Seany84 Seany84 added the Type: Bug Something isn't working label Apr 22, 2024
@MihaZupan
Copy link
Member

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.

@Seany84
Copy link
Author

Seany84 commented Apr 24, 2024

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

@Seany84 Seany84 closed this as completed Apr 24, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Type: Bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants