domaindrivendev / Swashbuckle.AspNetCore

Swagger tools for documenting API's built on ASP.NET Core
MIT License
5.25k stars 1.31k forks source link

How to define base url in swashbuckle 5.0 within asp.net core 3.1 #2004

Open KamranShahid opened 3 years ago

KamranShahid commented 3 years ago

I am using asp.net core 3.1 with swashbuckle 5. I am not able tofind the way for defining base url. RootUrl option seem to be not coming or (may be i am not able to call it)

Please let me know how to define it

 private static void SwaggerInjection(IServiceCollection services)
        {
            services.AddSwaggerGen(x =>
            {               
                x.SwaggerDoc("v1", new OpenApiInfo { Title = "Portal Backend APIs", Version = "v1" });
                x.DescribeAllEnumsAsStrings();
                x.TagActionsBy(api =>
                {
                    if (api.GroupName != null)
                    {
                        return new[] { api.GroupName };
                    }

                    var controllerActionDescriptor = api.ActionDescriptor as ControllerActionDescriptor;
                    if (controllerActionDescriptor != null)
                    {
                        return new[] { controllerActionDescriptor.ControllerName };
                    }

                    throw new InvalidOperationException("Unable to determine tag for endpoint.");
                });
                x.DocInclusionPredicate((name, api) => true);
                x.AddSecurityDefinition("Bearer", // name the security scheme
                    new OpenApiSecurityScheme
                    {
                        Description = "JWT Authorization header using the Bearer scheme.",
                        Type = SecuritySchemeType.Http, // we set the scheme type to http since we're using bearer authentication
                        Scheme = "bearer" // the name of the HTTP Authorization scheme to be used in the Authorization header. In this case "bearer".
                    });
                x.AddSecurityRequirement(new OpenApiSecurityRequirement{
                    {
                        new OpenApiSecurityScheme{
                            Reference = new OpenApiReference{
                                Id = "Bearer", // the name of the previously defined security scheme.
                                Type = ReferenceType.SecurityScheme
                            }
                        },new List<string>()
                    }
                });
                // to show API description and other stuff on swagger
                var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
                var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
                x.IncludeXmlComments(xmlPath);
            });
        }
domaindrivendev commented 3 years ago

I'm not sure what you mean - could you please elaborate on your setup and what you're actually trying to do.

KamranShahid commented 3 years ago

i have my application deployed on some linux server. private ip url http://xxx.xxx.xxx.xxx:4000/swagger/index.html is serving fine. I have deployed same url under with ngnix with url like http://mywebsitebasepath/myvitualpath/swagger/index.html but api url is missing myvitualpath and api address is returning 404 error. Any setting i also needed to do?

domaindrivendev commented 3 years ago

https://github.com/domaindrivendev/Swashbuckle.AspNetCore#dealing-with-proxies-that-change-the-request-path

KamranShahid commented 3 years ago

Changed My function in startup.cs from

private void SwaggerSetup(IApplicationBuilder app)
        {
        var swaggerOptions = new SwaggerOptions();
            Configuration.GetSection(nameof(SwaggerOptions)).Bind(swaggerOptions);
            app.UseSwagger(option => { option.RouteTemplate = swaggerOptions.JsonRoute; });
            app.UseSwaggerUI(option => 
            { 
                option.SwaggerEndpoint(swaggerOptions.UIEndpoint, swaggerOptions.Description);                
            });
        }
        to
        private void SwaggerSetup(IApplicationBuilder app)
        {
            ////https://github.com/domaindrivendev/Swashbuckle.AspNetCore#dealing-with-proxies-that-change-the-request-path
            app.Use((context, next) =>
            {
                if (context.Request.Headers.TryGetValue("X-Forwarded-Prefix", out var value))
                    context.Request.PathBase = value.First();

                return next();
            });
            var swaggerOptions = new SwaggerOptions();
            Configuration.GetSection(nameof(SwaggerOptions)).Bind(swaggerOptions);
            app.UseSwagger(option => { option.RouteTemplate = swaggerOptions.JsonRoute; });
            app.UseSwaggerUI(option => 
            {                 
                option.SwaggerEndpoint("v1/swagger.json", swaggerOptions.Description);
            });
        }

but not effect

alexkusuma commented 3 years ago

I also experience the same issue: the FQDN for the API server is: productname-api.domain-name.com, and we have a web route in front of it. So, people will need to use productname.domain-name.com/api to access the APIs. However, when swagger is displayed in the browser, it uses the original FQDN (productname-api.domain-name.com) instead of productname.domain-name.com/api. Therefore, whenever users click on the Try It button, it fails because it's unable to resolve. The hostname listed in the host drop-down list is the original FQDN instead of the /api ones. I've read the https://github.com/domaindrivendev/Swashbuckle.AspNetCore#dealing-with-proxies-that-change-the-request-path, and it doesn't resolve my issue. I've also using v6.0.7 now and still having the same issue. Any suggestions on how to solve it? Or should I create a pull request? Thank you.

domaindrivendev commented 3 years ago

The URL that you pass to SwaggerEndpoint is what the swagger-ui, a client-side application, uses to retrieve your API metadata. When the leading slash is omitted, you're telling the swagger-ui that the path is relative to itself. So, assuming the default RoutePrefix = "swagger" for the SwaggerUI middleware, you're telling it that the Swagger endpoint is available at swagger/v1/swagger.json. While this would be correct with the default setup, it won't work in your case because you've also changed the RouteTemplate for the Swagger middleware. To account for this, you need to alter the path provided to SwaggerEndpoint accordingly. For example, something along the following lines should work:

app.UseSwaggerUI(c =>
{
    c.SwaggerEndpoint($"../{swaggerOptions.JsonRoute}".Replace("{documentName}", "v1"), "V1 Docs");
});
alexkusuma commented 3 years ago

Hi, I think I need to clarify. The swagger.json can be found in its current location; it's never the issue. I'm trying to find out how to modify the hostnames listed in the Server dropdown list in the swagger.json.

I need to customise the Server dropdown list's content; at the moment, swashbuckler automatically uses the server's fqdn (productname-api.domain-name.com) instead of its accessible fqdn (productname.domain-name.com/api). I can't seem to find a way to do that.

If that's what you've been trying to tell me, then I apologise as I've certainly missed your explanation.

Looking forward to your further enlightenment.

KamranShahid commented 3 years ago

The URL that you pass to SwaggerEndpoint is what the swagger-ui, a client-side application, uses to retrieve your API metadata. When the leading slash is omitted, you're telling the swagger-ui that the path is relative to itself. So, assuming the default RoutePrefix = "swagger" for the SwaggerUI middleware, you're telling it that the Swagger endpoint is available at swagger/v1/swagger.json. While this would be correct with the default setup, it won't work in your case because you've also changed the RouteTemplate for the Swagger middleware. To account for this, you need to alter the path provided to SwaggerEndpoint accordingly. For example, something along the following lines should work:

app.UseSwaggerUI(c =>
{
    c.SwaggerEndpoint($"../{swaggerOptions.JsonRoute}".Replace("{documentName}", "v1"), "V1 Docs");
});

not worked

domaindrivendev commented 3 years ago

@KamranShahid, @alexkusuma - could you try pulling down the latest version (6.1.0) to see if this works for you?

In 6.0.0, I introduced logic to automatically infer and populate the servers metadata using info from the current request (Host header etc.), which drives the server dropdown in the swagger-ui. In doing this, I knew it would be problematic for apps that are behind a proxy, and provided guidance to use Microsoft's Forwarded Headers middleware, as this is their recommended solution for apps behind a proxy.

In hindsight though, I think this has caused more problems than it solved, and so as of 6.1.0, I've reverted to the old behavior that simply leaves the servers section empty. In this case, the swagger-ui will infer the absolute URL based on the current browser location (the fqdn). I think this should work for your case - can you try it and let me know?

alexkusuma commented 3 years ago

Hi @domaindrivendev - I had just upgraded it to 6.1.0 will test it in the next few days or early next week. I need to focus on the other part of the project for now. I will let you posted.

KamranShahid commented 3 years ago

Hi @domaindrivendev - I had just upgraded it to 6.1.0 will test it in the next few days or early next week. I need to focus on the other part of the project for now. I will let you posted.

Have you able to resolved it?

alexkusuma commented 3 years ago

Hi @domaindrivendev and @KamranShahid,

Unfortunately, it's not resolved yet. I've tried it with 2 servers, both behind webroute. the swagger kept using fqdn for the URL instead of the one that's accessible for user. For illustration:

server's fqdn is: servername-api.domain-name.com/ fqdn that's accessible from user is: service-name.domain-name.com/api

so on swagger page, when I have the the definition, let say for API called: api-endpoint-1 when I click on try it button, swagger kept using: servername-api.domain-name.com/api-endpoint-1 while actually it supposed to use: service-name.domain-name.com/api/api-endpoint-1

alexkusuma commented 3 years ago

Hi @domaindrivendev, should I do a pull request to solve this?

domaindrivendev commented 3 years ago

@alexkusuma - if you've upgraded to 6.1.0 and are still seeing issues, this is most likely a config issue. Could you possibly create a minimal app (i.e. starting with blank project) that repro's the issue and post to github so I can pull down and troubleshoot? I can use docker to put a proxy in front of it to mimic the setup you've described.

proudmonkey commented 3 years ago

We're using version 6.1.4 - which is the latest as of this time of writing and we're still having the same issue when our API is deployed in Azure and mapped through Azure Front Door and APIM. The "Try out" functionality does not work as the base path / api route prefix is stripped from the Swagger UI. For example,

Instead of https://{DOMAIN}.com/{BASEPATH}/v1/Foo, the Swagger UI uses this: https://{DOMAIN}.com/v1/Foo.

I spent the day trying to fix this, but couldn't get an elegant way to get the base path from swagger configuration. For the time being, here's what I did to fix it:

app.UseSwagger(options =>
{
    //Workaround to use the Swagger UI "Try Out" functionality when deployed behind a reverse proxy (APIM) with API prefix /sub context configured
    options.PreSerializeFilters.Add((swagger, httpReq) =>
    {
         if (httpReq.Headers.ContainsKey("X-Forwarded-Host"))
         {
            //The httpReq.PathBase and httpReq.Headers["X-Forwarded-Prefix"] is what we need to get the base path.
            //For some reason, they returning as null/blank. Perhaps this has something to do with how the proxy is configured which we don't have control.
            //For the time being, the base path is manually set here that corresponds to the APIM API Url Prefix.
            //In this case we set it to 'sample-app'. 

            var basePath = "sample-app"
            var serverUrl = $"{httpReq.Scheme}://{httpReq.Headers["X-Forwarded-Host"]}/{basePath}";
            swagger.Servers = new List<OpenApiServer> { new OpenApiServer { Url = serverUrl } };
         }
    });
})
.UseSwaggerUI(options =>
{
    options.RoutePrefix = string.Empty;
    options.SwaggerEndpoint("swagger/v1/swagger.json", "My Api (v1)");
});
KamranShahid commented 3 years ago

We're using version 6.1.4 - which is the latest as of this time of writing and we're still having the same issue when our API is deployed in Azure and mapped through Azure Front Door and APIM. The "Try out" functionality does not work as the base path / api route prefix is stripped from the Swagger UI. For example,

Instead of https://{DOMAIN}.com/{BASEPATH}/v1/Foo, the Swagger UI uses this: https://{DOMAIN}.com/v1/Foo.

I spent the day trying to fix this, but couldn't get an elegant way to get the base path from swagger configuration. For the time being, here's what I did to fix it:

app.UseSwagger(options =>
{
    //Workaround to use the Swagger UI "Try Out" functionality when deployed behind a reverse proxy (APIM) with API prefix /sub context configured
    options.PreSerializeFilters.Add((swagger, httpReq) =>
    {
         if (httpReq.Headers.ContainsKey("X-Forwarded-Host"))
         {
            //The httpReq.PathBase and httpReq.Headers["X-Forwarded-Prefix"] is what we need to get the base path.
            //For some reason, they returning as null/blank. Perhaps this has something to do with how the proxy is configured which we don't have control.
            //For the time being, the base path is manually set here that corresponds to the APIM API Url Prefix.
            //In this case we set it to 'sample-app'. 

            var basePath = "sample-app"
            var serverUrl = $"{httpReq.Scheme}://{httpReq.Headers["X-Forwarded-Host"]}/{basePath}";
            swagger.Servers = new List<OpenApiServer> { new OpenApiServer { Url = serverUrl } };
         }
    });
})
.UseSwaggerUI(options =>
{
    options.RoutePrefix = string.Empty;
    options.SwaggerEndpoint("swagger/v1/swagger.json", "My Api (v1)");
});

this mean's basepath should be there somewhere in configuration

proudmonkey commented 3 years ago

@KamranShahid I would think so, yes. But for the time being, we set it as a static value since the basebath won't change in all of our environments.

KamranShahid commented 3 years ago

@KamranShahid I would think so, yes. But for the time being, we set it as a static value since the basebath won't change in all of our environments.

Thanks Vincent Maverick, I also had similar idea but like to avoid one additional configuration as my application is containerize and almost all configuration is driven via environment variables + vault combination. Anyway thanks for this

Vaccano commented 3 years ago

So, I could not find what I needed here, but the docs clearly show how to do this here.

Basically, you need to give it a path that it can know where it is starting in on the "swagger stuff". If you set RoutePrefix to "swagger" then it will see all URLs that have swagger in them as needing to be handled by swagger and will work from there.

So if you have a url like https://localhost:32332/service/swagger/index.html it will ignore the stuff before the text that matches RoutePrefix. It will then start adding on from there.

Here is the example code (copied from the docs):

        app.UseSwagger();
        app.UseSwaggerUI(options =>
        {
            options.SwaggerEndpoint("v1/swagger.json", "Addition Service");
            options.RoutePrefix = "swagger";
        });
marlon-tucker commented 2 years ago

Hello,

I've just ran into this as well while deploying apps to a Kubernetes cluster, the intention being development builds will be deployed to prefixed paths on the same domain. I've also hit this problem when deploying apps to IIS using it's virtual applications, which does a similar thing, hosts the application under a subpath of the root domain.

ASP.NET handles this in a standard way, by using the BasePath property on the incoming request. This can be set by various middlewares or other utilities, for example the IIS Hosting sets this BasePath if the application is hosted under a virtual path. The use cases are documented here:

https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-6.0

However, it seems the SwaggerUI doesn't respect the BasePath property, looking at the code it only uses the RoutePrefix property which as far as I can tell, can only be set once at startup time.

At the moment I can get it to work by having a configuration property which can be set, but doing so locks that application to one specific path, whereas only things that are sent down to the client browser should really care about the BasePath, so it should be set on a per request basis.

brian-pickens commented 2 years ago

Has anyone come up with a solution for this? My route to my app gets rewritten. So what happens is I can get to swagger UI, but the swagger.json gets requested by a different URI based on the SwaggerEndpoint configuration. E.g.

app.UseSwaggerUI(c =>
{
    c.SwaggerEndpoint("/api/v1/swagger.json", "SagaControllerApp v1");
    c.RoutePrefix = "api";
});

Navigate to app at https://domain/api/myapp but the app requests swagger.json at https://domain/api/v1/swagger.json.

brian-pickens commented 2 years ago

Has anyone come up with a solution for this? My route to my app gets rewritten. So what happens is I can get to swagger UI, but the swagger.json gets requested by a different URI based on the SwaggerEndpoint configuration. E.g.

app.UseSwaggerUI(c =>
{
    c.SwaggerEndpoint("/api/v1/swagger.json", "SagaControllerApp v1");
    c.RoutePrefix = "api";
});

Navigate to app at https://domain/api/myapp but the app requests swagger.json at https://domain/api/v1/swagger.json.

I did find a workaround by setting the SwaggerEndpoint to a relative path. E.g: c.SwaggerEndpoint("v1/swagger.json", "SagaControllerApp v1");

awp-sirius commented 1 year ago

Thanks everyone for your recommendations!

My case: I deployed my service along the path: https://domain.com/path/... Methods path: https://domain.com/path/api/v1/Method Swagger path: https://domain.com/path/swagger/index.html

What I've done...

  1. Set up nginx:
    
    location ^~ /path/swagger/ {
    proxy_set_header X-Forwarded-Prefix /path;
    rewrite ^/path/(.*)$ /$1 break;
    ...

location ^~ /path/api/ { ... rewrite ^/path/(.*)$ /$1 break; ...


2. Set relative path for swagger.json:

app.UseSwaggerUI(options => { foreach (var description in apiProvider.ApiVersionDescriptions) { options.SwaggerEndpoint($"../swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant()); } });

_(You can use `../` or `$"{description.GroupName}/swagger.json"`)_

3. Installed PreSerializeFilters to set up paths to methods:

app.UseSwagger(c => { c.PreSerializeFilters.Add((swaggerDoc, request) => { if (request.Headers.TryGetValue("X-Forwarded-Prefix", out var serverPath)) { swaggerDoc.Servers = new List() { new OpenApiServer() { Description = "Server behind", Url = serverPath } }; } }); });



And it works for me!

![image](https://user-images.githubusercontent.com/17874415/216397794-5b6c3369-f5c1-4ece-9d29-79a5fb8a74f1.png)