< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.Hosting.Options.ExceptionOptions
Assembly: Kestrun
File(s): /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Hosting/Options/ExceptionOptions.cs
Tag: Kestrun/Kestrun@5f1d2b981c9d7292c11fd448428c6ab6c811c5de
Line coverage
80%
Covered lines: 111
Uncovered lines: 27
Coverable lines: 138
Total lines: 211
Line coverage: 80.4%
Branch coverage
87%
Covered branches: 7
Total branches: 8
Branch coverage: 87.5%
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: 80.5% (108/134) Branch coverage: 87.5% (7/8) Total lines: 208 Tag: Kestrun/Kestrun@fcf33342333cef0516fe0d0912a86709874fd02612/12/2025 - 17:27:19 Line coverage: 48.5% (65/134) Branch coverage: 87.5% (7/8) Total lines: 208 Tag: Kestrun/Kestrun@826bf9dcf9db118c5de4c78a3259bce9549f0dcd12/15/2025 - 02:23:46 Line coverage: 80.7% (109/135) Branch coverage: 87.5% (7/8) Total lines: 208 Tag: Kestrun/Kestrun@7a3839f4de2254e22daae81ab8dc7cb2f40c833001/02/2026 - 00:16:25 Line coverage: 81.9% (109/133) Branch coverage: 87.5% (7/8) Total lines: 204 Tag: Kestrun/Kestrun@8405dc23b786b9d436fba0d65fb80baa4171e1d003/26/2026 - 03:54:59 Line coverage: 81.8% (108/132) Branch coverage: 87.5% (7/8) Total lines: 204 Tag: Kestrun/Kestrun@844b5179fb0492dc6b1182bae3ff65fa7365521d03/27/2026 - 14:18:40 Line coverage: 80.4% (111/138) Branch coverage: 87.5% (7/8) Total lines: 211 Tag: Kestrun/Kestrun@63388ea9aed376ffbb41cd2727be2fb7646f6402 11/19/2025 - 17:40:50 Line coverage: 80.5% (108/134) Branch coverage: 87.5% (7/8) Total lines: 208 Tag: Kestrun/Kestrun@fcf33342333cef0516fe0d0912a86709874fd02612/12/2025 - 17:27:19 Line coverage: 48.5% (65/134) Branch coverage: 87.5% (7/8) Total lines: 208 Tag: Kestrun/Kestrun@826bf9dcf9db118c5de4c78a3259bce9549f0dcd12/15/2025 - 02:23:46 Line coverage: 80.7% (109/135) Branch coverage: 87.5% (7/8) Total lines: 208 Tag: Kestrun/Kestrun@7a3839f4de2254e22daae81ab8dc7cb2f40c833001/02/2026 - 00:16:25 Line coverage: 81.9% (109/133) Branch coverage: 87.5% (7/8) Total lines: 204 Tag: Kestrun/Kestrun@8405dc23b786b9d436fba0d65fb80baa4171e1d003/26/2026 - 03:54:59 Line coverage: 81.8% (108/132) Branch coverage: 87.5% (7/8) Total lines: 204 Tag: Kestrun/Kestrun@844b5179fb0492dc6b1182bae3ff65fa7365521d03/27/2026 - 14:18:40 Line coverage: 80.4% (111/138) Branch coverage: 87.5% (7/8) Total lines: 211 Tag: Kestrun/Kestrun@63388ea9aed376ffbb41cd2727be2fb7646f6402

Coverage delta

Coverage delta 33 -33

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%22100%
get_DeveloperExceptionPageOptions()100%11100%
get_LanguageOptions()100%11100%
set_LanguageOptions(...)83.33%6685.71%
get_Host()100%11100%
BuildScriptExceptionHandler(...)100%1159.52%
UseJsonExceptionHandler(...)100%1188.75%

File(s)

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Hosting/Options/ExceptionOptions.cs

#LineLine coverage
 1using System.Management.Automation;
 2using System.Management.Automation.Runspaces;
 3using System.Diagnostics.CodeAnalysis;
 4using Kestrun.Languages;
 5using Kestrun.Models;
 6using Kestrun.Runtime;
 7using Microsoft.AspNetCore.Diagnostics;
 8
 9namespace Kestrun.Hosting.Options;
 10/// <summary>
 11/// Options for configuring Kestrun-style exception handling middleware.
 12/// </summary>
 13public sealed class ExceptionOptions : ExceptionHandlerOptions
 14{
 15    /// <summary>
 16    /// Optional scripting options (e.g., PowerShell). If present, a script-based handler is used.
 17    /// </summary>
 18    private LanguageOptions? _languageOptions;
 19
 20    /// <summary>
 21    /// Initializes a new instance of the <see cref="ExceptionOptions"/> class.
 22    /// </summary>
 23    /// <param name="host">The KestrunHost instance associated with these options.</param>
 24    /// <param name="developerExceptionPage">If true, enables the Developer Exception Page middleware with default optio
 25    [SetsRequiredMembers]
 526    public ExceptionOptions(KestrunHost host, bool developerExceptionPage = false)
 27    {
 528        ArgumentNullException.ThrowIfNull(host);
 529        Host = host;
 530        if (developerExceptionPage)
 31        {
 132            DeveloperExceptionPageOptions = new DeveloperExceptionPageOptions();
 33        }
 534    }
 35
 36    /// <summary>
 37    /// Optional Developer Exception Page options. If present, the Developer Exception Page middleware is used.
 38    /// </summary>
 739    public DeveloperExceptionPageOptions? DeveloperExceptionPageOptions { get; set; }
 40
 41    /// <summary>
 42    /// Optional scripting options (e.g., PowerShell). If present, a script-based handler is used.
 43    /// </summary>
 44    public LanguageOptions? LanguageOptions
 45    {
 246        get => _languageOptions;
 47        set
 48        {
 249            _languageOptions = value;
 250            if (value is not null && ExceptionHandler is null)
 51            {
 252                if (value.Language == Scripting.ScriptLanguage.PowerShell)
 53                {
 054                    throw new NotSupportedException("PowerShell is not supported for ExceptionOptions scripting at this 
 55                }
 256                var compiled = Host.CompileScript(value);
 57
 258                ExceptionHandler = BuildScriptExceptionHandler(this, compiled);
 59            }
 260        }
 61    }
 62
 63    /// <summary>Host is needed for runspace/bootstrap if using PowerShell.</summary>
 864    public required KestrunHost Host { get; init; }
 65
 66    // .NET 8 compatibility: ExceptionHandlerOptions in .NET 8 does not expose StatusCodeSelector.
 67    // Provide our own selector for net8.0; on net9.0+ prefer the base property.
 68#if !NET9_0_OR_GREATER
 69    /// <summary>
 70    /// .NET 8: Optional custom status code selector for JSON/script fallback.
 71    /// Lets you map exception types to status codes.
 72    /// </summary>
 73    public Func<Exception, int>? LegacyStatusCodeSelector { get; set; }
 74#endif
 75
 76    private RequestDelegate BuildScriptExceptionHandler(ExceptionOptions o, RequestDelegate compiled)
 77    {
 278        return async httpContext =>
 279        {
 280            // Ensure a PS runspace exists if we're executing a PowerShell-based handler outside the normal PS middlewar
 281            var needsBootstrap = o.LanguageOptions!.Language == Scripting.ScriptLanguage.PowerShell &&
 282                                 !httpContext.Items.ContainsKey(PowerShellDelegateBuilder.PS_INSTANCE_KEY);
 283
 284            if (!needsBootstrap)
 285            {
 286                await compiled(httpContext);
 287                return;
 288            }
 289
 090            var pool = o.Host.RunspacePool; // throws if not initialized
 091            Runspace? runspace = null;
 092            PowerShell? ps = null;
 293
 294            try
 295            {
 096                runspace = await pool.AcquireAsync(httpContext.RequestAborted);
 097                ps = PowerShell.Create();
 098                ps.Runspace = runspace;
 299
 2100                // Build Kestrun abstractions and inject for the script to consume
 2101
 0102                var kr = new KestrunContext(Host, httpContext);
 0103                httpContext.Items[PowerShellDelegateBuilder.PS_INSTANCE_KEY] = ps;
 0104                httpContext.Items[PowerShellDelegateBuilder.KR_CONTEXT_KEY] = kr;
 0105                ps.Runspace.SessionStateProxy.SetVariable("Context", kr);
 2106
 0107                await compiled(httpContext);
 0108            }
 2109            finally
 2110            {
 0111                ps?.Dispose();
 0112                if (runspace is not null)
 2113                {
 0114                    o.Host.RunspacePool.Release(runspace);
 2115                }
 0116                _ = httpContext.Items.Remove(PowerShellDelegateBuilder.PS_INSTANCE_KEY);
 0117                _ = httpContext.Items.Remove(PowerShellDelegateBuilder.KR_CONTEXT_KEY);
 2118            }
 4119        };
 120    }
 121
 122    /// <summary>
 123    /// Builds a JSON fallback handler that can be used if no script/inline handler is provided.
 124    /// </summary>
 125    /// <param name="useProblemDetails">Whether to use RFC 7807 ProblemDetails in the JSON fallback.</param>
 126    /// <param name="includeDetailsInDevelopment">Whether to include exception details in development.</param>
 127    /// <param name="compress">Whether to compress the JSON response.</param>
 128    /// <returns></returns>
 129    public void UseJsonExceptionHandler(bool useProblemDetails, bool includeDetailsInDevelopment, bool compress = false)
 130    {
 1131        ExceptionHandler = async httpContext =>
 1132        {
 1133            // If the response already started, let the server bubble up
 1134            if (httpContext.Response.HasStarted)
 1135            {
 1136                // optional: log here
 0137                return;
 1138            }
 1139
 1140            var env = httpContext.RequestServices.GetService(typeof(IHostEnvironment)) as IHostEnvironment;
 1141            var feature = httpContext.Features.Get<IExceptionHandlerFeature>();
 1142            var ex = feature?.Error;
 1143
 1144#if NET9_0_OR_GREATER
 1145            var status = StatusCodeSelector?.Invoke(ex ?? new Exception("Unhandled exception"))
 1146                         ?? StatusCodes.Status500InternalServerError;
 1147#else
 1148            var status = LegacyStatusCodeSelector?.Invoke(ex ?? new Exception("Unhandled exception"))
 1149                         ?? StatusCodes.Status500InternalServerError;
 1150#endif
 1151
 1152            var kestrunContext = new KestrunContext(Host, httpContext);
 1153
 1154            // Always set the HTTP status first
 1155            kestrunContext.Response.StatusCode = status;
 1156
 1157            // Choose the right media type
 1158            var contentType = useProblemDetails
 1159                ? "application/problem+json; charset=utf-8"
 1160                : "application/json; charset=utf-8";
 1161
 1162            // No-cache for error payloads
 1163            httpContext.Response.Headers.CacheControl = "no-store, no-cache, must-revalidate";
 1164            httpContext.Response.Headers.Pragma = "no-cache";
 1165            httpContext.Response.Headers.Expires = "0";
 1166
 1167            // Build body
 1168            var body = new Dictionary<string, object?>
 1169            {
 1170                ["status"] = status,
 1171                ["traceId"] = System.Diagnostics.Activity.Current?.Id ?? httpContext.TraceIdentifier,
 1172                ["instance"] = httpContext.Request.Path.HasValue ? httpContext.Request.Path.Value : "/"
 1173            };
 1174
 1175            if (useProblemDetails)
 1176            {
 1177                body["type"] = "about:blank"; // you can swap for a doc URL later
 1178                body["title"] = status switch
 1179                {
 0180                    400 => "Bad Request",
 0181                    401 => "Unauthorized",
 0182                    403 => "Forbidden",
 0183                    404 => "Not Found",
 1184                    _ => "Internal Server Error"
 1185                };
 1186                body["detail"] = includeDetailsInDevelopment && EnvironmentHelper.IsDevelopment()
 1187                                    ? ex?.ToString()
 1188                                    : ex?.Message;
 1189            }
 1190            else
 1191            {
 0192                body["error"] = true;
 0193                body["message"] = (includeDetailsInDevelopment && EnvironmentHelper.IsDevelopment())
 0194                                    ? ex?.ToString()
 0195                                    : ex?.Message;
 1196            }
 1197
 1198            // Note: `compress` means "compact JSON". Pretty-print when not compressing.
 1199            // Adjust the `5` (maxDepth) if your WriteJsonResponseAsync uses it that way.
 1200            await kestrunContext.Response.WriteJsonResponseAsync(
 1201                body,
 1202                5,
 1203                compress,       // compact when true
 1204                status,
 1205                contentType
 1206            );
 1207
 1208            await kestrunContext.Response.ApplyTo(httpContext.Response);
 2209        };
 1210    }
 211}