dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.

Home Page:https://asp.net

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[9.0-preview.4] OpenAPI response does get served if a static file with the same path exists as a file

martincostello opened this issue · comments

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

I'm not sure if this is a bug or not, but it's certainly an unexpected behaviour to me.

Because NSwag doesn't support native AoT, I have an application where I have a setup like the following:

  • NSwag renders the OpenAPI response when dynamic code is supported.
  • A copy of that document is stored in wwwroot as a fall back for the deployed native AoT app.
  • UseOpenApi() is registered before UseStaticFiles().

For the purposes of comparison, I disabled NSwag and configured the new OpenAPI support for .NET 9 preview 4 to serve the new generated document from the same URL:

app.MapOpenApi("/swagger/api/swagger.json");
app.UseStaticFiles();

In this scenario I would expect the application to behave the same, but instead I always get the content from the static file instead.

If I delete the file, then I get the generated content. If I then restore the file, I get the static one again.

Expected Behavior

I would expect the generated content to always be served as the OpenAPI middleware is registered before static files.

Steps To Reproduce

  1. Configure an API application with the new OpenAPI support and static files.
  2. Save the content generated by MapOpenApi() into wwwroot at a location that has the same path, but make a trivial edit so you can tell which content is served.
  3. Run the application and request the URL for the Open API document.

Exceptions (if any)

No response

.NET Version

9.0.100-preview.4.24267.66

Anything else?

No response

Interesting bug! I assume that for the purposes of this repro you don't actually need to have NSwag.AspNetCore installed in the application and that MapOpenApi + UseStaticFiles is enough . To that end, I have this Program.cs:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenApi();

var app = builder.Build();

app.MapOpenApi("/swagger/api/swagger.json");
app.UseStaticFiles();

app.MapGet("/", () => "Hello World!");

app.Run();

And the following directory structure:

$ tree -I "bin|obj"
.
├── NSwagOpenApi.csproj
├── NSwagOpenApi.sln
├── Program.cs
├── Properties
│   └── launchSettings.json
├── appsettings.Development.json
├── appsettings.json
└── wwwroot
    └── swagger
        └── api
            └── swagger.json

4 directories, 7 files

With this setup, I get the behavior that I expect to see: which is that the generated content is always registered because the endpoint routing middleware runs before the static files middleware in the default setup for the minimal host. If I comment ou the app.MapOpenApi(..) line, I see that the static file is rendered because endpoint routing fails to resolve an endpoint and falls back to the static files middleware.

Is there a chance the middleware registration differs in your setup?

I'll take a look into this again later this week and see if I can work out what the factor that causes the behaviour is (or if I was somehow being an idiot 🙃).

I'll take a look into this again later this week and see if I can work out what the factor that causes the behaviour is (or if I was somehow being an idiot 🙃).

You? Never! Take your time. Hopefully we don't discover anything spooky here 😅

I haven't ruled out me being stupid or overlooking something, but I definitely get the wrong OpenAPI document here: martincostello/api@ec552ac.

I would expect the Dynamic_Schema_Is_Served() test to pass because it gets the OpenAPI document with a version of 3.0.1 as it's coming from the new library, but instead I get 3.0.0 from NSwag.

I've ripped out everything related to NSwag in this commit, and I can still repro it: martincostello/api@997aa2b

If I delete the static file the test passes as I get the generated document, otherwise it fails because I get the content from disk.

Still seeing this with preview 5.

@martincostello I think I figured out the isuse here as it pertains to the differences between my minimal repo here and your sample app.

I believe the gotcha here is that the routing middleware is registered after the static files middleware in your API:

if (RuntimeFeature.IsDynamicCodeSupported)
{
    app.MapOpenApi("/swagger/{documentName}/swagger.json");
}

app.UseStaticFiles();

app.UseRouting();

In this case, the static files middleware always runs before endpoint routing terminates the middleware pipeline before endpoint routing is able to kick in and map the request to the OpenAPI document endpoint that is registered. In the repro from my comment, the endpoint routing middleware is implicitly registered first and always processes the request.

Moving endpoint routing before the static files middleware should resolve the issue. As long as you continue not registering MapOpenApi when dynamic code is not supported endpoint routing should 404 and fallback to static files.

Edit with some additional thoughts on this for posterity and others reading:

The fact that we use an endpoint to represent the OpenAPI document is a big divergence between what users might be familiar with in pages like NSwag and Swashbuckle. This issue is an example of how this divergence comes into play with unexpected behavior WRTs to middleware ordering. We'll probably want to document what this means for end-users a bit more clearly.

Ahhhhhh. Good detective work 🕵️‍♀️

It didn't even occur to me to look at that!

The repro project started out back when we still had project.json, so there's probably a few things lurking I've forgotten about/not paid much attention to in a long time that have continued to work with no real issues and this has gotten caught up with that.

I'll just take that out tomorrow and recover my sanity 🤣

I've got this working as expected in my repo now with UseRouting() removed completely - feel free to close.

This issue has been resolved and has not had any activity for 1 day. It will be closed for housekeeping purposes.

See our Issue Management Policies for more information.