< 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@ca54e35c77799b76774b3805b6f075cdbc0c5fbe
Line coverage
81%
Covered lines: 109
Uncovered lines: 24
Coverable lines: 133
Total lines: 204
Line coverage: 81.9%
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 10/13/2025 - 16:52:37 Line coverage: 80.5% (108/134) Branch coverage: 87.5% (7/8) Total lines: 208 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e12/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@8405dc23b786b9d436fba0d65fb80baa4171e1d0 10/13/2025 - 16:52:37 Line coverage: 80.5% (108/134) Branch coverage: 87.5% (7/8) Total lines: 208 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e12/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@8405dc23b786b9d436fba0d65fb80baa4171e1d0

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%
get_LegacyStatusCodeSelector()100%11100%
BuildScriptExceptionHandler(...)100%1161.11%
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.Diagnostics.CodeAnalysis;
 3using Kestrun.Languages;
 4using Kestrun.Models;
 5using Kestrun.Runtime;
 6using Microsoft.AspNetCore.Diagnostics;
 7
 8namespace Kestrun.Hosting.Options;
 9/// <summary>
 10/// Options for configuring Kestrun-style exception handling middleware.
 11/// </summary>
 12public sealed class ExceptionOptions : ExceptionHandlerOptions
 13{
 14    /// <summary>
 15    /// Optional scripting options (e.g., PowerShell). If present, a script-based handler is used.
 16    /// </summary>
 17    private LanguageOptions? _languageOptions;
 18
 19    /// <summary>
 20    /// Initializes a new instance of the <see cref="ExceptionOptions"/> class.
 21    /// </summary>
 22    /// <param name="host">The KestrunHost instance associated with these options.</param>
 23    /// <param name="developerExceptionPage">If true, enables the Developer Exception Page middleware with default optio
 24    [SetsRequiredMembers]
 525    public ExceptionOptions(KestrunHost host, bool developerExceptionPage = false)
 26    {
 527        ArgumentNullException.ThrowIfNull(host);
 528        Host = host;
 529        if (developerExceptionPage)
 30        {
 131            DeveloperExceptionPageOptions = new DeveloperExceptionPageOptions();
 32        }
 533    }
 34
 35    /// <summary>
 36    /// Optional Developer Exception Page options. If present, the Developer Exception Page middleware is used.
 37    /// </summary>
 738    public DeveloperExceptionPageOptions? DeveloperExceptionPageOptions { get; set; }
 39
 40    /// <summary>
 41    /// Optional scripting options (e.g., PowerShell). If present, a script-based handler is used.
 42    /// </summary>
 43    public LanguageOptions? LanguageOptions
 44    {
 245        get => _languageOptions;
 46        set
 47        {
 248            _languageOptions = value;
 249            if (value is not null && ExceptionHandler is null)
 50            {
 251                if (value.Language == Scripting.ScriptLanguage.PowerShell)
 52                {
 053                    throw new NotSupportedException("PowerShell is not supported for ExceptionOptions scripting at this 
 54                }
 255                var compiled = Host.CompileScript(value);
 56
 257                ExceptionHandler = BuildScriptExceptionHandler(this, compiled);
 58            }
 259        }
 60    }
 61
 62    /// <summary>Host is needed for runspace/bootstrap if using PowerShell.</summary>
 863    public required KestrunHost Host { get; init; }
 64
 65    // .NET 8 compatibility: ExceptionHandlerOptions in .NET 8 does not expose StatusCodeSelector.
 66    // Provide our own selector for net8.0; on net9.0+ prefer the base property.
 67#if !NET9_0_OR_GREATER
 68    /// <summary>
 69    /// .NET 8: Optional custom status code selector for JSON/script fallback.
 70    /// Lets you map exception types to status codes.
 71    /// </summary>
 172    public Func<Exception, int>? LegacyStatusCodeSelector { get; set; }
 73#endif
 74
 75    private RequestDelegate BuildScriptExceptionHandler(ExceptionOptions o, RequestDelegate compiled)
 76    {
 277        return async httpContext =>
 278        {
 279            // Ensure a PS runspace exists if we're executing a PowerShell-based handler outside the normal PS middlewar
 280            var needsBootstrap = o.LanguageOptions!.Language == Scripting.ScriptLanguage.PowerShell &&
 281                                 !httpContext.Items.ContainsKey(PowerShellDelegateBuilder.PS_INSTANCE_KEY);
 282
 283            if (!needsBootstrap)
 284            {
 285                await compiled(httpContext);
 286                return;
 287            }
 288
 089            var pool = o.Host.RunspacePool; // throws if not initialized
 090            var runspace = await pool.AcquireAsync(httpContext.RequestAborted);
 091            using var ps = PowerShell.Create();
 092            ps.Runspace = runspace;
 293
 294            // Build Kestrun abstractions and inject for the script to consume
 295
 096            var kr = new KestrunContext(Host, httpContext);
 097            httpContext.Items[PowerShellDelegateBuilder.PS_INSTANCE_KEY] = ps;
 098            httpContext.Items[PowerShellDelegateBuilder.KR_CONTEXT_KEY] = kr;
 099            ps.Runspace.SessionStateProxy.SetVariable("Context", kr);
 2100
 2101            try
 2102            {
 0103                await compiled(httpContext);
 0104            }
 2105            finally
 2106            {
 0107                o.Host.RunspacePool.Release(ps.Runspace);
 0108                ps.Dispose();
 0109                _ = httpContext.Items.Remove(PowerShellDelegateBuilder.PS_INSTANCE_KEY);
 0110                _ = httpContext.Items.Remove(PowerShellDelegateBuilder.KR_CONTEXT_KEY);
 2111            }
 4112        };
 113    }
 114
 115    /// <summary>
 116    /// Builds a JSON fallback handler that can be used if no script/inline handler is provided.
 117    /// </summary>
 118    /// <param name="useProblemDetails">Whether to use RFC 7807 ProblemDetails in the JSON fallback.</param>
 119    /// <param name="includeDetailsInDevelopment">Whether to include exception details in development.</param>
 120    /// <param name="compress">Whether to compress the JSON response.</param>
 121    /// <returns></returns>
 122    public void UseJsonExceptionHandler(bool useProblemDetails, bool includeDetailsInDevelopment, bool compress = false)
 123    {
 1124        ExceptionHandler = async httpContext =>
 1125        {
 1126            // If the response already started, let the server bubble up
 1127            if (httpContext.Response.HasStarted)
 1128            {
 1129                // optional: log here
 0130                return;
 1131            }
 1132
 1133            var env = httpContext.RequestServices.GetService(typeof(IHostEnvironment)) as IHostEnvironment;
 1134            var feature = httpContext.Features.Get<IExceptionHandlerFeature>();
 1135            var ex = feature?.Error;
 1136
 1137#if NET9_0_OR_GREATER
 1138            var status = StatusCodeSelector?.Invoke(ex ?? new Exception("Unhandled exception"))
 1139                         ?? StatusCodes.Status500InternalServerError;
 1140#else
 1141            var status = LegacyStatusCodeSelector?.Invoke(ex ?? new Exception("Unhandled exception"))
 1142                         ?? StatusCodes.Status500InternalServerError;
 1143#endif
 1144
 1145            var kestrunContext = new KestrunContext(Host, httpContext);
 1146
 1147            // Always set the HTTP status first
 1148            kestrunContext.Response.StatusCode = status;
 1149
 1150            // Choose the right media type
 1151            var contentType = useProblemDetails
 1152                ? "application/problem+json; charset=utf-8"
 1153                : "application/json; charset=utf-8";
 1154
 1155            // No-cache for error payloads
 1156            httpContext.Response.Headers.CacheControl = "no-store, no-cache, must-revalidate";
 1157            httpContext.Response.Headers.Pragma = "no-cache";
 1158            httpContext.Response.Headers.Expires = "0";
 1159
 1160            // Build body
 1161            var body = new Dictionary<string, object?>
 1162            {
 1163                ["status"] = status,
 1164                ["traceId"] = System.Diagnostics.Activity.Current?.Id ?? httpContext.TraceIdentifier,
 1165                ["instance"] = httpContext.Request.Path.HasValue ? httpContext.Request.Path.Value : "/"
 1166            };
 1167
 1168            if (useProblemDetails)
 1169            {
 1170                body["type"] = "about:blank"; // you can swap for a doc URL later
 1171                body["title"] = status switch
 1172                {
 0173                    400 => "Bad Request",
 0174                    401 => "Unauthorized",
 0175                    403 => "Forbidden",
 0176                    404 => "Not Found",
 1177                    _ => "Internal Server Error"
 1178                };
 1179                body["detail"] = includeDetailsInDevelopment && EnvironmentHelper.IsDevelopment()
 1180                                    ? ex?.ToString()
 1181                                    : ex?.Message;
 1182            }
 1183            else
 1184            {
 0185                body["error"] = true;
 0186                body["message"] = (includeDetailsInDevelopment && EnvironmentHelper.IsDevelopment())
 0187                                    ? ex?.ToString()
 0188                                    : ex?.Message;
 1189            }
 1190
 1191            // Note: `compress` means "compact JSON". Pretty-print when not compressing.
 1192            // Adjust the `5` (maxDepth) if your WriteJsonResponseAsync uses it that way.
 1193            await kestrunContext.Response.WriteJsonResponseAsync(
 1194                body,
 1195                5,
 1196                compress,       // compact when true
 1197                status,
 1198                contentType
 1199            );
 1200
 1201            await kestrunContext.Response.ApplyTo(httpContext.Response);
 2202        };
 1203    }
 204}