Lisa-Marie
Lisa-Marie

Reputation: 393

How do I setup Swashbuckle v5 with swagger when I have a custom base url?

I am upgrading a .net API to .net Core 3.1 and using Swashbuckle.AspNetcore 5.4.1. The API is running inside a ServiceFabric app. I found this https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/1173 and tried to follow that and swagger gets generated but if I try to use the Swagger UI to send requests the request URL is with the wrong IP so the request fail. In the old Swashbuckle 4.0.1 setup we did not specify host, only the relative basePath. How can I achieve the same?

Startup.cs

var swaggerBasePath = "/MySfApp/SfApp.ClientApi/";

app.UseSwagger(c =>
{
    c.SerializeAsV2 = serializeAsSwaggerV2;
    
    c.RouteTemplate = "swagger/{documentName}/swagger.json";
    c.PreSerializeFilters.Add((swaggerDoc, httpReq) =>
    {
        swaggerDoc.Servers = new List<OpenApiServer> { new OpenApiServer { Url = $"{httpReq.Scheme}://{httpReq.Host.Value}{swaggerBasePath}" } };
    });
});

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

The result is that the Swagger UI loads correctly on URL:

http://145.12.23.1:54000/MySfApp/SfApp.ClientApi/swagger/index.html

and it says under name that BaseUrl is:

[ Base URL: 10.0.0.4:10680/MySfApp/SfApp.ClientApi/ ]

The 10.0.0.4:10680 is the node inside the ServiceFabric cluster. Correct IP to reach from outside is 145.12.23.1:54000. In the older version (4.0.1) of Swashbuckle it says baseUrl without IP first: "/MySfApp/SfApp.ClientApi"

Swagger.json is located at:

http://40.68.213.118:19081/MySfApp/SfApp.ClientApi/swagger/api/swagger.json

and it says:

"swagger": "2.0",
... 
"host": "10.0.0.4:10680",  
"basePath": "/MySfApp/SfApp.ClientApi/",
"schemes": [
"http"
],
"paths": {
"/activity/{activityId}": {
"get"
...etc

If i try to send a GET request from the Swagger UI the request is sent to wrong IP:

curl -X GET "http://10.0.0.4:10680/MySfApp/MySfApp/activity/3443"

EDIT 1: After some digging I have now changed the setup to this in startup.cs

var swaggerBasePath = "/MySfApp/SfApp.ClientApi/";
app.UsePathBase($"/{swaggerBasePath}");
app.UseMvc();
app.UseSwagger(c =>
{
    c.SerializeAsV2 = serializeAsSwaggerV2;

    c.PreSerializeFilters.Add((swaggerDoc, httpReq) =>
    {
        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 }
        };
    });
});
app.UseSwaggerUI(options => {
    options.SwaggerEndpoint("api/swagger.json", "My API V1");
});

This now leads to the Swagger UI loading properly with the baseUrl

http://145.12.23.1:54000/MySfApp/SfApp.ClientApi/swagger/index.html

and also swagger.json is served correctly with the correct baseUrl.

http://145.12.23.1:54000/MySfApp/SfApp.ClientApi/swagger/api/swagger.json

So the wrong hostname is resolved. Thanks to idea from this thread.

However when I try to call an endpoint from the Swagger UI page, the curl URL does not include the baseUrl. So closer... but currently not possible to use Swagger UI.

curl -X GET "http://10.0.0.4:10680/activity/3443"

The swagger.json does not have 'host' nor 'basePath' defined.

Upvotes: 11

Views: 8534

Answers (4)

Pawel Cioch
Pawel Cioch

Reputation: 3194

Here is my version based on Vincent Maverick Durano answer. The idea is to be portable without hardcoding the paths. I take the Referer header sent by a browser and use that url as base. This is with the assumption your app is configured to have index.html in the root of the API.

When you host the app directly it would be something like https://example.com/index.html and the UI API call to be something like https://example.com/v1/users.

When you host the app via proxy like Azure Front Door using routing you would want something like https://anotherexample.com/myapp/index.html the whole https://example.com/ is proxied as https://anotherexample.com/myapp/ thus index that follows with that, so the UI API call to be https://anotherexample.com/myapp/v1/users

The code

app.UseSwagger(options =>
{
    //Workaround to use the Swagger UI "Try Out" functionality when deployed behind a reverse proxy with routing
    options.PreSerializeFilters.Add((swagger, httpReq) =>
    {
        //The assumption is being made here that index.html is hosted in the root of a virtual directory where a route can be a root
        //example without a proxy
        //https://example.com/index.html <-- the API is directly accesible at domain root like domain.com/v1/users
        //
        //example with a proxy and routing
        //https://example.com/appx/index.html <-- the API is accesible at domain.com/appx "root" like domain.com/appx/v1/users
        //everything should be relative to domain.com/appx
        if (httpReq.Headers.ContainsKey("Referer"))
        {
            var referer = httpReq.Headers["Referer"].ToString();
            var index = referer.IndexOf("index.html");
            var basePath = referer.Remove(index);
            swagger.Servers = new List<OpenApiServer> { new OpenApiServer { Url = basePath } };
        }
    });
});

And swagger UI registration with relative path and versioning

app.UseSwaggerUI(c =>
{
    // build a swagger endpoint for each discovered API version, and show latest as first
    for (var i = versionProvider.ApiVersionDescriptions.Count - 1; i >= 0; i--)
    {
        var description = versionProvider.ApiVersionDescriptions[i];
        c.SwaggerEndpoint($"swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant());
    }

    c.RoutePrefix = string.Empty;
});

If you don't support versioning then only important line is c.SwaggerEndpoint($"swagger/ notice $"swagger starts without / to make the UI relative to the proxy routing

Upvotes: 0

Brijesh Shah
Brijesh Shah

Reputation: 665

I were having something similar in my solution and I have used a little bit this way and that works well for me, in case that helps someone.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
   var pathBase = Configuration["PATH_BASE"];
   if (!string.IsNullOrWhiteSpace(pathBase))
   { 
      app.UsePathBase($"/{pathBase.TrimStart('/')}");
      app.Use((context, next) =>
      {
          context.Request.PathBase = new PathString($"/{pathBase.TrimStart('/')}");
          return next();
      });

    if (env.IsDevelopment())
    {
       app.UseSwagger(c =>
       {
           c.PreSerializeFilters.Add((swaggerDoc, httpReq) =>
           {
                 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 }
                 }
           });
       });
       app.UseSwaggerUI(c => c.SwaggerEndpoint($"/{pathBase.TrimStart('/')}/swagger/v1/swagger.json", "My.API v1"));    
    }
   }
}

check the last line app.UseSwaggerUI(c => c.SwaggerEndpoint($"/{pathBase.TrimStart('/')}/swagger/v1/swagger.json", "My.API v1"));

Upvotes: 1

We're using Swashbuckle 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 App Service that is 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. You can see that the /BASEPATH is missing.

I spent the whole day trying to fix this with trial and error, trying various approaches with no luck, I 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)");
});

Here's an open discussion related to this issue here.

Upvotes: 4

ThieuND
ThieuND

Reputation: 91

Try this: serverUrl = $"{httpReq.Headers["X-Forwarded-Proto"]}://" + $"{httpReq.Headers["X-Forwarded-Host"]}" + _basePath; where _basePath can be set using the ServiceName property of StatelessServiceContext. Please be noted that the original value of X-Forwarded-Proto may be overridden by SF.

Upvotes: 0

Related Questions