< 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@ca54e35c77799b76774b3805b6f075cdbc0c5fbe
Line coverage
72%
Covered lines: 57
Uncovered lines: 22
Coverable lines: 79
Total lines: 181
Line coverage: 72.1%
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 10/13/2025 - 16:52:37 Line coverage: 67.4% (56/83) Branch coverage: 90% (27/30) Total lines: 169 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e12/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@f59dcba478ea75f69584d696e5f1fb1cfa40aa51 10/13/2025 - 16:52:37 Line coverage: 67.4% (56/83) Branch coverage: 90% (27/30) Total lines: 169 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e12/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@f59dcba478ea75f69584d696e5f1fb1cfa40aa51

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.5%

File(s)

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

#LineLine coverage
 1using System.Management.Automation;
 2using Kestrun.Hosting;
 3using Kestrun.Hosting.Options;
 4using Kestrun.Languages;
 5using Kestrun.Models;
 6using Microsoft.AspNetCore.Diagnostics;
 7namespace Kestrun.Middleware;
 8
 9/// <summary>
 10/// Extension methods for adding status code pages middleware.
 11/// </summary>
 12public static class StatusCodePageExtensions
 13{
 14    /// <summary>
 15    /// Applies the configured status code pages middleware to the specified application builder.
 16    /// </summary>
 17    /// <param name="app">The application builder.</param>
 18    /// <param name="options">The status code options.</param>
 19    /// <returns>The application builder.</returns>
 20    public static IApplicationBuilder UseStatusCodePages(this IApplicationBuilder app, StatusCodeOptions options)
 21    {
 1022        ArgumentNullException.ThrowIfNull(options);
 23        // 1) direct delegate options
 924        if (TryUseDirectOptions(app, options) is { } direct)
 25        {
 226            return direct;
 27        }
 28
 29        // 2) redirects
 730        if (TryUseRedirects(app, options) is { } redirects)
 31        {
 232            return redirects;
 33        }
 34
 35        // 3) re-execute
 536        if (TryUseReExecute(app, options) is { } reexec)
 37        {
 238            return reexec;
 39        }
 40
 41        // 4) static body
 342        if (TryUseStaticBody(app, options) is { } staticBody)
 43        {
 144            return staticBody;
 45        }
 46
 47        // 5) custom script handler
 248        if (TryUseScriptHandler(app, options) is { } script)
 49        {
 150            return script;
 51        }
 52
 53        // default to built-in behavior
 154        return app.UseStatusCodePages();
 55    }
 56
 57    private static IApplicationBuilder? TryUseDirectOptions(IApplicationBuilder app, StatusCodeOptions options)
 958        => options.Options is not null ? app.UseStatusCodePages(options.Options) : null;
 59
 60    private static IApplicationBuilder? TryUseRedirects(IApplicationBuilder app, StatusCodeOptions options)
 761        => HasValue(options.LocationFormat) ? app.UseStatusCodePagesWithRedirects(options.LocationFormat!) : null;
 62
 63    private static IApplicationBuilder? TryUseReExecute(IApplicationBuilder app, StatusCodeOptions options)
 64    {
 565        if (!HasValue(options.PathFormat))
 66        {
 367            return null;
 68        }
 269        var query = NormalizeQuery(options.QueryFormat);
 270        return app.UseStatusCodePagesWithReExecute(options.PathFormat!, query);
 71    }
 72
 73    private static IApplicationBuilder? TryUseStaticBody(IApplicationBuilder app, StatusCodeOptions options)
 74    {
 375        if (!HasValue(options.ContentType) || !HasValue(options.BodyFormat))
 76        {
 277            return null;
 78        }
 179        var safeBody = EscapeTemplate(options.BodyFormat!);
 180        return app.UseStatusCodePages(options.ContentType!, safeBody);
 81    }
 82
 83    private static IApplicationBuilder? TryUseScriptHandler(IApplicationBuilder app, StatusCodeOptions options)
 84    {
 285        if (options.LanguageOptions is null)
 86        {
 187            return null;
 88        }
 189        var compiled = options.Host.CompileScript(options.LanguageOptions);
 190        var handler = BuildScriptHandler(options, compiled);
 191        return app.UseStatusCodePages(handler);
 92    }
 1693    private static bool HasValue(string? s) => !string.IsNullOrWhiteSpace(s);
 94
 95    /// <summary>
 96    /// Normalizes the query string to ensure it starts with '?' if not empty.
 97    /// </summary>
 98    /// <param name="query">The query string.</param>
 99    /// <returns>The normalized query string.</returns>
 100    private static string NormalizeQuery(string? query)
 101    {
 2102        if (query is null)
 103        {
 1104            return string.Empty;
 105        }
 106
 1107        var q = query.Trim();
 1108        return q.Length > 0 && q[0] == '?'
 1109            ? q
 1110            : "?" + q;
 111    }
 112
 113    /// <summary>
 114    /// Escape curly braces for String.Format safety, but preserve the {0} placeholder used for status code.
 115    /// </summary>
 116    /// <param name="bodyFormat">The body format string to escape.</param>
 117    /// <returns>The escaped body format string.</returns>
 118    /// <remarks>If the template already contains escaped braces ({{ or }}), assume it is pre-escaped and skip.</remarks
 119    private static string EscapeTemplate(string bodyFormat)
 120    {
 1121        if (bodyFormat.Contains("{{", StringComparison.Ordinal) || bodyFormat.Contains("}}", StringComparison.Ordinal))
 122        {
 0123            return bodyFormat; // already escaped
 124        }
 125
 1126        var escaped = bodyFormat.Replace("{", "{{").Replace("}", "}}");
 127        // restore the status-code placeholder
 1128        escaped = escaped.Replace("{{0}}", "{0}");
 1129        return escaped;
 130    }
 131
 132    /// <summary>
 133    /// Builds a status code handler that executes the compiled script delegate.
 134    /// </summary>
 135    /// <param name="options">The status code options.</param>
 136    /// <param name="compiled">The compiled request delegate.</param>
 137    /// <returns>A function that handles the status code context.</returns>
 138    private static Func<StatusCodeContext, Task> BuildScriptHandler(StatusCodeOptions options, RequestDelegate compiled)
 139    {
 1140        return async context =>
 1141        {
 0142            var httpContext = context.HttpContext;
 1143
 1144            // If running a PowerShell script but the runspace middleware did not execute
 1145            // (e.g., no matched endpoint so UseWhen predicate failed), bootstrap a temporary
 1146            // runspace and KestrunContext so the compiled delegate can run safely.
 0147            var needsBootstrap = options.LanguageOptions!.Language == Scripting.ScriptLanguage.PowerShell &&
 0148                                 !httpContext.Items.ContainsKey(PowerShellDelegateBuilder.PS_INSTANCE_KEY);
 1149
 0150            if (!needsBootstrap)
 1151            {
 0152                await compiled(httpContext);
 0153                return;
 1154            }
 1155
 0156            var pool = options.Host.RunspacePool; // throws if not initialized
 0157            var runspace = await pool.AcquireAsync(httpContext.RequestAborted);
 0158            using var ps = PowerShell.Create();
 0159            ps.Runspace = runspace;
 1160
 1161            // Build Kestrun abstractions and inject into context for PS delegate to use
 0162            var kr = new KestrunContext(pool.Host, httpContext);
 0163            httpContext.Items[PowerShellDelegateBuilder.PS_INSTANCE_KEY] = ps;
 0164            httpContext.Items[PowerShellDelegateBuilder.KR_CONTEXT_KEY] = kr;
 0165            var ss = ps.Runspace.SessionStateProxy;
 0166            ss.SetVariable("Context", kr);
 1167
 1168            try
 1169            {
 0170                await compiled(httpContext);
 0171            }
 1172            finally
 1173            {
 0174                pool.Release(ps.Runspace);
 0175                ps.Dispose();
 0176                _ = httpContext.Items.Remove(PowerShellDelegateBuilder.PS_INSTANCE_KEY);
 0177                _ = httpContext.Items.Remove(PowerShellDelegateBuilder.KR_CONTEXT_KEY);
 1178            }
 1179        };
 180    }
 181}