Swashbuckle.AspNetCore icon indicating copy to clipboard operation
Swashbuckle.AspNetCore copied to clipboard

Support root-relative paths in endpoint paths in Swagger UI

Open jbogard opened this issue 4 years ago • 27 comments

In a reverse-proxy environment, the base path can often include multiple segments:

https://myapi.com/some/root/path

The correct way to set up custom base paths is to use app.UsePathBase:

// in Startup.Configure
public void Configure(IApplicationBuilder app) {
    app.UsePathBase("/some/root/path");
}

When you do this, your new "application root" path is the above path. Any place you use ~/ in your paths will add that request base path. When using IIS integration, this is all taken care of for you:

https://github.com/aspnet/AspNetCore/blob/master/src/Servers/IIS/IISIntegration/src/WebHostBuilderIISExtensions.cs#L43

I found, however, that not all pieces in the SwaggerUI support these alternate application roots.

If you do this:

app.UsePathBase("/some/root/path");
app.UseMvc();
app.UseSwagger();
app.UseSwaggerUI(options => {
     options.SwaggerEndpoint("swagger/v1/swagger.json", "My API V1");
});

Then my swagger JSON is correctly served by /some/root/path/swagger/v1/swagger.json and the UI is served at /some/root/path/swagger/index.html.

However, it can't find the swagger JSON off the relative path. It assumes a relative path off of the current UI /some/root/path/swagger/swagger/v1/swagger.json.

The only way I can get this to work is manually include the root path in the URLs:

app.UsePathBase("/some/root/path");
app.UseMvc();
app.UseSwagger();
app.UseSwaggerUI(options => {
    options.SwaggerEndpoint("/some/root/path/swagger/v1/swagger.json", "My API V1");
});

If my application's path base is set, then everything should work seamlessly, I shouldn't need to configure anything. All the middleware does is alter the Request.BasePath part. Additionally, that root path could come in from configuration somewhere, and I don't want to have my Swagger UI aware of this.

Ideally, I could use root-relative paths:

app.UsePathBase("/some/root/path");
app.UseMvc();
app.UseSwagger();
app.UseSwaggerUI(options => {
    options.SwaggerEndpoint("~/swagger/v1/swagger.json", "My API V1");
});

This doesn't work, however, as the URL requested becomes:

/some/root/path/swagger/~/swagger/v1/swagger.json.

Note that I do not need to specify the RoutePrefix, since that part does respect the request base path.

jbogard avatar Sep 12 '19 14:09 jbogard

The path that you provide to SwaggerEndpoint is relative to the Swagger UI page. Therefore, you should be able to make the whole setup proxy / virtual path agnostic with the following configuration:

app.UsePathBase("/some/root/path");
app.UseMvc();
app.UseSwagger();
app.UseSwaggerUI(options => {
    options.SwaggerEndpoint("v1/swagger.json", "My API V1");
});

domaindrivendev avatar Sep 12 '19 14:09 domaindrivendev

Perfect, thanks!

jbogard avatar Sep 12 '19 17:09 jbogard

@domaindrivendev how would I be able to apply the same logic for defining a OpenApiServer that should be dependant of a relative path (because of the reverse proxy) ?

diogonborges avatar Apr 21 '20 17:04 diogonborges

@diogonborges see https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/1298#issuecomment-620269062

domaindrivendev avatar Apr 27 '20 22:04 domaindrivendev

I tried this but the swagger.json is not served on the relative path.

app.UsePathBase("/some/root/path");
app.UseMvc();
app.UseSwagger();
app.UseSwaggerUI(options => {
    options.SwaggerEndpoint("api/swagger.json", "My API V1");
});

Then my swagger JSON is served by /swagger/api/swagger.json and not the relative path. And the UI is served at /some/root/path/swagger/index.html, complaining about Fetch error Internal Server Error http://localhost:10680/some/root/path/swagger/api/swagger.json

This is in a WebApi inside a ServiceFabric cluster with kestrel. How can I correctly add the basePath to swagger?

using Swashbuckle.AspNetCore v 5.4.1 using .net core 3.1

Same question also asked here, just with different setup tried https://stackoverflow.com/questions/61733433/how-do-i-setup-swashbucle-v5-with-swagger-when-i-have-a-custom-base-url

lisamariet avatar May 12 '20 13:05 lisamariet

I added the PreSerializeFilter from #1298 (comment)

if (!httpReq.Headers.ContainsKey("X-Original-Host")) return;

var serverUrl = $"{httpReq.Headers["X-Original-Proto"]}://" +
				$"{httpReq.Headers["X-Original-Host"]}/" +
				$"{httpReq.Headers["X-Original-Prefix"]}";

swaggerDoc.Servers = new List<OpenApiServer>()
{
	new OpenApiServer { Url = serverUrl }
};

This helps in that the swagger.json is now loaded correctly at: /some/root/path/swagger/api/swagger.json

But when i try out an endpoint from the Swagger UI the curl url does not have the relative basePath. It goes to http://X.X.X.X/path , missing the basePath in it.

lisamariet avatar May 13 '20 09:05 lisamariet

I added the PreSerializeFilter from #1298 (comment)

if (!httpReq.Headers.ContainsKey("X-Original-Host")) return;

var serverUrl = $"{httpReq.Headers["X-Original-Proto"]}://" +
				$"{httpReq.Headers["X-Original-Host"]}/" +
				$"{httpReq.Headers["X-Original-Prefix"]}";

swaggerDoc.Servers = new List<OpenApiServer>()
{
	new OpenApiServer { Url = serverUrl }
};

This helps in that the swagger.json is now loaded correctly at: /some/root/path/swagger/api/swagger.json

But when i try out an endpoint from the Swagger UI the curl url does not have the relative basePath. It goes to http://X.X.X.X/path , missing the basePath in it.

Did you find how to generate the Try Out endpoint correctly?. I am having the same issues and I am not sure how to solve it

dluciano avatar Jun 01 '20 23:06 dluciano

No. This is still an unresolved problem. :-/

lisamariet avatar Jun 08 '20 09:06 lisamariet

@lisamariet the original issue here was resolved and closed by the submitter. If you have a separate (albeit related) problem, please create a new issue for it, providing clear repro steps. Thanks

domaindrivendev avatar Jun 08 '20 11:06 domaindrivendev

use "../"

app.UseSwaggerUI(options => { options.SwaggerEndpoint("../swagger/v1/swagger.json", "My API V1"); });

anukochummen avatar Aug 06 '20 22:08 anukochummen

IMO no web application should care what it's final external path is. It should always work relative to itself and never assume that / is the root. The reason is that an app should work in front of and behind a reverse proxy. The suggestions above (UsePathBase) mean swagger will ONLY work behind a reverse proxy. I am pretty sure if Swagger is careful with paths (never referencing / but only ./) this should not be a problem.

Alternatively swagger should detect (in the browser) that, although it's been configured to serve /api/ internally it has been externally started from /extpath/api/ and work accordingly.

cawoodm avatar Aug 07 '20 09:08 cawoodm

This is still a problem. And it doesn't work the way @domaindrivendev described. Why was this closed I wouldn't know but it shouldn't have been. I'm experiencing the exact same problem when using a subapplication within IIS site.

brgrz avatar Mar 03 '21 12:03 brgrz

It was closed by the original submitter and therefore the problem , as defined by them, IS resolved. As I said above, if something related is not working for you, please create a separate issue and (most importantly) create a minimal app (i.e. starting from blank project) that reproduce the issue, and post to GitHub where I can pull down and troubleshoot

domaindrivendev avatar Mar 03 '21 14:03 domaindrivendev

I thought this was working completely, but it's still not quite there. This works to get the JSON and UI to show up at the relative path:

app.UsePathBase("/some/root/path");
app.UseMvc();
app.UseSwagger();
app.UseSwaggerUI(options => {
    options.SwaggerEndpoint("v1/swagger.json", "My API V1");
});

However, the base URl in the Swagger JSON and path used in the UI still don't reflect the base path. From https://localhost:7110/some/root/path/swagger/v1/swagger.json:

{
  "openapi": "3.0.1",
  "info": {
    "title": "My API",
    "version": "v1"
  },
  "servers": [
    {
      "url": "https://localhost:7110"
    }
  ],
  "paths": {
    "/my/api": {

The path base is not reflected in the servers value, so the Swagger UI won't be correct either, still going off the root.

jbogard avatar Sep 09 '21 13:09 jbogard

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            app.UsePathBase("/api");

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseSwagger();
                app.UseSwaggerUI(c => c.SwaggerEndpoint("v1/swagger.json", "FakeClient"));
            }

            app.UseHttpsRedirection();
            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }

Here is my Configure method. And it's not working for me. I am using .NET 5.

abuzaforfagun avatar Sep 26 '21 19:09 abuzaforfagun

try to use this one. via OpenApiServer. The basePath should reflect properly the servers value in the Swagger JSON

app.UseSwagger(c =>
{
    if (!env.IsDevelopment())
    {
        var basePath = "/test";
        c.PreSerializeFilters.Add((swaggerDoc, httpReq) =>
        {
            swaggerDoc.Servers = new List<OpenApiServer> { new OpenApiServer { Url = $"https://{httpReq.Host.Value}{basePath}" } };
        });
        c.RouteTemplate = "/swagger/{documentName}/swagger.json";
    }
});
app.UseSwaggerUI(c =>
{
    if (!env.IsDevelopment())
    {
        c.SwaggerEndpoint("/test/swagger/v1/swagger.json", "Test.Api v1");
    }
    else
        c.SwaggerEndpoint("/swagger/v1/swagger.json", "Test.Api v1");

    c.DocumentTitle = "Test API Swagger Documentation";
    c.DocExpansion(DocExpansion.List);
});

aredz avatar Oct 13 '21 07:10 aredz

I don't mean to suggest this is a responsibility of swagger or not.... but my apps I dont know how a user is going to ultimately install them. Could be installed at a root level where its http://somedomain.com/ or it could be http://somedomain.com/<somevirtual>

My problem is I don't know how its being installed so I can't exactly go and set a urlpathbase. I get that there is a possibility that we may not know where an app is 'mounted' until we handle the first request... which seems like in this case would be too late.

Hoping there exists some workaround that isn't a overly large amount of work.

I've tried using the old school path tricks with ./swagger/v1/swagger.json as well as ~/swagger/v1/swagger.json, and naturally neither really did anything for me.

Many of these suggestions seem like they require pre-knowledge of where everything will live. Is there a work around where I can just say "I dont know or care where the app was mounted, I just want to include /swagger/v1/swagger.json at least relative to the root of my web app?

ronnyek avatar Dec 09 '21 18:12 ronnyek

@ronnyek if you're using the latest version and follow the steps described here, I believe Swashbuckle will work regardless of where the app is mounted. Are you saying this is not the case? If so, then maybe you could create a minimal sample app that repro's the problem and post to github, where I can pull down and troubleshoot.

domaindrivendev avatar Dec 10 '21 13:12 domaindrivendev

@domaindrivendev , here's my config:

app.UsePathBase(pathBase);
app.UseForwardedHeaders();

app.UseSwagger();
app.UseSwaggerUI(options => {
    options.RoutePrefix = "swagger";
    options.SwaggerEndpoint("v1/swagger.json", "Order Fulfillment Portal API V1");
});

Unfortunately, UI completely ignores path base =(

voroninp avatar Jan 04 '22 16:01 voroninp

In your "SwaggerEndpoint" you're telling the swagger UI where to find the swagger URL. Swagger UI has no idea about your path base so you have to explicitly put that in there.

Also, in your Swagger server configuration, I added the base path as a variable so that requests go to the /my-base-path prefix. Something like:

options.AddServer(new OpenApiServer 
    {
    Url = $"https://{host}{{basePath}}",
    Variables = {
        ["basePath"] = new OpenApiServerVariable
        {
            Default = "/my-base-path"
        }
    }
});

jbogard avatar Jan 04 '22 17:01 jbogard

@jbogard , now I am confused. @domaindrivendev claims that it should work without any adjustments just because it uses relative paths. And in your example, I assume, we also need to account for X-Frowarded-* headers, right?

voroninp avatar Jan 04 '22 17:01 voroninp

Oh now that DID work for me, Swagger UI came up just fine following the instructions linked. I was on a slightly older version before.

jbogard avatar Jan 04 '22 19:01 jbogard

@jbogard You are killing me =) UI works fine for me, but the routes to endpoints are wrong.

voroninp avatar Jan 05 '22 07:01 voroninp

Ok, I had to add server

services.AddSwaggerGen(c =>
{
    c.AddServer(new OpenApiServer
    {
        Url = pathBase
    });
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Order Fulfillment", Version = "v1" });

voroninp avatar Jan 05 '22 08:01 voroninp

I struggled with this one too. Hopefully Google can pick up on some of the relevant tags to send people straight to this thread. In my case I'm running my Api in a Kubernetes cluster using Traefik with ingress and stripprefix middleware (https://doc.traefik.io/traefik/middlewares/http/stripprefix/) so that I'm able to host the service at https://my.domain.com/accounts. The Swagger UI is then available at https://my.domain.com/accounts/swagger/index.html.

I wanted it all to work when developing locally too also didn't want to include base path configuration. Here's how I got it to work:

app.UseSwagger(c=>
    { 
        c.PreSerializeFilters.Add((swaggerDoc, request) =>
        {
            const string prefixHeader = "X-Forwarded-Prefix";
            if (!request.Headers.ContainsKey(prefixHeader))
                return;
            
            var serverUrl = request.Headers[prefixHeader];
            swaggerDoc.Servers = new List<OpenApiServer>()
            {
                new() { Description = "Server behind traefik", Url = serverUrl }
            };
        });
    });
    app.UseSwaggerUI(options =>
    {
        options.RoutePrefix = "swagger";
        options.SwaggerEndpoint("v1/swagger.json", "My API V1");
    });

The PreSerializeFilters configuration is required so that the Swagger UI routes the call correctly to https://my.domain.com/accounts/WeatherForecast. If not added, then it goes to https://my.domain.com/WeatherForecast.

badokun avatar Jan 23 '22 00:01 badokun

Same problem as in issue https://github.com/dotnet/aspnetcore/issues/42559, Swashbuckle.AspNetCore version 6.5 on .NET 7 and Kubernetes with Nginx ingress. This is what finally got it working:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: orleans-concept-server-ingress
  namespace: orleans-concept
  annotations:
    nginx.ingress.kubernetes.io/use-regex: "true"
    nginx.ingress.kubernetes.io/rewrite-target: /$3
    nginx.ingress.kubernetes.io/x-forwarded-prefix: /$1
spec:
  ingressClassName: nginx
  rules:
  - http:
      paths:
      - path: /(server)(/|$)(.*)
        pathType: Prefix
        backend:
          service:
            name: orleans-concept-server
            port:
              number: 80
// Note: important for swagger to work behind proxy
// https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.All
});

// Note: important for swagger to work behind proxy
// https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/1253#issuecomment-1019382999
app.UseSwagger(c =>
{
    c.PreSerializeFilters.Add((swaggerDoc, request) =>
    {
        const string prefixHeader = "X-Forwarded-Prefix";
        if (!request.Headers.ContainsKey(prefixHeader))
        {
            return;
        }

        var prefix = request.Headers[prefixHeader];
        swaggerDoc.Servers = new List<OpenApiServer>()
        {
            new() { Url = prefix }
        };
    });
});

app.UseSwaggerUI();

Must say that the result is not very nice, as this basically hacks the Servers configuration. But nothing else worked.

Maybe the proper approach would be to use BasePath property.

akovac35 avatar Jan 22 '23 21:01 akovac35

What you may really need for reverse proxy is not Swagger side solution but .net side solution https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-6.0

image

Swagger or not, if you did middleware with await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme); Then the auto redirect URL will be the origin URL, the url where app is hosted, but you need to be able to pull full URL form HTTP Context that is proxy URL, and for this you need the setup above. (Keywords: Azure Front Door proxy)

Actually using UsePathBase will postpend instead of append, so before UseRouting add this:

app.Use((context, next) =>
{
    context.Request.PathBase = new PathString("/someRouteAtProxyLevel");
    return next(context);
});

Other than that try/fail yourself, I'm sill working on it :)

skironDotNet avatar Apr 16 '24 21:04 skironDotNet