< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.Languages.PowerShellDelegateBuilder
Assembly: Kestrun
File(s): /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Languages/PowerShellDelegateBuilder.cs
Tag: Kestrun/Kestrun@eeafbe813231ed23417e7b339e170e307b2c86f9
Line coverage
42%
Covered lines: 100
Uncovered lines: 135
Coverable lines: 235
Total lines: 533
Line coverage: 42.5%
Branch coverage
47%
Covered branches: 58
Total branches: 122
Branch coverage: 47.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 09/12/2025 - 13:32:05 Line coverage: 80.5% (91/113) Branch coverage: 63.8% (23/36) Total lines: 194 Tag: Kestrun/Kestrun@63ea5841fe73fd164406accba17a956e8c08357f10/13/2025 - 16:52:37 Line coverage: 87.5% (91/104) Branch coverage: 70% (21/30) Total lines: 194 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e11/14/2025 - 12:29:34 Line coverage: 87.6% (92/105) Branch coverage: 70% (21/30) Total lines: 196 Tag: Kestrun/Kestrun@5e12b09a6838e68e704cd3dc975331b9e680a62611/19/2025 - 02:25:56 Line coverage: 87.7% (93/106) Branch coverage: 70% (21/30) Total lines: 196 Tag: Kestrun/Kestrun@98ff905e5605a920343154665980a71211a03c6d12/12/2025 - 17:27:19 Line coverage: 86.5% (103/119) Branch coverage: 70% (21/30) Total lines: 208 Tag: Kestrun/Kestrun@826bf9dcf9db118c5de4c78a3259bce9549f0dcd01/12/2026 - 18:03:06 Line coverage: 85.4% (112/131) Branch coverage: 70% (21/30) Total lines: 220 Tag: Kestrun/Kestrun@956332ccc921363590dccd99d5707fb20b50966b01/17/2026 - 04:33:35 Line coverage: 80.4% (115/143) Branch coverage: 70% (21/30) Total lines: 232 Tag: Kestrun/Kestrun@aca34ea8d284564e2f9f6616dc937668dce926ba01/21/2026 - 17:07:46 Line coverage: 67.4% (58/86) Branch coverage: 53.4% (31/58) Total lines: 246 Tag: Kestrun/Kestrun@3f6f61710c7ef7d5953cab578fe699c1e5e01a3601/24/2026 - 19:35:59 Line coverage: 67% (59/88) Branch coverage: 53.3% (32/60) Total lines: 250 Tag: Kestrun/Kestrun@f59dcba478ea75f69584d696e5f1fb1cfa40aa5102/05/2026 - 00:28:18 Line coverage: 61.2% (60/98) Branch coverage: 48.6% (35/72) Total lines: 277 Tag: Kestrun/Kestrun@d9261bd752e45afa789d10bc0c82b7d5724d958902/18/2026 - 08:33:07 Line coverage: 43% (99/230) Branch coverage: 47.4% (56/118) Total lines: 523 Tag: Kestrun/Kestrun@bf8a937cfb7e8936c225b9df4608f8ddd85558b103/03/2026 - 20:14:57 Line coverage: 42.5% (100/235) Branch coverage: 47.5% (58/122) Total lines: 533 Tag: Kestrun/Kestrun@d169bd1d1e32ec576bfd2135357b6d01a2ad23ba 09/12/2025 - 13:32:05 Line coverage: 80.5% (91/113) Branch coverage: 63.8% (23/36) Total lines: 194 Tag: Kestrun/Kestrun@63ea5841fe73fd164406accba17a956e8c08357f10/13/2025 - 16:52:37 Line coverage: 87.5% (91/104) Branch coverage: 70% (21/30) Total lines: 194 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e11/14/2025 - 12:29:34 Line coverage: 87.6% (92/105) Branch coverage: 70% (21/30) Total lines: 196 Tag: Kestrun/Kestrun@5e12b09a6838e68e704cd3dc975331b9e680a62611/19/2025 - 02:25:56 Line coverage: 87.7% (93/106) Branch coverage: 70% (21/30) Total lines: 196 Tag: Kestrun/Kestrun@98ff905e5605a920343154665980a71211a03c6d12/12/2025 - 17:27:19 Line coverage: 86.5% (103/119) Branch coverage: 70% (21/30) Total lines: 208 Tag: Kestrun/Kestrun@826bf9dcf9db118c5de4c78a3259bce9549f0dcd01/12/2026 - 18:03:06 Line coverage: 85.4% (112/131) Branch coverage: 70% (21/30) Total lines: 220 Tag: Kestrun/Kestrun@956332ccc921363590dccd99d5707fb20b50966b01/17/2026 - 04:33:35 Line coverage: 80.4% (115/143) Branch coverage: 70% (21/30) Total lines: 232 Tag: Kestrun/Kestrun@aca34ea8d284564e2f9f6616dc937668dce926ba01/21/2026 - 17:07:46 Line coverage: 67.4% (58/86) Branch coverage: 53.4% (31/58) Total lines: 246 Tag: Kestrun/Kestrun@3f6f61710c7ef7d5953cab578fe699c1e5e01a3601/24/2026 - 19:35:59 Line coverage: 67% (59/88) Branch coverage: 53.3% (32/60) Total lines: 250 Tag: Kestrun/Kestrun@f59dcba478ea75f69584d696e5f1fb1cfa40aa5102/05/2026 - 00:28:18 Line coverage: 61.2% (60/98) Branch coverage: 48.6% (35/72) Total lines: 277 Tag: Kestrun/Kestrun@d9261bd752e45afa789d10bc0c82b7d5724d958902/18/2026 - 08:33:07 Line coverage: 43% (99/230) Branch coverage: 47.4% (56/118) Total lines: 523 Tag: Kestrun/Kestrun@bf8a937cfb7e8936c225b9df4608f8ddd85558b103/03/2026 - 20:14:57 Line coverage: 42.5% (100/235) Branch coverage: 47.5% (58/122) Total lines: 533 Tag: Kestrun/Kestrun@d169bd1d1e32ec576bfd2135357b6d01a2ad23ba

Coverage delta

Coverage delta 19 -19

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
Build(...)100%22100%
ExecutePowerShellRequestAsync()36.84%24697625.45%
WriteErrorResponseWithCustomHandlerAsync()100%22100%
TryExecuteCustomErrorResponseScriptAsync()50%111082.6%
GetPowerShellFromContext(...)66.66%66100%
GetKestrunContext(...)50%22100%
LogTopResults(...)66.66%7675%
HandleErrorsIfAnyAsync()75%12860%
LogSideChannelMessagesIfAny(...)50%12860%
HandleRedirectIfAny(...)100%22100%
ApplyResponseAsync(...)100%11100%

File(s)

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Languages/PowerShellDelegateBuilder.cs

#LineLine coverage
 1using System.Management.Automation;
 2using System.Net.Http.Headers;
 3using Kestrun.Hosting;
 4using Kestrun.KrException;
 5using Kestrun.Logging;
 6using Kestrun.Models;
 7using Kestrun.Utilities;
 8using Serilog.Events;
 9
 10namespace Kestrun.Languages;
 11
 12internal static class PowerShellDelegateBuilder
 13{
 14    public const string PS_INSTANCE_KEY = "PS_INSTANCE";
 15    public const string KR_CONTEXT_KEY = "KR_CONTEXT";
 16    internal static RequestDelegate Build(KestrunHost host, string code, Dictionary<string, object?>? arguments)
 17    {
 818        var log = host.Logger;
 819        ArgumentNullException.ThrowIfNull(code);
 820        if (log.IsEnabled(LogEventLevel.Debug))
 21        {
 622            log.Debug("Building PowerShell delegate, script length={Length}", code.Length);
 23        }
 24
 1525        return context => ExecutePowerShellRequestAsync(context, log, code, arguments);
 26    }
 27
 28    /// <summary>
 29    /// Executes the PowerShell request pipeline and applies the resulting response.
 30    /// </summary>
 31    /// <param name="context">Current HTTP context.</param>
 32    /// <param name="log">Logger instance.</param>
 33    /// <param name="code">PowerShell script code.</param>
 34    /// <param name="arguments">Arguments to inject as variables into the script.</param>
 35    private static async Task ExecutePowerShellRequestAsync(
 36        HttpContext context,
 37        Serilog.ILogger log,
 38        string code,
 39        Dictionary<string, object?>? arguments)
 40    {
 741        var isLogVerbose = log.IsEnabled(LogEventLevel.Verbose);
 42        // Log invocation
 743        if (log.IsEnabled(LogEventLevel.Debug))
 44        {
 545            log.DebugSanitized("PS delegate invoked for {Path}", context.Request.Path);
 46        }
 47
 48        // Prepare for execution
 749        KestrunContext? krContext = null;
 50        // Get the PowerShell instance from the context (set by middleware)
 751        var ps = GetPowerShellFromContext(context, log);
 52
 53        // Ensure the runspace pool is open before executing the script
 54        try
 55        {
 656            PowerShellExecutionHelpers.SetVariables(ps, arguments, log);
 657            if (isLogVerbose)
 58            {
 059                log.Verbose("Setting PowerShell variables for Request and Response in the runspace.");
 60            }
 661            krContext = GetKestrunContext(context);
 62
 663            var allowed = krContext.MapRouteOptions.AllowedRequestContentTypes;
 64
 665            if (allowed is { Count: > 0 })
 66            {
 67                // Reliable body detection
 068                var hasBody =
 069                    (context.Request.ContentLength.HasValue && context.Request.ContentLength.Value > 0) ||
 070                    context.Request.Headers.TransferEncoding.Count > 0;
 71
 072                if (string.IsNullOrWhiteSpace(context.Request.ContentType))
 73                {
 074                    if (hasBody)
 75                    {
 076                        var message =
 077                            "Content-Type header is required. Supported types: " + string.Join(", ", allowed);
 78
 079                        log.Warning(
 080                            "Request with missing Content-Type header is not allowed. {Message}",
 081                            message);
 82
 083                        await WriteErrorResponseWithCustomHandlerAsync(
 084                            context,
 085                            krContext,
 086                            ps,
 087                            log,
 088                            message,
 089                            StatusCodes.Status415UnsupportedMediaType).ConfigureAwait(false);
 090                        return;
 91                    }
 92                }
 93                else
 94                {
 095                    if (!MediaTypeHeaderValue.TryParse(context.Request.ContentType, out var mediaType))
 96                    {
 97                        // Malformed Content-Type → 400 (syntax error, not support issue)
 098                        var message = $"Invalid Content-Type header value '{context.Request.ContentType}'.";
 99
 0100                        log.WarningSanitized(
 0101                            "Malformed Content-Type header '{ContentType}'.",
 0102                            context.Request.ContentType);
 103
 0104                        await WriteErrorResponseWithCustomHandlerAsync(
 0105                            context,
 0106                            krContext,
 0107                            ps,
 0108                            log,
 0109                            message,
 0110                            StatusCodes.Status400BadRequest).ConfigureAwait(false);
 0111                        return;
 112                    }
 113                    // Canonicalize the request content type and check against allowed list
 0114                    var requestContentTypeRaw = mediaType.MediaType ?? string.Empty;
 115                    // Canonicalize the request content type and check against allowed list
 0116                    var requestContentTypeCanonical = MediaTypeHelper.Canonicalize(requestContentTypeRaw);
 117                    // Check both raw and canonical forms against the allowed list to allow for flexible matching
 0118                    var rawAllowed = allowed.Contains(requestContentTypeRaw, StringComparer.OrdinalIgnoreCase);
 119                    // Canonicalize the request content type and check against allowed list
 0120                    var canonicalAllowed = allowed
 0121                        .Select(MediaTypeHelper.Canonicalize)
 0122                        .Contains(requestContentTypeCanonical, StringComparer.OrdinalIgnoreCase);
 123                    // If neither the raw nor canonical content type is allowed, return 415 Unsupported Media Type
 0124                    if (!rawAllowed && !canonicalAllowed)
 125                    {
 0126                        var message =
 0127                            $"Request content type '{requestContentTypeRaw}' is not allowed. Supported types: {string.Jo
 128
 0129                        log.Warning(
 0130                            "Request content type '{ContentType}' (canonical '{Canonical}') is not allowed for this rout
 0131                            requestContentTypeRaw,
 0132                            requestContentTypeCanonical);
 133
 0134                        await WriteErrorResponseWithCustomHandlerAsync(
 0135                            context,
 0136                            krContext,
 0137                            ps,
 0138                            log,
 0139                            message,
 0140                            StatusCodes.Status415UnsupportedMediaType).ConfigureAwait(false);
 0141                        return;
 142                    }
 143                }
 144            }
 145
 6146            if (krContext.HasRequestCulture)
 147            {
 0148                PowerShellExecutionHelpers.AddCulturePrelude(ps, krContext.Culture, log);
 149            }
 6150            PowerShellExecutionHelpers.AddScript(ps, code);
 151
 152            // Extract and add parameters for injection
 6153            ParameterForInjectionInfo.InjectParameters(krContext, ps);
 154
 155            // Execute the script
 6156            if (isLogVerbose)
 157            {
 0158                log.Verbose("Invoking PowerShell script...");
 159            }
 6160            var psResults = await ps.InvokeAsync(log, context.RequestAborted).ConfigureAwait(false);
 4161            LogTopResults(log, psResults);
 162
 4163            if (await HandleErrorsIfAnyAsync(context, krContext, ps, log).ConfigureAwait(false))
 164            {
 1165                return;
 166            }
 167
 3168            LogSideChannelMessagesIfAny(log, ps);
 169
 3170            if (HandleRedirectIfAny(context, krContext, log))
 171            {
 1172                return;
 173            }
 174
 175            // Some endpoints (e.g., SSE streaming) write directly to the HttpResponse and
 176            // intentionally start the response early. In that case, applying KestrunResponse
 177            // would attempt to set headers/status again and throw.
 2178            if (context.Response.HasStarted)
 179            {
 0180                if (isLogVerbose)
 181                {
 0182                    log.Verbose("HttpResponse has already started; skipping KestrunResponse.ApplyTo().");
 183                }
 0184                return;
 185            }
 2186            if (isLogVerbose)
 187            {
 0188                log.Verbose("No redirect detected; applying response to HttpResponse...");
 189            }
 190
 2191            var postponed = krContext.Response.PostPonedWriteObject;
 2192            if (postponed.Error is int postponedError && postponedError != 0)
 193            {
 0194                log.Error("Postponed response contains error code {ErrorCode}; throwing before response write.", postpon
 0195                throw new InvalidOperationException($"Postponed response error detected: {postponedError}");
 196            }
 197
 2198            if (krContext.Response.HasPostPonedWriteObject)
 199            {
 0200                if (isLogVerbose)
 201                {
 0202                    log.Verbose("Postponed Write-KrResponse detected; applying response with Write-KrResponse.");
 203                }
 0204                await krContext.Response.WriteResponseAsync(postponed).ConfigureAwait(false);
 205            }
 2206        }
 207        // optional: catch client cancellation to avoid noisy logs
 0208        catch (OperationCanceledException) when (context.RequestAborted.IsCancellationRequested)
 209        {
 210            // client disconnected – nothing to send
 0211        }
 0212        catch (ParameterForInjectionException pfiiex)
 213        {
 214            // Log parameter resolution errors with preview of code
 215            //   log.Error("Parameter resolution error ({Message}) - {Preview}",
 216            // pfiiex.Message, code[..Math.Min(40, code.Length)]);
 0217            if (krContext is not null)
 218            {
 219                // Return 400 Bad Request for parameter resolution errors
 0220                await WriteErrorResponseWithCustomHandlerAsync(
 0221                    context,
 0222                    krContext,
 0223                    ps,
 0224                    log,
 0225                    "Invalid request parameters: " + pfiiex.Message,
 0226                    pfiiex.StatusCode,
 0227                    pfiiex).ConfigureAwait(false);
 228            }
 229            else
 230            {
 0231                throw;
 232            }
 0233        }
 0234        catch (ParameterBindingException pbaex)
 235        {
 0236            var fqid = pbaex.ErrorRecord?.FullyQualifiedErrorId;
 0237            var cat = pbaex.ErrorRecord?.CategoryInfo?.Category;
 0238            if (krContext is not null)
 239            {
 240                // Return 400 Bad Request for parameter binding errors
 0241                await WriteErrorResponseWithCustomHandlerAsync(
 0242                    context,
 0243                    krContext,
 0244                    ps,
 0245                    log,
 0246                    "Invalid request parameters: " + pbaex.Message,
 0247                    StatusCodes.Status400BadRequest,
 0248                    pbaex).ConfigureAwait(false);
 249            }
 250            else
 251            {
 0252                throw;
 253            }
 0254        }
 0255        catch (Forms.KrFormException kfex)
 256        {
 257            // Log form parsing errors with preview of code
 0258            log.Error("Form parsing error ({Message}) - {Preview}",
 0259                kfex.Message, code[..Math.Min(40, code.Length)]);
 0260            if (krContext is not null)
 261            {
 262                // Return 400 Bad Request for form parsing errors
 0263                await WriteErrorResponseWithCustomHandlerAsync(
 0264                    context,
 0265                    krContext,
 0266                    ps,
 0267                    log,
 0268                    "Invalid form data: " + kfex.Message,
 0269                    kfex.StatusCode,
 0270                    kfex).ConfigureAwait(false);
 271            }
 0272            else { throw; }
 0273        }
 0274        catch (InvalidOperationException ioex) when (ioex.Message.StartsWith("Postponed response error detected:", Strin
 275        {
 0276            log.Error(ioex, "Postponed response error detected while applying Write-KrResponse result.");
 0277            if (krContext is not null)
 278            {
 0279                await WriteErrorResponseWithCustomHandlerAsync(
 0280                    context,
 0281                    krContext,
 0282                    ps,
 0283                    log,
 0284                    "An internal server error occurred.",
 0285                    StatusCodes.Status500InternalServerError,
 0286                    ioex).ConfigureAwait(false);
 287            }
 288            else
 289            {
 0290                throw;
 291            }
 0292        }
 2293        catch (Exception ex)
 294        {
 295            // If we have exception options, set a 500 status code and generic message.
 296            // Otherwise rethrow to let higher-level middleware handle it (e.g., Developer Exception Page
 2297            if (krContext?.Host?.ExceptionOptions is null)
 298            { // Log and handle script errors
 2299                log.Error(ex, "PowerShell script failed - {Preview}", code[..Math.Min(40, code.Length)]);
 2300                if (krContext is not null)
 301                {
 2302                    await WriteErrorResponseWithCustomHandlerAsync(
 2303                        context,
 2304                        krContext,
 2305                        ps,
 2306                        log,
 2307                        "An internal server error occurred.",
 2308                        StatusCodes.Status500InternalServerError,
 2309                        ex).ConfigureAwait(false);
 310                }
 311                else
 312                {
 0313                    context.Response.StatusCode = 500; // Internal Server Error
 0314                    context.Response.ContentType = "text/plain; charset=utf-8";
 0315                    await context.Response.WriteAsync("An error occurred while processing your request.");
 316                }
 317            }
 318            else
 319            {
 320                // re-throw to let higher-level middleware handle it (e.g., Developer Exception Page)
 0321                throw;
 322            }
 323        }
 324        finally
 325        {
 6326            if (krContext is not null)
 327            {
 6328                await ApplyResponseAsync(context, krContext).ConfigureAwait(false);
 329            }
 330            // Do not call Response.CompleteAsync here; leaving the response open allows
 331            // downstream middleware like StatusCodePages to generate a body for status-only responses.
 332        }
 6333    }
 334
 335    /// <summary>
 336    /// Writes an error response using a custom PowerShell handler when configured; otherwise falls back to the built-in
 337    /// </summary>
 338    /// <param name="context">Current HTTP context.</param>
 339    /// <param name="krContext">Current Kestrun context.</param>
 340    /// <param name="ps">PowerShell instance bound to the request runspace.</param>
 341    /// <param name="log">Logger instance.</param>
 342    /// <param name="message">Error message to expose to the handler/fallback writer.</param>
 343    /// <param name="statusCode">HTTP status code for the error.</param>
 344    /// <param name="exception">Optional exception that triggered the error flow.</param>
 345    private static async Task WriteErrorResponseWithCustomHandlerAsync(
 346        HttpContext context,
 347        KestrunContext krContext,
 348        PowerShell ps,
 349        Serilog.ILogger log,
 350        string message,
 351        int statusCode,
 352        Exception? exception = null)
 353    {
 2354        if (!await TryExecuteCustomErrorResponseScriptAsync(context, krContext, ps, log, message, statusCode, exception)
 355        {
 1356            await krContext.Response.WriteErrorResponseAsync(message, statusCode).ConfigureAwait(false);
 357        }
 2358    }
 359
 360    /// <summary>
 361    /// Attempts to execute a configured custom PowerShell error response script for the current request.
 362    /// </summary>
 363    /// <param name="context">Current HTTP context.</param>
 364    /// <param name="krContext">Current Kestrun context.</param>
 365    /// <param name="ps">PowerShell instance bound to the request runspace.</param>
 366    /// <param name="log">Logger instance.</param>
 367    /// <param name="message">Error message to expose to the script.</param>
 368    /// <param name="statusCode">HTTP status code to expose to the script.</param>
 369    /// <param name="exception">Optional exception to expose to the script.</param>
 370    /// <returns>True when a custom script executed successfully; otherwise false.</returns>
 371    private static async Task<bool> TryExecuteCustomErrorResponseScriptAsync(
 372        HttpContext context,
 373        KestrunContext krContext,
 374        PowerShell ps,
 375        Serilog.ILogger log,
 376        string message,
 377        int statusCode,
 378        Exception? exception)
 379    {
 2380        var script = krContext.Host.PowerShellErrorResponseScript;
 2381        if (string.IsNullOrWhiteSpace(script) || ps.Runspace is null || context.Response.HasStarted)
 382        {
 0383            return false;
 384        }
 385
 386        try
 387        {
 2388            using var customErrorPs = PowerShell.Create();
 2389            customErrorPs.Runspace = ps.Runspace;
 390
 2391            var sessionState = customErrorPs.Runspace.SessionStateProxy;
 2392            sessionState.SetVariable("Context", krContext);
 2393            sessionState.SetVariable("KrContext", krContext);
 2394            sessionState.SetVariable("StatusCode", statusCode);
 2395            sessionState.SetVariable("ErrorMessage", message);
 2396            sessionState.SetVariable("Exception", exception);
 397
 2398            if (krContext.HasRequestCulture)
 399            {
 0400                PowerShellExecutionHelpers.AddCulturePrelude(customErrorPs, krContext.Culture, log);
 401            }
 402
 2403            PowerShellExecutionHelpers.AddScript(customErrorPs, script);
 2404            _ = await customErrorPs.InvokeAsync(log, context.RequestAborted).ConfigureAwait(false);
 405
 1406            if (customErrorPs.Streams.Error.Count > 0)
 407            {
 0408                log.Warning("Custom PowerShell error response script reported errors; falling back to default error resp
 0409                return false;
 410            }
 411
 1412            return true;
 413        }
 1414        catch (Exception ex)
 415        {
 1416            log.Error(ex, "Custom PowerShell error response script failed; falling back to default error response.");
 1417            return false;
 418        }
 2419    }
 420
 421    /// <summary>
 422    /// Retrieves the PowerShell instance from the HttpContext items.
 423    /// </summary>
 424    /// <param name="context">The HttpContext from which to retrieve the PowerShell instance.</param>
 425    /// <param name="log">The logger to use for logging.</param>
 426    /// <returns>The PowerShell instance associated with the current request.</returns>
 427    /// <exception cref="InvalidOperationException">Thrown if the PowerShell instance is not found in the context items.
 428    private static PowerShell GetPowerShellFromContext(HttpContext context, Serilog.ILogger log)
 429    {
 7430        if (!context.Items.ContainsKey(PS_INSTANCE_KEY))
 431        {
 1432            throw new InvalidOperationException("PowerShell runspace not found in context items. Ensure PowerShellRunspa
 433        }
 434
 6435        log.Verbose("Retrieving PowerShell instance from context items.");
 6436        var ps = context.Items[PS_INSTANCE_KEY] as PowerShell
 6437                 ?? throw new InvalidOperationException("PowerShell instance not found in context items.");
 6438        return ps.Runspace == null
 6439            ? throw new InvalidOperationException("PowerShell runspace is not set. Ensure PowerShellRunspaceMiddleware i
 6440            : ps;
 441    }
 442
 443    /// <summary>
 444    /// Retrieves the KestrunContext from the HttpContext items.
 445    /// </summary>
 446    /// <param name="context">The HttpContext from which to retrieve the KestrunContext.</param>
 447    /// <returns>The KestrunContext associated with the current request.</returns>
 448    /// <exception cref="InvalidOperationException">Thrown if the KestrunContext is not found in the context items.</exc
 449    private static KestrunContext GetKestrunContext(HttpContext context)
 6450        => context.Items[KR_CONTEXT_KEY] as KestrunContext
 6451           ?? throw new InvalidOperationException($"{KR_CONTEXT_KEY} key not found in context items.");
 452
 453    ///<summary>
 454    /// Logs the top results from the PowerShell script output for debugging purposes.
 455    /// Only logs if the log level is set to Debug.
 456    /// </summary>
 457    /// <param name="log">The logger to use for logging.</param>
 458    /// <param name="psResults">The collection of PSObject results from the PowerShell script.</param>
 459    private static void LogTopResults(Serilog.ILogger log, PSDataCollection<PSObject> psResults)
 460    {
 4461        if (!log.IsEnabled(LogEventLevel.Debug))
 462        {
 2463            return;
 464        }
 465
 2466        log.Debug("PowerShell script output:");
 4467        foreach (var r in psResults.Take(10))
 468        {
 0469            log.Debug("   • {Result}", r);
 470        }
 2471        if (psResults.Count > 10)
 472        {
 0473            log.Debug("   … {Count} more", psResults.Count - 10);
 474        }
 2475    }
 476
 477    /// <summary>
 478    /// Handles any errors that occurred during the PowerShell script execution.
 479    /// </summary>
 480    /// <param name="context">The HttpContext for the current request.</param>
 481    /// <param name="krContext">The Kestrun context for the current request.</param>
 482    /// <param name="ps">The PowerShell instance used for script execution.</param>
 483    /// <param name="log">The logger used for diagnostic messages.</param>
 484    /// <returns>True if errors were handled, false otherwise.</returns>
 485    private static async Task<bool> HandleErrorsIfAnyAsync(HttpContext context, KestrunContext krContext, PowerShell ps,
 486    {
 4487        if (ps.HadErrors || ps.Streams.Error.Count != 0)
 488        {
 1489            if (context.Response.HasStarted || krContext.Response.StatusCode >= StatusCodes.Status400BadRequest)
 490            {
 0491                log.Debug(
 0492                    "PowerShell error stream contains records but response is already set (StatusCode={StatusCode}); ski
 0493                    krContext.Response.StatusCode);
 0494                return false;
 495            }
 496
 1497            await BuildError.ResponseAsync(context, ps).ConfigureAwait(false);
 1498            return true;
 499        }
 3500        return false;
 4501    }
 502
 503    /// <summary>
 504    /// Logs any side-channel messages (Verbose, Debug, Warning, Information) produced by the PowerShell script.
 505    /// </summary>
 506    /// <param name="log">The logger to use for logging.</param>
 507    /// <param name="ps">The PowerShell instance used to invoke the script.</param>
 508    private static void LogSideChannelMessagesIfAny(Serilog.ILogger log, PowerShell ps)
 509    {
 3510        if (ps.Streams.Verbose.Count > 0 || ps.Streams.Debug.Count > 0 || ps.Streams.Warning.Count > 0 || ps.Streams.Inf
 511        {
 0512            log.Verbose("PowerShell script completed with verbose/debug/warning/info messages.");
 0513            log.Verbose(BuildError.Text(ps));
 514        }
 3515        log.Verbose("PowerShell script completed successfully.");
 3516    }
 517
 518    private static bool HandleRedirectIfAny(HttpContext context, KestrunContext krContext, Serilog.ILogger log)
 519    {
 3520        if (!string.IsNullOrEmpty(krContext.Response.RedirectUrl))
 521        {
 1522            log.Verbose($"Redirecting to {krContext.Response.RedirectUrl}");
 1523            context.Response.Redirect(krContext.Response.RedirectUrl);
 1524            return true;
 525        }
 2526        return false;
 527    }
 528
 529    private static Task ApplyResponseAsync(HttpContext context, KestrunContext krContext)
 6530        => krContext.Response.ApplyTo(context.Response);
 531
 532    // Removed explicit Response.CompleteAsync to allow StatusCodePages to run after endpoints when appropriate.
 533}