diff --git a/src/OpenApi/src/Services/OpenApiDocumentService.cs b/src/OpenApi/src/Services/OpenApiDocumentService.cs index f342f5b7943b..a2b070ae7981 100644 --- a/src/OpenApi/src/Services/OpenApiDocumentService.cs +++ b/src/OpenApi/src/Services/OpenApiDocumentService.cs @@ -209,6 +209,12 @@ internal List GetOpenApiServers(HttpRequest? httpRequest = null) if (httpRequest is not null) { var serverUrl = UriHelper.BuildAbsolute(httpRequest.Scheme, httpRequest.Host, httpRequest.PathBase); + // Remove trailing slash when pathBase is empty to align with OpenAPI specification. + // Keep the trailing slash if pathBase explicitly contains "/" to preserve intentional path structure. + if (serverUrl.EndsWith('/') && !httpRequest.PathBase.HasValue) + { + serverUrl = serverUrl.TrimEnd('/'); + } return [new OpenApiServer { Url = serverUrl }]; } else diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentLocalizationTests.VerifyOpenApiDocumentIsInvariant.verified.txt b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentLocalizationTests.VerifyOpenApiDocumentIsInvariant.verified.txt index eec2cfe16702..df155fbc8d21 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentLocalizationTests.VerifyOpenApiDocumentIsInvariant.verified.txt +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentLocalizationTests.VerifyOpenApiDocumentIsInvariant.verified.txt @@ -7,7 +7,7 @@ }, "servers": [ { - "url": "http://localhost/" + "url": "http://localhost" } ], "paths": { diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentService/OpenApiDocumentServiceTests.Servers.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentService/OpenApiDocumentServiceTests.Servers.cs index 967e84380d9d..e7aaea169e96 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentService/OpenApiDocumentServiceTests.Servers.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentService/OpenApiDocumentServiceTests.Servers.cs @@ -11,7 +11,7 @@ public partial class OpenApiDocumentServiceTests { [Theory] - [InlineData("Development", "localhost:5001", "", "http", "http://localhost:5001/")] + [InlineData("Development", "localhost:5001", "", "http", "http://localhost:5001")] [InlineData("Development", "example.com", "/api", "https", "https://example.com/api")] [InlineData("Staging", "localhost:5002", "/v1", "http", "http://localhost:5002/v1")] [InlineData("Staging", "api.example.com", "/base/path", "https", "https://api.example.com/base/path")] @@ -145,4 +145,39 @@ public void GetOpenApiServers_HandlesServerAddressFeatureWithNoValues() // Assert Assert.Empty(servers); } + + [Fact] + public void GetOpenApiServers_RemovesTrailingSlashWhenPathBaseIsEmpty() + { + // Arrange + var hostEnvironment = new HostingEnvironment + { + ApplicationName = "TestApplication", + EnvironmentName = "Development" + }; + var docService = new OpenApiDocumentService( + "v1", + new Mock().Object, + hostEnvironment, + GetMockOptionsMonitor(), + new Mock().Object, + new OpenApiTestServer(["http://localhost:5000"])); + var httpContext = new DefaultHttpContext() + { + Request = + { + Host = new HostString("example.com"), + PathBase = "", + Scheme = "https" + } + }; + + // Act + var servers = docService.GetOpenApiServers(httpContext.Request); + + // Assert + Assert.Single(servers); + Assert.Equal("https://example.com", servers[0].Url); + Assert.DoesNotContain("https://example.com/", servers.Select(s => s.Url)); + } }