diff --git a/src/OpenApi/src/Extensions/OpenApiEndpointRouteBuilderExtensions.cs b/src/OpenApi/src/Extensions/OpenApiEndpointRouteBuilderExtensions.cs index c5bed38669e4..de74fd8d1257 100644 --- a/src/OpenApi/src/Extensions/OpenApiEndpointRouteBuilderExtensions.cs +++ b/src/OpenApi/src/Extensions/OpenApiEndpointRouteBuilderExtensions.cs @@ -43,7 +43,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(documentName); 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 5d678e67c8c7..b907cc6ecb20 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; @@ -55,7 +56,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) { // For good hygiene, operation-level tags must also appear in the document-level // tags collection. This set captures all tags that have been seen so far. @@ -74,7 +75,7 @@ public async Task GetOpenApiDocumentAsync(IServiceProvider scop { Info = GetOpenApiInfo(), Paths = await GetOpenApiPathsAsync(capturedTags, scopedServiceProvider, operationTransformers, schemaTransformers, cancellationToken), - Servers = GetOpenApiServers(), + Servers = GetOpenApiServers(httpRequest), Tags = [.. capturedTags] }; try @@ -192,12 +193,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 e773ebf5ff89..b33eb153de4c 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentServiceTestsBase.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentServiceTestsBase.cs @@ -35,16 +35,16 @@ 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); } - public static async Task VerifyOpenApiDocument(ActionDescriptor action, Action verifyOpenApiDocument) + public static async Task VerifyOpenApiDocument(ActionDescriptor action, Action verifyOpenApiDocument, CancellationToken cancellationToken = default) { var builder = CreateBuilder(); var documentService = CreateDocumentService(builder, action); var scopedService = ((TestServiceProvider)builder.ServiceProvider).CreateScope(); - var document = await documentService.GetOpenApiDocumentAsync(scopedService.ServiceProvider); + var document = await documentService.GetOpenApiDocumentAsync(scopedService.ServiceProvider, null); verifyOpenApiDocument(document); }