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

Swagger fails to load resources when my site is running under a virtual directory.

Open niemyjski opened this issue 9 years ago • 31 comments

I noticed that everything is working fine when my site is running at (http://localhost:50000) but fails if I change my site to use a virtual directory (http://localhost/exceptionless).

Here is a link to our configuration: https://github.com/exceptionless/Exceptionless/blob/master/Source/Api/AppBuilder.cs#L148

Here is how I initially found out about this issue: https://github.com/exceptionless/Exceptionless/issues/101

Has anyone else ran into this issue? Any ideas on a work around for this behavior?

niemyjski avatar Apr 29 '15 20:04 niemyjski

Can you elaborate a little? For example, is your server even responding to the swagger endpoints (swagger/ui, swagger/docs/v1)? If so, are you seeing JavaScript errors in the swagger-ui?

domaindrivendev avatar Apr 29 '15 21:04 domaindrivendev

It doesn't seem to be loading the swagger schema:

image

niemyjski avatar Apr 29 '15 21:04 niemyjski

Hmmm - the virtual directory is missing in the discovery URL above, hence the 404. If you update the URL in the input field above to include it and then click "Explore", I imagine you'll get the docs to show up.

Let me know if this works?

If it does, you'll likely get a similar error if you subsequently "Try out" one of the operations because the same code is used to determine the API basePath in the Swagger json. Namely the following ...

    public static string DefaultRootUrlResolver(HttpRequestMessage request)
    {
        var scheme = GetHeaderValue(request, "X-Forwarded-Proto") ?? request.RequestUri.Scheme;
        var host = GetHeaderValue(request, "X-Forwarded-Host") ?? request.RequestUri.Host;
        var port = GetHeaderValue(request, "X-Forwarded-Port") ?? request.RequestUri.Port.ToString(CultureInfo.InvariantCulture);

        var httpConfiguration = request.GetConfiguration();
        var virtualPathRoot = httpConfiguration.VirtualPathRoot.TrimEnd('/');

        return string.Format("{0}://{1}:{2}{3}", scheme, host, port, virtualPathRoot);
    } 

Can't think of a good reason why virtualPathRoot would be empty here but it looks like that's what could be happening.

As a side note, you can override this logic in SwaggerConfig.cs via the RootUrl option. If you can figure out a better way to reliably determine the root url of your website, you can easily wire it up as a workaround.

domaindrivendev avatar Apr 29 '15 22:04 domaindrivendev

Yeah, I'll take a look and let you know. I was hoping this would be handled by the underlying library as we are an open source project too and someone could be hosting this in really weird configurations.

niemyjski avatar Apr 29 '15 23:04 niemyjski

Getting other reports of this issue (see referenced ticket). It seems httpConfig.VirtualPathRoot can't be relied on in all environments.

I'm going to try get something more robust in there for the next release

In the meantime, you can always wire up your own root url resolver via the RootUrl config setting (see readme)

domaindrivendev avatar May 13 '15 16:05 domaindrivendev

Thanks for the heads up!

niemyjski avatar May 13 '15 21:05 niemyjski

@domaindrivendev any ideas when this fix will go live?

niemyjski avatar Jul 01 '15 15:07 niemyjski

Has this been fixed?

niemyjski avatar Jul 28 '15 18:07 niemyjski

I was able to dynamically set the rooturl via the EnableSwagger callback:

c.RootUrl(req => new Uri(req.RequestUri, HttpContext.Current.Request.ApplicationPath ?? string.Empty).ToString());

spardo avatar Jul 30 '15 13:07 spardo

@domaindrivendev any luck getting this resolved?

niemyjski avatar Oct 29 '15 15:10 niemyjski

The only way I found to make it works is to add runAllManagedModulesForAllRequests="true" in the web.config... Otherwise, its doesn't read the Swagger JSON...

I also use the rooturi submitted by @spardo

Adriien-M avatar Oct 29 '15 16:10 Adriien-M

Has there been any updates on this?

niemyjski avatar Jan 12 '16 17:01 niemyjski

@niemyjski c.RootUrl(req => { var pathBase = req.GetOwinContext().Get<string>("owin.RequestPathBase"); return new Uri(req.RequestUri, pathBase).ToString(); }); Works for me.

StanislavKuzymkiv avatar Apr 26 '16 09:04 StanislavKuzymkiv

Be nice to have a more perm fix that works on .net core (non owin as well ) :)

niemyjski avatar Dec 15 '16 21:12 niemyjski

how we can fix this in .net core

sachmahajan avatar Jan 04 '17 20:01 sachmahajan

Not for OWIN:

c.RootUrl(req => req.RequestUri.GetLeftPart(UriPartial.Authority).TrimEnd('/') +
                 '/' +
                 req.GetRequestContext().VirtualPathRoot.TrimStart('/'));

testfirstcoder avatar Mar 16 '17 13:03 testfirstcoder

For .NET Core use Swashbuckle.AspNetCore instead.

testfirstcoder avatar Mar 16 '17 13:03 testfirstcoder

I have this issue on .net core / IIS 7.5, using the AspNetCore package

jlanng avatar Mar 16 '17 15:03 jlanng

@Adriien-M's comment on issue #42 worked in my case when VirtualPathRoot wasn't:

c.RootUrl(req => req.RequestUri.GetLeftPart(UriPartial.Authority) + VirtualPathUtility.ToAbsolute("~/").TrimEnd('/'));

csrowell avatar Jun 20 '17 13:06 csrowell

I had a similar problem with Swashbuckle.AspNetCore in my .NET Core Web API project; I was able to get the correct discovery URL to appear (and change the prefix) by adding the following to Configure():

app.UseSwagger(c =>
{
    c.RouteTemplate = "api-docs/{documentName}/swagger.json";
});
app.UseSwaggerUI(c =>
{
    c.RoutePrefix = "api-docs";
    c.SwaggerEndpoint("v1/swagger.json", "My API v1"); // will be relative to route prefix, which is itself relative to the application basepath
});

Atenista avatar Jul 28 '17 02:07 Atenista

adding a relative path worked for me

            app.UseSwaggerUI(s => {
                s.RoutePrefix = "help";
                s.SwaggerEndpoint("../swagger/v1/swagger.json", "MySite");
                s.InjectStylesheet("../css/swagger.min.css");
            });

scastaldi avatar Aug 28 '17 16:08 scastaldi

@scastaldi but did you have any problems with using the UI later? I came up with the same solution, but the "Try it!" buttons keep sending the request on domain only URLs.

quilin avatar Sep 29 '17 09:09 quilin

@quilin I'm not sure what do you mean with domain only when you click "Try it out" button, everything it should be based on your current domain, from that everything should be based on the relative path

remember to add app.UseStaticFiles() This is a more detail version of my code, I hope it helps...

        public void ConfigureServices(IServiceCollection services)
        {
            // Add framework services.
            services.AddMvc();
            services.AddSwaggerGen(c => {
                c.SwaggerDoc("v1", new Swashbuckle.AspNetCore.Swagger.Info { Title = "Salesforce Utility", Version = "v1" });
                var filePath = Path.Combine(PlatformServices.Default.Application.ApplicationBasePath, "SalesforceWebUtil.xml");
                c.IncludeXmlComments(filePath);
            });
            // initialize configuration
            string env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
            string e = ConfigurationHelper.GetEnvironment(new string[] { env });
            var conf = new ConfigurationHelper(Microsoft.DotNet.PlatformAbstractions.ApplicationEnvironment.ApplicationBasePath, e);
            Configuration = conf.Configuration; // just in case
            // inject the RestApiWrapperService as singleton into the services configuration
            var restService = new RestApiWrapperService(conf);
            services.AddSingleton<IRestApiWrapperService>(restService);
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole(Configuration.GetSection("Logging"));
            loggerFactory.AddDebug();

            app.UseSwagger();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseBrowserLink();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }

            app.UseStaticFiles();
            // app.UseMvc();
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });

            app.UseSwaggerUI(s => {
                s.RoutePrefix = "help";
                s.SwaggerEndpoint("../swagger/v1/swagger.json", "Salesforce Utility");
                s.InjectStylesheet("../css/swagger.min.css");
            });
        }

scastaldi avatar Sep 29 '17 15:09 scastaldi

For me the following config work, the thing is that i had to deploy in two server, one with virtualDir (sub-folders) and the other no, bouth on IIS. This work for the two scenario:

        app.UseSwagger(c =>
        {
            c.PreSerializeFilters.Add((swaggerDoc, httpReq) =>
            {
                swaggerDoc.Host = httpReq.Host.Value;
                swaggerDoc.Schemes = new List<string>() {httpReq.Scheme};
                swaggerDoc.BasePath = httpReq.PathBase;
            });
        });

        app.UseSwaggerUI(c =>
        {
            c.SwaggerEndpoint("./v1/swagger.json", "NewHotelBackOffice API V1");
            c.RoutePrefix = string.Empty;
        });

miguelg1583 avatar Jul 04 '19 00:07 miguelg1583

@miguelg1583 Where to add this configuration in project? I'm using spring mvc along with jersey

SaxenaAryan avatar Mar 02 '20 02:03 SaxenaAryan

miguelg1583 suggestion works for me

app.UseSwagger(c =>
            {
                c.RouteTemplate = "api-docs/{documentName}/swagger.json";
            });
            app.UseSwaggerUI(c =>
            {
                c.RoutePrefix = "api-docs";
                c.SwaggerEndpoint("v1/swagger.json", "v1");
                
            });

hdu7 avatar Sep 11 '20 18:09 hdu7

adding a relative path worked for me

app.UseSwaggerUI(c => { c.SwaggerEndpoint("./swagger/v1/swagger.json", "API V1"); c.RoutePrefix = string.Empty; });

agusalay avatar Oct 14 '20 09:10 agusalay

I discovered that {virtual directory}/swagger is added if you don't start the specification with a /, hence "v1/swagger.json" is the same as "/swagger/v1/swagger.json" when you don't have a virtual directory, but when you have one specifying it the first way will add it automatically. So the following solution works well:

app.UseSwagger(); app.UseSwaggerUI(c => { c.SwaggerEndpoint("v1/swagger.json", "My Api v1"); });

oyzar avatar Dec 03 '20 17:12 oyzar

@oyzar thanks work for me

app.UseSwagger(); app.UseSwaggerUI(c => { c.SwaggerEndpoint("v1/swagger.json", "My Api v1"); });

leodeveloper avatar Feb 17 '21 10:02 leodeveloper

Swagger is integrated into .net5 API project template.

 public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {

            services.AddControllers();
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebApplication3", Version = "v1" });
            });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseSwagger();
                app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "WebApplication3 v1"));
            }

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthorization();

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

HaiyanDu7 avatar Feb 17 '21 12:02 HaiyanDu7