< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.Middleware.StatusCodePageExtensions
Assembly: Kestrun
File(s): /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Middleware/StatusCodePageExtensions.cs
Tag: Kestrun/Kestrun@5f1d2b981c9d7292c11fd448428c6ab6c811c5de
Line coverage
70%
Covered lines: 60
Uncovered lines: 25
Coverable lines: 85
Total lines: 188
Line coverage: 70.5%
Branch coverage
90%
Covered branches: 29
Total branches: 32
Branch coverage: 90.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 11/19/2025 - 17:40:50 Line coverage: 67.4% (56/83) Branch coverage: 90% (27/30) Total lines: 169 Tag: Kestrun/Kestrun@fcf33342333cef0516fe0d0912a86709874fd02612/21/2025 - 19:25:34 Line coverage: 68.2% (58/85) Branch coverage: 90.6% (29/32) Total lines: 181 Tag: Kestrun/Kestrun@63eee3e6ff7662a7eb5bb3603d667daccb809f2d01/02/2026 - 00:16:25 Line coverage: 72.1% (57/79) Branch coverage: 90.6% (29/32) Total lines: 175 Tag: Kestrun/Kestrun@8405dc23b786b9d436fba0d65fb80baa4171e1d001/24/2026 - 19:35:59 Line coverage: 72.1% (57/79) Branch coverage: 90.6% (29/32) Total lines: 181 Tag: Kestrun/Kestrun@f59dcba478ea75f69584d696e5f1fb1cfa40aa5103/27/2026 - 14:18:40 Line coverage: 70.5% (60/85) Branch coverage: 90.6% (29/32) Total lines: 188 Tag: Kestrun/Kestrun@63388ea9aed376ffbb41cd2727be2fb7646f6402 11/19/2025 - 17:40:50 Line coverage: 67.4% (56/83) Branch coverage: 90% (27/30) Total lines: 169 Tag: Kestrun/Kestrun@fcf33342333cef0516fe0d0912a86709874fd02612/21/2025 - 19:25:34 Line coverage: 68.2% (58/85) Branch coverage: 90.6% (29/32) Total lines: 181 Tag: Kestrun/Kestrun@63eee3e6ff7662a7eb5bb3603d667daccb809f2d01/02/2026 - 00:16:25 Line coverage: 72.1% (57/79) Branch coverage: 90.6% (29/32) Total lines: 175 Tag: Kestrun/Kestrun@8405dc23b786b9d436fba0d65fb80baa4171e1d001/24/2026 - 19:35:59 Line coverage: 72.1% (57/79) Branch coverage: 90.6% (29/32) Total lines: 181 Tag: Kestrun/Kestrun@f59dcba478ea75f69584d696e5f1fb1cfa40aa5103/27/2026 - 14:18:40 Line coverage: 70.5% (60/85) Branch coverage: 90.6% (29/32) Total lines: 188 Tag: Kestrun/Kestrun@63388ea9aed376ffbb41cd2727be2fb7646f6402

Coverage delta

Coverage delta 4 -4

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
UseStatusCodePages(...)100%1010100%
TryUseDirectOptions(...)100%22100%
TryUseRedirects(...)100%22100%
TryUseReExecute(...)100%22100%
TryUseStaticBody(...)100%44100%
TryUseScriptHandler(...)100%22100%
HasValue(...)100%11100%
NormalizeQuery(...)83.33%66100%
EscapeTemplate(...)50%4480%
BuildScriptHandler(...)100%1147.83%

File(s)

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Middleware/StatusCodePageExtensions.cs

#LineLine coverage
 1using System.Management.Automation;
 2using System.Management.Automation.Runspaces;
 3using Kestrun.Hosting;
 4using Kestrun.Hosting.Options;
 5using Kestrun.Languages;
 6using Kestrun.Models;
 7using Microsoft.AspNetCore.Diagnostics;
 8namespace Kestrun.Middleware;
 9
 10/// <summary>
 11/// Extension methods for adding status code pages middleware.
 12/// </summary>
 13public static class StatusCodePageExtensions
 14{
 15    /// <summary>
 16    /// Applies the configured status code pages middleware to the specified application builder.
 17    /// </summary>
 18    /// <param name="app">The application builder.</param>
 19    /// <param name="options">The status code options.</param>
 20    /// <returns>The application builder.</returns>
 21    public static IApplicationBuilder UseStatusCodePages(this IApplicationBuilder app, StatusCodeOptions options)
 22    {
 1023        ArgumentNullException.ThrowIfNull(options);
 24        // 1) direct delegate options
 925        if (TryUseDirectOptions(app, options) is { } direct)
 26        {
 227            return direct;
 28        }
 29
 30        // 2) redirects
 731        if (TryUseRedirects(app, options) is { } redirects)
 32        {
 233            return redirects;
 34        }
 35
 36        // 3) re-execute
 537        if (TryUseReExecute(app, options) is { } reexec)
 38        {
 239            return reexec;
 40        }
 41
 42        // 4) static body
 343        if (TryUseStaticBody(app, options) is { } staticBody)
 44        {
 145            return staticBody;
 46        }
 47
 48        // 5) custom script handler
 249        if (TryUseScriptHandler(app, options) is { } script)
 50        {
 151            return script;
 52        }
 53
 54        // default to built-in behavior
 155        return app.UseStatusCodePages();
 56    }
 57
 58    private static IApplicationBuilder? TryUseDirectOptions(IApplicationBuilder app, StatusCodeOptions options)
 959        => options.Options is not null ? app.UseStatusCodePages(options.Options) : null;
 60
 61    private static IApplicationBuilder? TryUseRedirects(IApplicationBuilder app, StatusCodeOptions options)
 762        => HasValue(options.LocationFormat) ? app.UseStatusCodePagesWithRedirects(options.LocationFormat!) : null;
 63
 64    private static IApplicationBuilder? TryUseReExecute(IApplicationBuilder app, StatusCodeOptions options)
 65    {
 566        if (!HasValue(options.PathFormat))
 67        {
 368            return null;
 69        }
 270        var query = NormalizeQuery(options.QueryFormat);
 271        return app.UseStatusCodePagesWithReExecute(options.PathFormat!, query);
 72    }
 73
 74    private static IApplicationBuilder? TryUseStaticBody(IApplicationBuilder app, StatusCodeOptions options)
 75    {
 376        if (!HasValue(options.ContentType) || !HasValue(options.BodyFormat))
 77        {
 278            return null;
 79        }
 180        var safeBody = EscapeTemplate(options.BodyFormat!);
 181        return app.UseStatusCodePages(options.ContentType!, safeBody);
 82    }
 83
 84    private static IApplicationBuilder? TryUseScriptHandler(IApplicationBuilder app, StatusCodeOptions options)
 85    {
 286        if (options.LanguageOptions is null)
 87        {
 188            return null;
 89        }
 190        var compiled = options.Host.CompileScript(options.LanguageOptions);
 191        var handler = BuildScriptHandler(options, compiled);
 192        return app.UseStatusCodePages(handler);
 93    }
 1694    private static bool HasValue(string? s) => !string.IsNullOrWhiteSpace(s);
 95
 96    /// <summary>
 97    /// Normalizes the query string to ensure it starts with '?' if not empty.
 98    /// </summary>
 99    /// <param name="query">The query string.</param>
 100    /// <returns>The normalized query string.</returns>
 101    private static string NormalizeQuery(string? query)
 102    {
 2103        if (query is null)
 104        {
 1105            return string.Empty;
 106        }
 107
 1108        var q = query.Trim();
 1109        return q.Length > 0 && q[0] == '?'
 1110            ? q
 1111            : "?" + q;
 112    }
 113
 114    /// <summary>
 115    /// Escape curly braces for String.Format safety, but preserve the {0} placeholder used for status code.
 116    /// </summary>
 117    /// <param name="bodyFormat">The body format string to escape.</param>
 118    /// <returns>The escaped body format string.</returns>
 119    /// <remarks>If the template already contains escaped braces ({{ or }}), assume it is pre-escaped and skip.</remarks
 120    private static string EscapeTemplate(string bodyFormat)
 121    {
 1122        if (bodyFormat.Contains("{{", StringComparison.Ordinal) || bodyFormat.Contains("}}", StringComparison.Ordinal))
 123        {
 0124            return bodyFormat; // already escaped
 125        }
 126
 1127        var escaped = bodyFormat.Replace("{", "{{").Replace("}", "}}");
 128        // restore the status-code placeholder
 1129        escaped = escaped.Replace("{{0}}", "{0}");
 1130        return escaped;
 131    }
 132
 133    /// <summary>
 134    /// Builds a status code handler that executes the compiled script delegate.
 135    /// </summary>
 136    /// <param name="options">The status code options.</param>
 137    /// <param name="compiled">The compiled request delegate.</param>
 138    /// <returns>A function that handles the status code context.</returns>
 139    private static Func<StatusCodeContext, Task> BuildScriptHandler(StatusCodeOptions options, RequestDelegate compiled)
 140    {
 1141        return async context =>
 1142        {
 0143            var httpContext = context.HttpContext;
 1144
 1145            // If running a PowerShell script but the runspace middleware did not execute
 1146            // (e.g., no matched endpoint so UseWhen predicate failed), bootstrap a temporary
 1147            // runspace and KestrunContext so the compiled delegate can run safely.
 0148            var needsBootstrap = options.LanguageOptions!.Language == Scripting.ScriptLanguage.PowerShell &&
 0149                                 !httpContext.Items.ContainsKey(PowerShellDelegateBuilder.PS_INSTANCE_KEY);
 1150
 0151            if (!needsBootstrap)
 1152            {
 0153                await compiled(httpContext);
 0154                return;
 1155            }
 1156
 0157            var pool = options.Host.RunspacePool; // throws if not initialized
 0158            Runspace? runspace = null;
 0159            PowerShell? ps = null;
 1160
 1161            try
 1162            {
 0163                runspace = await pool.AcquireAsync(httpContext.RequestAborted);
 0164                ps = PowerShell.Create();
 0165                ps.Runspace = runspace;
 1166
 1167                // Build Kestrun abstractions and inject into context for PS delegate to use
 0168                var kr = new KestrunContext(pool.Host, httpContext);
 0169                httpContext.Items[PowerShellDelegateBuilder.PS_INSTANCE_KEY] = ps;
 0170                httpContext.Items[PowerShellDelegateBuilder.KR_CONTEXT_KEY] = kr;
 0171                var ss = ps.Runspace.SessionStateProxy;
 0172                ss.SetVariable("Context", kr);
 1173
 0174                await compiled(httpContext);
 0175            }
 1176            finally
 1177            {
 0178                ps?.Dispose();
 0179                if (runspace is not null)
 1180                {
 0181                    pool.Release(runspace);
 1182                }
 0183                _ = httpContext.Items.Remove(PowerShellDelegateBuilder.PS_INSTANCE_KEY);
 0184                _ = httpContext.Items.Remove(PowerShellDelegateBuilder.KR_CONTEXT_KEY);
 1185            }
 1186        };
 187    }
 188}