From 586a3f83cb40addb3d1f797738d3eea7dc85db89 Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Fri, 14 Feb 2025 17:11:46 +0100 Subject: [PATCH] Refactor actuator middleware: move parsing of request (body) into separate virtual method, so that handler invocation is agnostic of HttpContext --- .../CloudFoundryEndpointMiddleware.cs | 27 ++++------ .../DbMigrationsEndpointMiddleware.cs | 6 +-- .../EnvironmentEndpointMiddleware.cs | 5 +- .../Health/HealthEndpointMiddleware.cs | 54 +++++++++++-------- .../HeapDump/HeapDumpEndpointMiddleware.cs | 21 ++++---- .../HttpExchangesEndpointMiddleware.cs | 5 +- .../HypermediaEndpointMiddleware.cs | 38 ++++++------- .../Actuators/Info/InfoEndpointMiddleware.cs | 5 +- .../Loggers/LoggersEndpointMiddleware.cs | 54 +++++++++---------- .../Refresh/RefreshEndpointMiddleware.cs | 5 +- .../RouteMappingsEndpointMiddleware.cs | 7 ++- .../Services/ServicesEndpointMiddleware.cs | 5 +- .../ThreadDumpEndpointMiddleware.cs | 9 +--- .../Endpoint/Middleware/EndpointMiddleware.cs | 50 +++++++++++------ .../src/Endpoint/PublicAPI.Unshipped.txt | 21 ++++---- .../SpringBootAdminClient/TestMiddleware.cs | 2 +- 16 files changed, 158 insertions(+), 156 deletions(-) diff --git a/src/Management/src/Endpoint/Actuators/CloudFoundry/CloudFoundryEndpointMiddleware.cs b/src/Management/src/Endpoint/Actuators/CloudFoundry/CloudFoundryEndpointMiddleware.cs index 3dad45f1e4..d265774afb 100644 --- a/src/Management/src/Endpoint/Actuators/CloudFoundry/CloudFoundryEndpointMiddleware.cs +++ b/src/Management/src/Endpoint/Actuators/CloudFoundry/CloudFoundryEndpointMiddleware.cs @@ -20,27 +20,22 @@ internal sealed class CloudFoundryEndpointMiddleware( ICloudFoundryEndpointHandler endpointHandler, IOptionsMonitor managementOptionsMonitor, ILoggerFactory loggerFactory) : EndpointMiddleware(endpointHandler, managementOptionsMonitor, loggerFactory) { - private readonly ILogger _logger = loggerFactory.CreateLogger(); - - protected override async Task InvokeEndpointHandlerAsync(HttpContext context, CancellationToken cancellationToken) + protected override Task ParseRequestAsync(HttpContext httpContext, CancellationToken cancellationToken) { - ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(httpContext); - _logger.LogDebug("InvokeAsync({Method}, {Path})", context.Request.Method, context.Request.Path.Value); - string uri = GetRequestUri(context); - return await EndpointHandler.InvokeAsync(uri, cancellationToken); + string scheme = httpContext.Request.Headers.TryGetValue("X-Forwarded-Proto", out StringValues headerScheme) + ? headerScheme.ToString() + : httpContext.Request.Scheme; + + string uri = $"{scheme}://{httpContext.Request.Host}{httpContext.Request.PathBase}{httpContext.Request.Path}"; + return Task.FromResult(uri); } - private string GetRequestUri(HttpContext context) + protected override async Task InvokeEndpointHandlerAsync(string? uri, CancellationToken cancellationToken) { - HttpRequest request = context.Request; - string scheme = request.Scheme; + ArgumentNullException.ThrowIfNull(uri); - if (request.Headers.TryGetValue("X-Forwarded-Proto", out StringValues headerScheme)) - { - scheme = headerScheme.ToString(); - } - - return $"{scheme}://{request.Host}{request.PathBase}{request.Path}"; + return await EndpointHandler.InvokeAsync(uri, cancellationToken); } } diff --git a/src/Management/src/Endpoint/Actuators/DbMigrations/DbMigrationsEndpointMiddleware.cs b/src/Management/src/Endpoint/Actuators/DbMigrations/DbMigrationsEndpointMiddleware.cs index ea4319ea6f..56ec50d70e 100644 --- a/src/Management/src/Endpoint/Actuators/DbMigrations/DbMigrationsEndpointMiddleware.cs +++ b/src/Management/src/Endpoint/Actuators/DbMigrations/DbMigrationsEndpointMiddleware.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Steeltoe.Management.Endpoint.Configuration; @@ -14,9 +13,8 @@ internal sealed class DbMigrationsEndpointMiddleware( IDbMigrationsEndpointHandler endpointHandler, IOptionsMonitor managementOptionsMonitor, ILoggerFactory loggerFactory) : EndpointMiddleware>(endpointHandler, managementOptionsMonitor, loggerFactory) { - protected override async Task> InvokeEndpointHandlerAsync(HttpContext context, - CancellationToken cancellationToken) + protected override async Task> InvokeEndpointHandlerAsync(object? request, CancellationToken cancellationToken) { - return await EndpointHandler.InvokeAsync(null, cancellationToken); + return await EndpointHandler.InvokeAsync(request, cancellationToken); } } diff --git a/src/Management/src/Endpoint/Actuators/Environment/EnvironmentEndpointMiddleware.cs b/src/Management/src/Endpoint/Actuators/Environment/EnvironmentEndpointMiddleware.cs index d144214194..874bf186da 100644 --- a/src/Management/src/Endpoint/Actuators/Environment/EnvironmentEndpointMiddleware.cs +++ b/src/Management/src/Endpoint/Actuators/Environment/EnvironmentEndpointMiddleware.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Steeltoe.Management.Endpoint.Configuration; @@ -14,8 +13,8 @@ internal sealed class EnvironmentEndpointMiddleware( IEnvironmentEndpointHandler endpointHandler, IOptionsMonitor managementOptionsMonitor, ILoggerFactory loggerFactory) : EndpointMiddleware(endpointHandler, managementOptionsMonitor, loggerFactory) { - protected override async Task InvokeEndpointHandlerAsync(HttpContext context, CancellationToken cancellationToken) + protected override async Task InvokeEndpointHandlerAsync(object? request, CancellationToken cancellationToken) { - return await EndpointHandler.InvokeAsync(null, context.RequestAborted); + return await EndpointHandler.InvokeAsync(request, cancellationToken); } } diff --git a/src/Management/src/Endpoint/Actuators/Health/HealthEndpointMiddleware.cs b/src/Management/src/Endpoint/Actuators/Health/HealthEndpointMiddleware.cs index 83f4045be6..0655c70005 100644 --- a/src/Management/src/Endpoint/Actuators/Health/HealthEndpointMiddleware.cs +++ b/src/Management/src/Endpoint/Actuators/Health/HealthEndpointMiddleware.cs @@ -32,40 +32,37 @@ public override ActuatorMetadataProvider GetMetadataProvider() return new HealthActuatorMetadataProvider(ContentType); } - protected override async Task InvokeEndpointHandlerAsync(HttpContext context, CancellationToken cancellationToken) + protected override Task ParseRequestAsync(HttpContext httpContext, CancellationToken cancellationToken) { - HealthEndpointOptions currentEndpointOptions = _endpointOptionsMonitor.CurrentValue; - string groupName = GetRequestedHealthGroup(context.Request.Path, currentEndpointOptions, _logger); + ArgumentNullException.ThrowIfNull(httpContext); - if (!IsValidGroup(groupName, currentEndpointOptions)) + HealthEndpointRequest? request = null; + HealthEndpointOptions options = _endpointOptionsMonitor.CurrentValue; + string groupName = GetRequestedHealthGroup(httpContext.Request.Path, options); + + if (IsValidGroup(groupName, options)) { - return new HealthEndpointResponse - { - Exists = false - }; + bool hasClaim = GetHasClaim(httpContext, options); + request = new HealthEndpointRequest(groupName, hasClaim); } - bool hasClaim = GetHasClaim(context, currentEndpointOptions); - - var request = new HealthEndpointRequest(groupName, hasClaim); - return await EndpointHandler.InvokeAsync(request, context.RequestAborted); + return Task.FromResult(request); } /// /// Returns the last segment of the HTTP request path, which is expected to be the name of a configured health group. /// - private static string GetRequestedHealthGroup(PathString requestPath, HealthEndpointOptions endpointOptions, ILogger logger) + private string GetRequestedHealthGroup(PathString requestPath, HealthEndpointOptions endpointOptions) { string[] requestComponents = requestPath.Value?.Split('/') ?? []; if (requestComponents.Length > 0 && requestComponents[^1] != endpointOptions.Id) { - logger.LogTrace("Found group '{HealthGroup}' in the request path.", requestComponents[^1]); + _logger.LogTrace("Found group '{HealthGroup}' in the request path.", requestComponents[^1]); return requestComponents[^1]; } - logger.LogTrace("Did not find a health group in the request path."); - + _logger.LogTrace("Did not find a health group in the request path."); return string.Empty; } @@ -80,20 +77,33 @@ private static bool GetHasClaim(HttpContext context, HealthEndpointOptions endpo return claim is { Type: not null, Value: not null } && context.User.HasClaim(claim.Type, claim.Value); } - protected override async Task WriteResponseAsync(HealthEndpointResponse result, HttpContext context, CancellationToken cancellationToken) + protected override async Task InvokeEndpointHandlerAsync(HealthEndpointRequest? request, CancellationToken cancellationToken) + { + if (request == null) + { + return new HealthEndpointResponse + { + Exists = false + }; + } + + return await EndpointHandler.InvokeAsync(request, cancellationToken); + } + + protected override async Task WriteResponseAsync(HealthEndpointResponse response, HttpContext httpContext, CancellationToken cancellationToken) { - if (!result.Exists) + if (!response.Exists) { - context.Response.StatusCode = (int)HttpStatusCode.NotFound; + httpContext.Response.StatusCode = (int)HttpStatusCode.NotFound; return; } - if (ManagementOptionsMonitor.CurrentValue.UseStatusCodeFromResponse || UseStatusCodeFromResponseInHeader(context.Request.Headers)) + if (ManagementOptionsMonitor.CurrentValue.UseStatusCodeFromResponse || UseStatusCodeFromResponseInHeader(httpContext.Request.Headers)) { - context.Response.StatusCode = ((HealthEndpointHandler)EndpointHandler).GetStatusCode(result); + httpContext.Response.StatusCode = ((HealthEndpointHandler)EndpointHandler).GetStatusCode(response); } - await base.WriteResponseAsync(result, context, cancellationToken); + await base.WriteResponseAsync(response, httpContext, cancellationToken); } private static bool UseStatusCodeFromResponseInHeader(IHeaderDictionary requestHeaders) diff --git a/src/Management/src/Endpoint/Actuators/HeapDump/HeapDumpEndpointMiddleware.cs b/src/Management/src/Endpoint/Actuators/HeapDump/HeapDumpEndpointMiddleware.cs index cb6eb66d09..f9bacc9bd6 100644 --- a/src/Management/src/Endpoint/Actuators/HeapDump/HeapDumpEndpointMiddleware.cs +++ b/src/Management/src/Endpoint/Actuators/HeapDump/HeapDumpEndpointMiddleware.cs @@ -16,31 +16,34 @@ internal sealed class HeapDumpEndpointMiddleware( : EndpointMiddleware(endpointHandler, managementOptionsMonitor, loggerFactory) { private readonly ILogger _logger = loggerFactory.CreateLogger(); - private protected override string ContentType { get; } = "application/octet-stream"; - protected override async Task InvokeEndpointHandlerAsync(HttpContext context, CancellationToken cancellationToken) + private protected override string ContentType => "application/octet-stream"; + + protected override async Task InvokeEndpointHandlerAsync(object? request, CancellationToken cancellationToken) { - return await EndpointHandler.InvokeAsync(null, context.RequestAborted); + return await EndpointHandler.InvokeAsync(request, cancellationToken); } - protected override async Task WriteResponseAsync(string? fileName, HttpContext context, CancellationToken cancellationToken) + protected override async Task WriteResponseAsync(string? fileName, HttpContext httpContext, CancellationToken cancellationToken) { + ArgumentNullException.ThrowIfNull(httpContext); + _logger.LogDebug("Returning: {FileName}", fileName); if (!File.Exists(fileName)) { - context.Response.StatusCode = StatusCodes.Status404NotFound; + httpContext.Response.StatusCode = StatusCodes.Status404NotFound; return; } - context.Response.ContentType = ContentType; - context.Response.Headers.Append("Content-Disposition", $"attachment; filename=\"{Path.GetFileName(fileName)}.gz\""); - context.Response.StatusCode = StatusCodes.Status200OK; + httpContext.Response.ContentType = ContentType; + httpContext.Response.Headers.Append("Content-Disposition", $"attachment; filename=\"{Path.GetFileName(fileName)}.gz\""); + httpContext.Response.StatusCode = StatusCodes.Status200OK; try { await using var inputStream = new FileStream(fileName, FileMode.Open); - await using var outputStream = new GZipStream(context.Response.Body, CompressionLevel.Fastest, true); + await using var outputStream = new GZipStream(httpContext.Response.Body, CompressionLevel.Fastest, true); await inputStream.CopyToAsync(outputStream, cancellationToken); } finally diff --git a/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangesEndpointMiddleware.cs b/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangesEndpointMiddleware.cs index 06d926de1c..1d264ab7fc 100644 --- a/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangesEndpointMiddleware.cs +++ b/src/Management/src/Endpoint/Actuators/HttpExchanges/HttpExchangesEndpointMiddleware.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Steeltoe.Management.Endpoint.Configuration; @@ -14,8 +13,8 @@ internal sealed class HttpExchangesEndpointMiddleware( IHttpExchangesEndpointHandler endpointHandler, IOptionsMonitor managementOptionsMonitor, ILoggerFactory loggerFactory) : EndpointMiddleware(endpointHandler, managementOptionsMonitor, loggerFactory) { - protected override async Task InvokeEndpointHandlerAsync(HttpContext context, CancellationToken cancellationToken) + protected override async Task InvokeEndpointHandlerAsync(object? request, CancellationToken cancellationToken) { - return await EndpointHandler.InvokeAsync(null, cancellationToken); + return await EndpointHandler.InvokeAsync(request, cancellationToken); } } diff --git a/src/Management/src/Endpoint/Actuators/Hypermedia/HypermediaEndpointMiddleware.cs b/src/Management/src/Endpoint/Actuators/Hypermedia/HypermediaEndpointMiddleware.cs index 442e918e17..5ebb437991 100644 --- a/src/Management/src/Endpoint/Actuators/Hypermedia/HypermediaEndpointMiddleware.cs +++ b/src/Management/src/Endpoint/Actuators/Hypermedia/HypermediaEndpointMiddleware.cs @@ -15,33 +15,27 @@ internal sealed class HypermediaEndpointMiddleware( IActuatorEndpointHandler endpointHandler, IOptionsMonitor managementOptionsMonitor, ILoggerFactory loggerFactory) : EndpointMiddleware(endpointHandler, managementOptionsMonitor, loggerFactory) { - private readonly ILogger _logger = loggerFactory.CreateLogger(); - - protected override async Task InvokeEndpointHandlerAsync(HttpContext context, CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(context); - - _logger.LogDebug("InvokeAsync({Method}, {Path})", context.Request.Method, context.Request.Path.Value); - string requestUri = GetRequestUri(context.Request); - return await EndpointHandler.InvokeAsync(requestUri, cancellationToken); - } - - private static string GetRequestUri(HttpRequest request) + protected override Task ParseRequestAsync(HttpContext httpContext, CancellationToken cancellationToken) { - string scheme = request.Scheme; + ArgumentNullException.ThrowIfNull(httpContext); - if (request.Headers.TryGetValue("X-Forwarded-Proto", out StringValues headerScheme)) - { - scheme = headerScheme.ToString(); - } + string scheme = httpContext.Request.Headers.TryGetValue("X-Forwarded-Proto", out StringValues headerScheme) + ? headerScheme.ToString() + : httpContext.Request.Scheme; // request.Host automatically includes or excludes the port based on whether it is standard for the scheme // ... except when we manually change the scheme to match the X-Forwarded-Proto - if (scheme == "https" && request.Host.Port == 443) - { - return $"{scheme}://{request.Host.Host}{request.PathBase}{request.Path}"; - } + string requestUri = scheme == "https" && httpContext.Request.Host.Port == 443 + ? $"{scheme}://{httpContext.Request.Host.Host}{httpContext.Request.PathBase}{httpContext.Request.Path}" + : $"{scheme}://{httpContext.Request.Host}{httpContext.Request.PathBase}{httpContext.Request.Path}"; - return $"{scheme}://{request.Host}{request.PathBase}{request.Path}"; + return Task.FromResult(requestUri); + } + + protected override async Task InvokeEndpointHandlerAsync(string? requestUri, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(requestUri); + + return await EndpointHandler.InvokeAsync(requestUri, cancellationToken); } } diff --git a/src/Management/src/Endpoint/Actuators/Info/InfoEndpointMiddleware.cs b/src/Management/src/Endpoint/Actuators/Info/InfoEndpointMiddleware.cs index 7c2e0c1260..0b79207a7c 100644 --- a/src/Management/src/Endpoint/Actuators/Info/InfoEndpointMiddleware.cs +++ b/src/Management/src/Endpoint/Actuators/Info/InfoEndpointMiddleware.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Steeltoe.Management.Endpoint.Configuration; @@ -14,8 +13,8 @@ internal sealed class InfoEndpointMiddleware( IInfoEndpointHandler endpointHandler, IOptionsMonitor managementOptionsMonitor, ILoggerFactory loggerFactory) : EndpointMiddleware>(endpointHandler, managementOptionsMonitor, loggerFactory) { - protected override async Task> InvokeEndpointHandlerAsync(HttpContext context, CancellationToken cancellationToken) + protected override async Task> InvokeEndpointHandlerAsync(object? request, CancellationToken cancellationToken) { - return await EndpointHandler.InvokeAsync(null, cancellationToken); + return await EndpointHandler.InvokeAsync(request, cancellationToken); } } diff --git a/src/Management/src/Endpoint/Actuators/Loggers/LoggersEndpointMiddleware.cs b/src/Management/src/Endpoint/Actuators/Loggers/LoggersEndpointMiddleware.cs index 60d5a4e0aa..a29774a5ae 100644 --- a/src/Management/src/Endpoint/Actuators/Loggers/LoggersEndpointMiddleware.cs +++ b/src/Management/src/Endpoint/Actuators/Loggers/LoggersEndpointMiddleware.cs @@ -19,29 +19,21 @@ internal sealed class LoggersEndpointMiddleware( { private readonly ILogger _logger = loggerFactory.CreateLogger(); - protected override async Task InvokeEndpointHandlerAsync(HttpContext context, CancellationToken cancellationToken) + protected override async Task ParseRequestAsync(HttpContext httpContext, CancellationToken cancellationToken) { - LoggersRequest? loggersRequest = await GetLoggersRequestAsync(context); - return loggersRequest == null ? LoggersResponse.Error : await EndpointHandler.InvokeAsync(loggersRequest, cancellationToken); - } - - private async Task GetLoggersRequestAsync(HttpContext context) - { - HttpRequest request = context.Request; + ArgumentNullException.ThrowIfNull(httpContext); - if (context.Request.Method == "POST") + if (httpContext.Request.Method == "POST") { // POST - change a logger level - _logger.LogDebug("Incoming path: {Path}", request.Path.Value); - - string? basePath = ManagementOptionsMonitor.CurrentValue.GetBasePath(context.Request.Path); + string? basePath = ManagementOptionsMonitor.CurrentValue.GetBasePath(httpContext.Request.Path); string path = EndpointOptions.GetEndpointPath(basePath); - if (request.Path.StartsWithSegments(path, out PathString remaining) && remaining.HasValue) + if (httpContext.Request.Path.StartsWithSegments(path, out PathString remaining) && remaining.HasValue) { string loggerName = remaining.Value!.TrimStart('/'); - Dictionary change = await DeserializeRequestAsync(request.Body); + Dictionary change = await DeserializeRequestAsync(httpContext.Request.Body, cancellationToken); change.TryGetValue("configuredLevel", out string? level); @@ -63,11 +55,11 @@ internal sealed class LoggersEndpointMiddleware( return new LoggersRequest(); } - private async Task> DeserializeRequestAsync(Stream stream) + private async Task> DeserializeRequestAsync(Stream stream, CancellationToken cancellationToken) { try { - var dictionary = await JsonSerializer.DeserializeAsync>(stream); + var dictionary = await JsonSerializer.DeserializeAsync>(stream, cancellationToken: cancellationToken); if (dictionary != null) { @@ -82,25 +74,29 @@ private async Task> DeserializeRequestAsync(Stream st return []; } - protected override async Task WriteResponseAsync(LoggersResponse? result, HttpContext context, CancellationToken cancellationToken) + protected override async Task InvokeEndpointHandlerAsync(LoggersRequest? request, CancellationToken cancellationToken) + { + return request == null ? LoggersResponse.Error : await EndpointHandler.InvokeAsync(request, cancellationToken); + } + + protected override async Task WriteResponseAsync(LoggersResponse? response, HttpContext httpContext, CancellationToken cancellationToken) { - if (result is { HasError: true }) + ArgumentNullException.ThrowIfNull(httpContext); + + if (response is { HasError: true }) + { + httpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; + } + else if (response == null) { - context.Response.StatusCode = (int)HttpStatusCode.BadRequest; + httpContext.Response.StatusCode = (int)HttpStatusCode.NoContent; } else { - if (result == null) - { - context.Response.StatusCode = (int)HttpStatusCode.NoContent; - } - else - { - context.Response.Headers.Append("Content-Type", ContentType); + httpContext.Response.Headers.Append("Content-Type", ContentType); - JsonSerializerOptions options = ManagementOptionsMonitor.CurrentValue.SerializerOptions; - await JsonSerializer.SerializeAsync(context.Response.Body, result, options, cancellationToken); - } + JsonSerializerOptions options = ManagementOptionsMonitor.CurrentValue.SerializerOptions; + await JsonSerializer.SerializeAsync(httpContext.Response.Body, response, options, cancellationToken); } } } diff --git a/src/Management/src/Endpoint/Actuators/Refresh/RefreshEndpointMiddleware.cs b/src/Management/src/Endpoint/Actuators/Refresh/RefreshEndpointMiddleware.cs index 6486eddb9a..8425a64eb4 100644 --- a/src/Management/src/Endpoint/Actuators/Refresh/RefreshEndpointMiddleware.cs +++ b/src/Management/src/Endpoint/Actuators/Refresh/RefreshEndpointMiddleware.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Steeltoe.Management.Endpoint.Configuration; @@ -19,8 +18,8 @@ public override ActuatorMetadataProvider GetMetadataProvider() return new RefreshActuatorMetadataProvider(ContentType); } - protected override async Task> InvokeEndpointHandlerAsync(HttpContext context, CancellationToken cancellationToken) + protected override async Task> InvokeEndpointHandlerAsync(object? request, CancellationToken cancellationToken) { - return await EndpointHandler.InvokeAsync(null, cancellationToken); + return await EndpointHandler.InvokeAsync(request, cancellationToken); } } diff --git a/src/Management/src/Endpoint/Actuators/RouteMappings/RouteMappingsEndpointMiddleware.cs b/src/Management/src/Endpoint/Actuators/RouteMappings/RouteMappingsEndpointMiddleware.cs index dbf9e7c9c4..7b99dad36d 100644 --- a/src/Management/src/Endpoint/Actuators/RouteMappings/RouteMappingsEndpointMiddleware.cs +++ b/src/Management/src/Endpoint/Actuators/RouteMappings/RouteMappingsEndpointMiddleware.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Steeltoe.Management.Endpoint.Actuators.RouteMappings.ResponseTypes; @@ -19,10 +18,10 @@ internal sealed class RouteMappingsEndpointMiddleware( : EndpointMiddleware(endpointHandler, managementOptionsMonitor, loggerFactory) { // There is no difference between v2 and v3 responses. This override is a workaround for Spring Boot Admin: https://github.com/codecentric/spring-boot-admin/issues/4001. - private protected override string ContentType { get; } = "application/vnd.spring-boot.actuator.v2+json"; + private protected override string ContentType => "application/vnd.spring-boot.actuator.v2+json"; - protected override async Task InvokeEndpointHandlerAsync(HttpContext context, CancellationToken cancellationToken) + protected override async Task InvokeEndpointHandlerAsync(object? request, CancellationToken cancellationToken) { - return await EndpointHandler.InvokeAsync(null, context.RequestAborted); + return await EndpointHandler.InvokeAsync(request, cancellationToken); } } diff --git a/src/Management/src/Endpoint/Actuators/Services/ServicesEndpointMiddleware.cs b/src/Management/src/Endpoint/Actuators/Services/ServicesEndpointMiddleware.cs index 0c922c4260..d401064a9d 100644 --- a/src/Management/src/Endpoint/Actuators/Services/ServicesEndpointMiddleware.cs +++ b/src/Management/src/Endpoint/Actuators/Services/ServicesEndpointMiddleware.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Steeltoe.Management.Endpoint.Configuration; @@ -14,8 +13,8 @@ public sealed class ServicesEndpointMiddleware( IServicesEndpointHandler endpointHandler, IOptionsMonitor managementOptionsMonitor, ILoggerFactory loggerFactory) : EndpointMiddleware>(endpointHandler, managementOptionsMonitor, loggerFactory) { - protected override async Task> InvokeEndpointHandlerAsync(HttpContext context, CancellationToken cancellationToken) + protected override async Task> InvokeEndpointHandlerAsync(object? request, CancellationToken cancellationToken) { - return await EndpointHandler.InvokeAsync(null, cancellationToken); + return await EndpointHandler.InvokeAsync(request, cancellationToken); } } diff --git a/src/Management/src/Endpoint/Actuators/ThreadDump/ThreadDumpEndpointMiddleware.cs b/src/Management/src/Endpoint/Actuators/ThreadDump/ThreadDumpEndpointMiddleware.cs index 331ae1dd04..c306b2a306 100644 --- a/src/Management/src/Endpoint/Actuators/ThreadDump/ThreadDumpEndpointMiddleware.cs +++ b/src/Management/src/Endpoint/Actuators/ThreadDump/ThreadDumpEndpointMiddleware.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Steeltoe.Management.Endpoint.Configuration; @@ -14,12 +13,8 @@ internal sealed class ThreadDumpEndpointMiddleware( IThreadDumpEndpointHandler endpointHandler, IOptionsMonitor managementOptionsMonitor, ILoggerFactory loggerFactory) : EndpointMiddleware>(endpointHandler, managementOptionsMonitor, loggerFactory) { - private readonly ILogger _logger = loggerFactory.CreateLogger(); - - protected override async Task> InvokeEndpointHandlerAsync(HttpContext context, CancellationToken cancellationToken) + protected override async Task> InvokeEndpointHandlerAsync(object? request, CancellationToken cancellationToken) { - _logger.LogDebug("Executing ThreadDumpHandler"); - - return await EndpointHandler.InvokeAsync(null, cancellationToken); + return await EndpointHandler.InvokeAsync(request, cancellationToken); } } diff --git a/src/Management/src/Endpoint/Middleware/EndpointMiddleware.cs b/src/Management/src/Endpoint/Middleware/EndpointMiddleware.cs index 10dde96f69..62529cd7b1 100644 --- a/src/Management/src/Endpoint/Middleware/EndpointMiddleware.cs +++ b/src/Management/src/Endpoint/Middleware/EndpointMiddleware.cs @@ -13,17 +13,17 @@ namespace Steeltoe.Management.Endpoint.Middleware; -public abstract class EndpointMiddleware : IEndpointMiddleware +public abstract class EndpointMiddleware : IEndpointMiddleware { private readonly ILogger _logger; protected IOptionsMonitor ManagementOptionsMonitor { get; } - protected IEndpointHandler EndpointHandler { get; } + protected IEndpointHandler EndpointHandler { get; } public EndpointOptions EndpointOptions => EndpointHandler.Options; private protected virtual string ContentType => "application/vnd.spring-boot.actuator.v3+json"; - protected EndpointMiddleware(IEndpointHandler endpointHandler, IOptionsMonitor managementOptionsMonitor, + protected EndpointMiddleware(IEndpointHandler endpointHandler, IOptionsMonitor managementOptionsMonitor, ILoggerFactory loggerFactory) { ArgumentNullException.ThrowIfNull(endpointHandler); @@ -32,7 +32,7 @@ protected EndpointMiddleware(IEndpointHandler endpointHandle EndpointHandler = endpointHandler; ManagementOptionsMonitor = managementOptionsMonitor; - _logger = loggerFactory.CreateLogger>(); + _logger = loggerFactory.CreateLogger>(); } public virtual ActuatorMetadataProvider GetMetadataProvider() @@ -57,6 +57,7 @@ public async Task InvokeAsync(HttpContext context, RequestDelegate? next) { if (!allowedVerbs.Contains(context.Request.Method)) { + _logger.LogTrace("{Method} method is unavailable at path {Path}.", context.Request.Method, context.Request.Path.Value); context.Response.StatusCode = (int)HttpStatusCode.MethodNotAllowed; } else if (!IsValidContentType(context.Request)) @@ -73,31 +74,39 @@ public async Task InvokeAsync(HttpContext context, RequestDelegate? next) } else { - TResult result = await InvokeEndpointHandlerAsync(context, context.RequestAborted); - await WriteResponseAsync(result, context, context.RequestAborted); + _logger.LogDebug("Reading {Method} request at path {Path} using {MiddlewareType}.", context.Request.Method, context.Request.Path.Value, + GetType()); + + TRequest? request = await ParseRequestAsync(context, context.RequestAborted); + TResponse response = await InvokeEndpointHandlerAsync(request, context.RequestAborted); + await WriteResponseAsync(response, context, context.RequestAborted); } return; } } + else + { + _logger.LogTrace("CanInvoke returned false for {Method} request at path {Path}.", context.Request.Method, context.Request.Path.Value); + } context.Response.StatusCode = (int)HttpStatusCode.NotFound; } - private bool IsValidContentType(HttpRequest request) + private bool IsValidContentType(HttpRequest httpRequest) { - if (request.ContentType == null) + if (httpRequest.ContentType == null) { return true; } // Media types are case-insensitive, according to https://stackoverflow.com/a/9842589. - return MediaTypeHeaderValue.TryParse(request.ContentType, out MediaTypeHeaderValue? headerValue) && headerValue.MatchesMediaType(ContentType); + return MediaTypeHeaderValue.TryParse(httpRequest.ContentType, out MediaTypeHeaderValue? headerValue) && headerValue.MatchesMediaType(ContentType); } - private bool IsCompatibleAcceptHeader(HttpRequest request) + private bool IsCompatibleAcceptHeader(HttpRequest httpRequest) { - string[] acceptHeaderValues = request.Headers.GetCommaSeparatedValues("Accept"); + string[] acceptHeaderValues = httpRequest.Headers.GetCommaSeparatedValues("Accept"); if (acceptHeaderValues.Length == 0) { @@ -115,20 +124,27 @@ private bool IsCompatibleAcceptHeader(HttpRequest request) return false; } - protected abstract Task InvokeEndpointHandlerAsync(HttpContext context, CancellationToken cancellationToken); + protected virtual Task ParseRequestAsync(HttpContext httpContext, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(httpContext); + + return Task.FromResult(default); + } + + protected abstract Task InvokeEndpointHandlerAsync(TRequest? request, CancellationToken cancellationToken); - protected virtual async Task WriteResponseAsync(TResult result, HttpContext context, CancellationToken cancellationToken) + protected virtual async Task WriteResponseAsync(TResponse response, HttpContext httpContext, CancellationToken cancellationToken) { - ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(httpContext); - if (Equals(result, null)) + if (Equals(response, null)) { return; } - context.Response.Headers.Append("Content-Type", ContentType); + httpContext.Response.Headers.Append("Content-Type", ContentType); JsonSerializerOptions options = ManagementOptionsMonitor.CurrentValue.SerializerOptions; - await JsonSerializer.SerializeAsync(context.Response.Body, result, options, cancellationToken); + await JsonSerializer.SerializeAsync(httpContext.Response.Body, response, options, cancellationToken); } } diff --git a/src/Management/src/Endpoint/PublicAPI.Unshipped.txt b/src/Management/src/Endpoint/PublicAPI.Unshipped.txt index 4755cfc90e..14042ef343 100755 --- a/src/Management/src/Endpoint/PublicAPI.Unshipped.txt +++ b/src/Management/src/Endpoint/PublicAPI.Unshipped.txt @@ -1,5 +1,5 @@ #nullable enable -abstract Steeltoe.Management.Endpoint.Middleware.EndpointMiddleware.InvokeEndpointHandlerAsync(Microsoft.AspNetCore.Http.HttpContext! context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +abstract Steeltoe.Management.Endpoint.Middleware.EndpointMiddleware.InvokeEndpointHandlerAsync(TRequest? request, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! const Steeltoe.Management.Endpoint.Actuators.Health.Availability.ApplicationAvailability.LivenessKey = "Liveness" -> string! const Steeltoe.Management.Endpoint.Actuators.Health.Availability.ApplicationAvailability.ReadinessKey = "Readiness" -> string! override Steeltoe.Management.Endpoint.Actuators.Health.Availability.AvailabilityState.ToString() -> string! @@ -452,12 +452,12 @@ Steeltoe.Management.Endpoint.IEndpointHandler.Options.get -> Steeltoe.Management.Endpoint.Middleware.ActuatorMetadataProvider Steeltoe.Management.Endpoint.Middleware.ActuatorMetadataProvider.ActuatorMetadataProvider(string! defaultContentType) -> void Steeltoe.Management.Endpoint.Middleware.ActuatorMetadataProvider.DefaultContentType.get -> string! -Steeltoe.Management.Endpoint.Middleware.EndpointMiddleware -Steeltoe.Management.Endpoint.Middleware.EndpointMiddleware.EndpointHandler.get -> Steeltoe.Management.Endpoint.IEndpointHandler! -Steeltoe.Management.Endpoint.Middleware.EndpointMiddleware.EndpointMiddleware(Steeltoe.Management.Endpoint.IEndpointHandler! endpointHandler, Microsoft.Extensions.Options.IOptionsMonitor! managementOptionsMonitor, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> void -Steeltoe.Management.Endpoint.Middleware.EndpointMiddleware.EndpointOptions.get -> Steeltoe.Management.Configuration.EndpointOptions! -Steeltoe.Management.Endpoint.Middleware.EndpointMiddleware.InvokeAsync(Microsoft.AspNetCore.Http.HttpContext! context, Microsoft.AspNetCore.Http.RequestDelegate? next) -> System.Threading.Tasks.Task! -Steeltoe.Management.Endpoint.Middleware.EndpointMiddleware.ManagementOptionsMonitor.get -> Microsoft.Extensions.Options.IOptionsMonitor! +Steeltoe.Management.Endpoint.Middleware.EndpointMiddleware +Steeltoe.Management.Endpoint.Middleware.EndpointMiddleware.EndpointHandler.get -> Steeltoe.Management.Endpoint.IEndpointHandler! +Steeltoe.Management.Endpoint.Middleware.EndpointMiddleware.EndpointMiddleware(Steeltoe.Management.Endpoint.IEndpointHandler! endpointHandler, Microsoft.Extensions.Options.IOptionsMonitor! managementOptionsMonitor, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> void +Steeltoe.Management.Endpoint.Middleware.EndpointMiddleware.EndpointOptions.get -> Steeltoe.Management.Configuration.EndpointOptions! +Steeltoe.Management.Endpoint.Middleware.EndpointMiddleware.InvokeAsync(Microsoft.AspNetCore.Http.HttpContext! context, Microsoft.AspNetCore.Http.RequestDelegate? next) -> System.Threading.Tasks.Task! +Steeltoe.Management.Endpoint.Middleware.EndpointMiddleware.ManagementOptionsMonitor.get -> Microsoft.Extensions.Options.IOptionsMonitor! Steeltoe.Management.Endpoint.Middleware.IEndpointMiddleware Steeltoe.Management.Endpoint.Middleware.IEndpointMiddleware.EndpointOptions.get -> Steeltoe.Management.Configuration.EndpointOptions! Steeltoe.Management.Endpoint.Middleware.IEndpointMiddleware.GetMetadataProvider() -> Steeltoe.Management.Endpoint.Middleware.ActuatorMetadataProvider! @@ -477,6 +477,7 @@ Steeltoe.Management.Endpoint.SpringBootAdminClient.SpringBootAdminClientOptions. Steeltoe.Management.Endpoint.SpringBootAdminClient.SpringBootAdminClientOptions.ValidateCertificates.set -> void virtual Steeltoe.Management.Endpoint.Configuration.ConfigureEndpointOptions.Configure(TOptions! options) -> void virtual Steeltoe.Management.Endpoint.Middleware.ActuatorMetadataProvider.GetMetadata(string! httpMethod) -> Microsoft.AspNetCore.Http.EndpointMetadataCollection! -virtual Steeltoe.Management.Endpoint.Middleware.EndpointMiddleware.CanInvoke(Microsoft.AspNetCore.Http.PathString requestPath) -> bool -virtual Steeltoe.Management.Endpoint.Middleware.EndpointMiddleware.GetMetadataProvider() -> Steeltoe.Management.Endpoint.Middleware.ActuatorMetadataProvider! -virtual Steeltoe.Management.Endpoint.Middleware.EndpointMiddleware.WriteResponseAsync(TResult result, Microsoft.AspNetCore.Http.HttpContext! context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +virtual Steeltoe.Management.Endpoint.Middleware.EndpointMiddleware.CanInvoke(Microsoft.AspNetCore.Http.PathString requestPath) -> bool +virtual Steeltoe.Management.Endpoint.Middleware.EndpointMiddleware.GetMetadataProvider() -> Steeltoe.Management.Endpoint.Middleware.ActuatorMetadataProvider! +virtual Steeltoe.Management.Endpoint.Middleware.EndpointMiddleware.ParseRequestAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +virtual Steeltoe.Management.Endpoint.Middleware.EndpointMiddleware.WriteResponseAsync(TResponse response, Microsoft.AspNetCore.Http.HttpContext! httpContext, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! diff --git a/src/Management/test/Endpoint.Test/SpringBootAdminClient/TestMiddleware.cs b/src/Management/test/Endpoint.Test/SpringBootAdminClient/TestMiddleware.cs index e05b805ae6..7be71c6961 100644 --- a/src/Management/test/Endpoint.Test/SpringBootAdminClient/TestMiddleware.cs +++ b/src/Management/test/Endpoint.Test/SpringBootAdminClient/TestMiddleware.cs @@ -22,7 +22,7 @@ public async Task InvokeAsync(HttpContext context, RequestDelegate? next) { if (context.Request.Path.Value?.EndsWith("instances", StringComparison.Ordinal) == true) { - var dictionary = await JsonSerializer.DeserializeAsync>(context.Request.Body); + var dictionary = await JsonSerializer.DeserializeAsync>(context.Request.Body, cancellationToken: context.RequestAborted); context.Response.Headers.Append("Content-Type", "application/json"); bool isValid = dictionary != null && KeyNames.All(dictionary.ContainsKey);