RicoSuter / NSwag

The Swagger/OpenAPI toolchain for .NET, ASP.NET Core and TypeScript.
http://NSwag.org
MIT License
6.73k stars 1.29k forks source link

NSwag and Service Fabric #1220

Open Eneuman opened 6 years ago

Eneuman commented 6 years ago

I'm trying to move from Swagger/Swatchbuckle to NSwag but im running into issues trying to access swagger ui.

Since im using Service Fabric, all my apis are behind a reverse proxy. Accessing the direct url:

http://localhost:60841/8f0077aa-6088-469d-9465-449acce86123/131654410884013502/swagger/

works, but accessing the reverse url:

http://localhost:19081/MyApp/MyService/swagger/

does not. It gets redirected to the direct url

http://localhost:19081/c94a3b54-f097-4cbe-bc82-d1e70a403134/131655768288075951/swagger/index.html?url=/c94a3b54-f097-4cbe-bc82-d1e70a403134/131655768288075951/swagger/v1/swagger.json

and reports a 404.

Accessing the reverse url did work when I was using swatchbuckle but then I also had this configuration:

app.UseSwagger(c =>
{
   c.PreSerializeFilters.Add((swaggerDoc, httpReq) => swaggerDoc.BasePath = "/MyApp/MyService");
});

Does NSwag have a option to set PreSerializeFilters? Is there another option that I can use to get the ui to work behind a reverse proxy?

RicoSuter commented 6 years ago

I think the property you are looking for is PostProcess

Eneuman commented 6 years ago

I tried PostProcess but I could not get it to work :( I think my problem has to do with the 302 redirect and a missconfigured endpoint.

When I was using swatchbuckle I manually added the endpoints like this:

      app.UseSwaggerUI(options =>
      {
        var provider = app.ApplicationServices.GetRequiredService<IApiVersionDescriptionProvider>();
        foreach (var description in provider.ApiVersionDescriptions)
        {
          options.SwaggerEndpoint($"{applicationBaseUrl}/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant());
        }
      });

Is there something similar in NSwag ?

RicoSuter commented 6 years ago

You can configure the routes in the settings object

RicoSuter commented 6 years ago

Is the problem, that you dont know the routes at configuration time? Can't you get the actual route via environment variable?

Eneuman commented 6 years ago

The problem I’m having at the moment is that the swagger gui is not showing up at all. All it gives me is a 404. I think Service Fabrics reverse proxy is messing things up but I’m gonna enable route tracing tomorrow to see what’s actually going on. I have a feeling I might need to create a new middleware to handle Service Fabrics X-forward header etc but I know more once I traced the routing. If I can box the specific configuration in a middleware, do you want a PR or shall I just type the solution here? Btw, thanks for your quick replies and a awsome produkt :)

Eneuman commented 6 years ago

After much digging I found my initial problem: If I entered the full url http://localhost:19081/MyApp/MyService/swagger/index.html it worked and I could see the Gui.

The problem seems to be theese lines of code https://github.com/RSuter/NSwag/blob/1eebd6a978d8a02cf861cffe189d4c7b4954350a/src/NSwag.AspNetCore/Middlewares/RedirectMiddleware.cs#L35-L36

When Service Fabric reverse proxy is used, context.Request.PathBase will contain the dynamicly temporary path for the micro service ie

/8f0077aa-6088-469d-9465-449acce86123/131654410884013502

So the page gets redirected to the wrong url. A possible solution to this might be to change this line https://github.com/RSuter/NSwag/blob/1eebd6a978d8a02cf861cffe189d4c7b4954350a/src/NSwag.AspNetCore/Middlewares/RedirectMiddleware.cs#L35

to also check it is coming from a proxy, like this: if (context.Request.PathBase.HasValue && string.IsNullOrEmpty(context.Request.Headers["X-Forwarded-For"]))

but I can only verify if it works for Service Fabrics reverse proxy.

Another solution would be to write a custom middleware that changes the PathBase.

My second problem is that I can not find a way to add multiple Endpoint to Swagger, one for each of my API Versions

MS has a example here: https://github.com/Microsoft/aspnet-api-versioning/blob/master/samples/aspnetcore/SwaggerSample/Startup.cs where they use swashbuckle in combination with Api explorer to dynamicly add them at startup. Is this possible in NSwag ?

RicoSuter commented 6 years ago

What exactly is this doing? Generate one route per version and instruct Swagger UI to aggregate these in a single UI?

Eneuman commented 6 years ago

Yeah, and in the gui I can select the version i want to use and it shows me only thoose APIs.

RicoSuter commented 6 years ago

I think this is not supported out of the box, there are three options in the middlewares:

  1. UseSwagger(assembly, settings) => serves only a single swagger.json
  2. UseSwaggerUi(assembly, settings) => serves the UI and a single swagger.json
  3. UseSwaggerUi(settings) => serves only the UI

I think versions are already supported but you'd need to call UseSwagger for each version with different routes. And I think there is no setting to specify multiple routes in the UI so that they are shown in a single UI.

ehvattum commented 6 years ago

The middleware should really respect Request.PathBase. When hosting an aspnet core application behind a reverse proxy at path, this property is used for other middelware, and by all url-generation. The middleware should also respect this property. At the moment, the Authorizations through SwaggerUI appends the wrong callback string to the authorization-endpoint, loosing the PathBase component.

RicoSuter commented 6 years ago

@Eneuman adding multiple endpoints to the ui is now supported (SwaggerRoutes), just call UseSwagger/UseSwaggerWithApiExplorer() multiple times and register the routes in SwaggerRoutes

Can someone create a PR to respect the BasePath in the middlewares?

ehvattum commented 6 years ago

Workaround that at least works for AspNetCore:

Edit: this is not working: see my next comment

The middleware is actually using a "PathBase", but a custom one on the SwaggerUISettingsBase called MiddlewareBasePath. To make swaggerUI redirect properly when hosted at a PathBase other than /, set this property to whatever the app uses, and prepend the same path to settings.SwaggerRoute and swaggerUiRoute.

Given that the app is hosted at /api add this line at the start of your Startup.Configure-method: app.UsePathBase("/api");

and to the Swagger middleware config:

 app.UseSwaggerUi3WithApiExplorer(settings => {
// settings.SwaggerRoute = "/api/swagger/v1/swagger.json"; edit: does not work
 settings.SwaggerUiRoute= "/api/swagger";
 settings.MiddlewareBasePath = "/api";    
});
RicoSuter commented 6 years ago

Maybe we can use UsePathBase() to "initialize" MiddlewareBasePath? Or does this break other things? Or is this a breaking change?

ehvattum commented 6 years ago

Most other middleware is silenty adhering to the PathBase, and not trying to do much path calculations. But for SwaggerUI and AspNetCoreToSwaggerMiddleware, the PathBase usage is strange.

My previous attempted fix actually turned out unexpected. The generated Swagger-spec has the wrong document.BasePath. The spec-generation is removing the basepath by substringing away the MiddlwareBasePath, meaning the fix to SwaggerUi breaks the spec-generation. Overriding the document.BasePath mends this error.

A full workaround is:

public void Configure(IApplicationBuilder app, IHostingEnvironment env){
   var basePath = "/api";
   app.UsePathBase(basePath); //This should go first.
  //Other interesting middleware
   app.UseSwaggerUi3WithApiExplorer(settings => {
      settings.SwaggerRoute = string.Concat(basePath, "/swagger/v1/swagger.json");
      settings.SwaggerUiRoute = string.Concat(basePath, "/swagger");
      settings.MiddlewareBasePath = basePath;
      settings.PostProcess = document => document.BasePath = basePath;
   });
}
jeefave commented 5 years ago

Better late than never. I managed to have it working that way:

From serviceContext of type StatelessServiceContext, I provide my service fabric settings via a custom ReverseProxyOptions,

services.AddSingleton(
    new ReverseProxyOptions()
    {
        AppPath = serviceContext.ServiceName.AbsolutePath,
        AppTitle = serviceContext.CodePackageActivationContext.CodePackageName,
        AppVersion = serviceContext.CodePackageActivationContext.CodePackageVersion
    });
services.AddSingleton(serviceContext);

Then I configure NSwag with the request headers and reverse proxy options:


            var reverseProxyOptions = app.ApplicationServices.GetService<ReverseProxyOptions>();
            app.Use((context, next) =>
                {
                    if (context.Request.Headers.TryGetValue("X-Forwarded-Host", out var c))
                    {
                        context.Request.PathBase = $"{reverseProxyOptions?.AppPath}";
                    }

                    return next();
                });

            app.UseDefaultFiles();
            app.UseStaticFiles();
            app.UseAuthentication();
            app.UseCors(options => options.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin());
            app.UseSwagger(
                settings =>
                    {
                        settings.Path = "/swagger/"+ reverseProxyOptions.AppVersion + "/swagger.json";
                        settings.PostProcess = (document, request) =>
                  {
                      document.Schemes = new[] { request.Headers["X-Forwarded-Proto"].FirstOrDefault() == "http" ? SwaggerSchema.Http : SwaggerSchema.Https };
                      document.Info.Title = reverseProxyOptions.AppTitle;
                      document.Info.Version = reverseProxyOptions.AppVersion;
                      document.BasePath = reverseProxyOptions.AppPath;
                      document.Host = request.Headers["X-Forwarded-Host"].FirstOrDefault();
                  };
                    });
            app.UseSwaggerUi3(options =>
                {
                    options.DocumentPath = "/swagger/" + reverseProxyOptions.AppVersion + "/swagger.json";
                });