< 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@2d87023b37eb91155071c91dd3d6a2eeb3004705
Line coverage
67%
Covered lines: 56
Uncovered lines: 27
Coverable lines: 83
Total lines: 169
Line coverage: 67.4%
Branch coverage
90%
Covered branches: 27
Total branches: 30
Branch coverage: 90%
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@10d476bee71c71ad215bb8ab59f219887b5b4a5e 10/13/2025 - 16:52:37 Line coverage: 67.4% (56/83) Branch coverage: 90% (27/30) Total lines: 169 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e

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(...)75%44100%
EscapeTemplate(...)50%4480%
BuildScriptHandler(...)100%1143.47%

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, options.Host.Logger);
 190        var handler = BuildScriptHandler(options, compiled);
 191        return app.UseStatusCodePages(handler);
 92    }
 1893    private static bool HasValue(string? s) => !string.IsNullOrWhiteSpace(s);
 94
 95    private static string? NormalizeQuery(string? query)
 96    {
 297        if (!HasValue(query))
 98        {
 199            return query;
 100        }
 1101        var q = query!.Trim();
 1102        return q.StartsWith('?') ? q : "?" + q;
 103    }
 104
 105    // Escape curly braces for String.Format safety, but preserve the {0} placeholder used for status code.
 106    // If the template already contains escaped braces ({{ or }}), assume it is pre-escaped and skip.
 107    private static string EscapeTemplate(string bodyFormat)
 108    {
 1109        if (bodyFormat.Contains("{{", StringComparison.Ordinal) || bodyFormat.Contains("}}", StringComparison.Ordinal))
 110        {
 0111            return bodyFormat; // already escaped
 112        }
 113
 1114        var escaped = bodyFormat.Replace("{", "{{").Replace("}", "}}");
 115        // restore the status-code placeholder
 1116        escaped = escaped.Replace("{{0}}", "{0}");
 1117        return escaped;
 118    }
 119
 120    private static Func<StatusCodeContext, Task> BuildScriptHandler(StatusCodeOptions options, RequestDelegate compiled)
 121    {
 1122        return async context =>
 1123        {
 0124            var httpContext = context.HttpContext;
 1125
 1126            // If running a PowerShell script but the runspace middleware did not execute
 1127            // (e.g., no matched endpoint so UseWhen predicate failed), bootstrap a temporary
 1128            // runspace and KestrunContext so the compiled delegate can run safely.
 0129            var needsBootstrap = options.LanguageOptions!.Language == Scripting.ScriptLanguage.PowerShell &&
 0130                                 !httpContext.Items.ContainsKey(PowerShellDelegateBuilder.PS_INSTANCE_KEY);
 1131
 0132            if (!needsBootstrap)
 1133            {
 0134                await compiled(httpContext);
 0135                return;
 1136            }
 1137
 0138            var pool = options.Host.RunspacePool; // throws if not initialized
 0139            var runspace = await pool.AcquireAsync(httpContext.RequestAborted);
 0140            using var ps = PowerShell.Create();
 0141            ps.Runspace = runspace;
 1142
 1143            // Build Kestrun abstractions and inject into context for PS delegate to use
 0144            var req = await KestrunRequest.NewRequest(httpContext);
 0145            var res = new KestrunResponse(req)
 0146            {
 0147                StatusCode = httpContext.Response.StatusCode
 0148            };
 0149            var kr = new KestrunContext(pool.Host, req, res, httpContext);
 1150
 0151            httpContext.Items[PowerShellDelegateBuilder.PS_INSTANCE_KEY] = ps;
 0152            httpContext.Items[PowerShellDelegateBuilder.KR_CONTEXT_KEY] = kr;
 0153            var ss = ps.Runspace.SessionStateProxy;
 0154            ss.SetVariable("Context", kr);
 1155
 1156            try
 1157            {
 0158                await compiled(httpContext);
 0159            }
 1160            finally
 1161            {
 0162                pool.Release(ps.Runspace);
 0163                ps.Dispose();
 0164                _ = httpContext.Items.Remove(PowerShellDelegateBuilder.PS_INSTANCE_KEY);
 0165                _ = httpContext.Items.Remove(PowerShellDelegateBuilder.KR_CONTEXT_KEY);
 1166            }
 1167        };
 168    }
 169}