diff --git a/src/OpenApi/src/Extensions/OpenApiEndpointRouteBuilderExtensions.cs b/src/OpenApi/src/Extensions/OpenApiEndpointRouteBuilderExtensions.cs index 52afbe7c58f5..fb36c71b1d8e 100644 --- a/src/OpenApi/src/Extensions/OpenApiEndpointRouteBuilderExtensions.cs +++ b/src/OpenApi/src/Extensions/OpenApiEndpointRouteBuilderExtensions.cs @@ -51,7 +51,7 @@ public static IEndpointConventionBuilder MapOpenApi(this IEndpointRouteBuilder e } else { - var document = await documentService.GetOpenApiDocumentAsync(context.RequestServices, context.RequestAborted); + var document = await documentService.GetOpenApiDocumentAsync(context.RequestServices, context.Request, context.RequestAborted); var documentOptions = options.Get(lowercasedDocumentName); using var output = MemoryBufferWriter.Get(); using var writer = Utf8BufferTextWriter.Get(output); diff --git a/src/OpenApi/src/Services/OpenApiDocumentService.cs b/src/OpenApi/src/Services/OpenApiDocumentService.cs index 73ebd9541cb9..c8775e6b8e24 100644 --- a/src/OpenApi/src/Services/OpenApiDocumentService.cs +++ b/src/OpenApi/src/Services/OpenApiDocumentService.cs @@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApiExplorer; @@ -56,7 +57,7 @@ internal sealed class OpenApiDocumentService( internal bool TryGetCachedOperationTransformerContext(string descriptionId, [NotNullWhen(true)] out OpenApiOperationTransformerContext? context) => _operationTransformerContextCache.TryGetValue(descriptionId, out context); - public async Task GetOpenApiDocumentAsync(IServiceProvider scopedServiceProvider, CancellationToken cancellationToken = default) + public async Task GetOpenApiDocumentAsync(IServiceProvider scopedServiceProvider, HttpRequest? httpRequest = null, CancellationToken cancellationToken = default) { // Schema and operation transformers are scoped per-request and can be // pre-allocated to hold the same number of transformers as the associated @@ -71,7 +72,7 @@ public async Task GetOpenApiDocumentAsync(IServiceProvider scop var document = new OpenApiDocument { Info = GetOpenApiInfo(), - Servers = GetOpenApiServers() + Servers = GetOpenApiServers(httpRequest) }; document.Paths = await GetOpenApiPathsAsync(document, scopedServiceProvider, operationTransformers, schemaTransformers, cancellationToken); document.Tags = document.Tags?.Distinct(OpenApiTagComparer.Instance).ToList(); @@ -198,12 +199,26 @@ internal OpenApiInfo GetOpenApiInfo() }; } - internal List GetOpenApiServers() + // Resolve server URL from the request to handle reverse proxies. + // If there is active request object, assume a development environment and use the server addresses. + internal List GetOpenApiServers(HttpRequest? httpRequest = null) + { + if (httpRequest is not null) + { + var serverUrl = UriHelper.BuildAbsolute(httpRequest.Scheme, httpRequest.Host, httpRequest.PathBase); + return [new OpenApiServer { Url = serverUrl }]; + } + else + { + return GetDevelopmentOpenApiServers(); + } + } + private List GetDevelopmentOpenApiServers() { if (hostEnvironment.IsDevelopment() && server?.Features.Get()?.Addresses is { Count: > 0 } addresses) { - return addresses.Select(address => new OpenApiServer { Url = address }).ToList(); + return [.. addresses.Select(address => new OpenApiServer { Url = address })]; } return []; } 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 c84c7e258510..1bc247c95ad4 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 @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.OpenApi; using Microsoft.Extensions.DependencyInjection; @@ -10,6 +11,45 @@ public partial class OpenApiDocumentServiceTests { + [Theory] + [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")] + [InlineData("Development", "localhost", "/", "http", "http://localhost/")] + public void GetOpenApiServers_FavorsHttpContextRequestOverServerAddress(string environment, string host, string pathBase, string scheme, string expectedUri) + { + // Arrange + var hostEnvironment = new HostingEnvironment + { + ApplicationName = "TestApplication", + EnvironmentName = environment + }; + 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(host), + PathBase = pathBase, + Scheme = scheme + + } + }; + + // Act + var servers = docService.GetOpenApiServers(httpContext.Request); + + // Assert + Assert.Contains(expectedUri, servers.Select(s => s.Url)); + } + [Fact] public void GetOpenApiServers_HandlesServerAddressFeatureWithValues() { diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentServiceTestsBase.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentServiceTestsBase.cs index 857325fa0cd3..aa2246385b31 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentServiceTestsBase.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentServiceTestsBase.cs @@ -34,7 +34,7 @@ public static async Task VerifyOpenApiDocument(IEndpointRouteBuilder builder, Op { var documentService = CreateDocumentService(builder, openApiOptions); var scopedService = ((TestServiceProvider)builder.ServiceProvider).CreateScope(); - var document = await documentService.GetOpenApiDocumentAsync(scopedService.ServiceProvider, cancellationToken); + var document = await documentService.GetOpenApiDocumentAsync(scopedService.ServiceProvider, null, cancellationToken); verifyOpenApiDocument(document); } @@ -43,7 +43,7 @@ public static async Task VerifyOpenApiDocument(ActionDescriptor action, Action