< 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@2d87023b37eb91155071c91dd3d6a2eeb3004705
Line coverage
80%
Covered lines: 108
Uncovered lines: 26
Coverable lines: 134
Total lines: 208
Line coverage: 80.5%
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@10d476bee71c71ad215bb8ab59f219887b5b4a5e 10/13/2025 - 16:52:37 Line coverage: 80.5% (108/134) Branch coverage: 87.5% (7/8) Total lines: 208 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e

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%1157.89%
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, Host.Logger);
 56
 257                ExceptionHandler = BuildScriptExceptionHandler(this, compiled);
 58            }
 259        }
 60    }
 61
 62    /// <summary>Host is needed for runspace/bootstrap if using PowerShell.</summary>
 1063    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>
 72    public Func<Exception, int>? LegacyStatusCodeSelector { get; set; }
 73#endif
 74
 75
 76
 77    private RequestDelegate BuildScriptExceptionHandler(ExceptionOptions o, RequestDelegate compiled)
 78    {
 279        return async httpContext =>
 280        {
 281            // Ensure a PS runspace exists if we're executing a PowerShell-based handler outside the normal PS middlewar
 282            var needsBootstrap = o.LanguageOptions!.Language == Scripting.ScriptLanguage.PowerShell &&
 283                                 !httpContext.Items.ContainsKey(PowerShellDelegateBuilder.PS_INSTANCE_KEY);
 284
 285            if (!needsBootstrap)
 286            {
 287                await compiled(httpContext);
 288                return;
 289            }
 290
 091            var pool = o.Host.RunspacePool; // throws if not initialized
 092            var runspace = await pool.AcquireAsync(httpContext.RequestAborted);
 093            using var ps = PowerShell.Create();
 094            ps.Runspace = runspace;
 295
 296            // Build Kestrun abstractions and inject for the script to consume
 097            var req = await KestrunRequest.NewRequest(httpContext);
 098            var res = new KestrunResponse(req) { StatusCode = httpContext.Response.StatusCode };
 099            var kr = new KestrunContext(Host, req, res, httpContext);
 2100
 0101            httpContext.Items[PowerShellDelegateBuilder.PS_INSTANCE_KEY] = ps;
 0102            httpContext.Items[PowerShellDelegateBuilder.KR_CONTEXT_KEY] = kr;
 0103            ps.Runspace.SessionStateProxy.SetVariable("Context", kr);
 2104
 2105            try
 2106            {
 0107                await compiled(httpContext);
 0108            }
 2109            finally
 2110            {
 0111                o.Host.RunspacePool.Release(ps.Runspace);
 0112                ps.Dispose();
 0113                _ = httpContext.Items.Remove(PowerShellDelegateBuilder.PS_INSTANCE_KEY);
 0114                _ = httpContext.Items.Remove(PowerShellDelegateBuilder.KR_CONTEXT_KEY);
 2115            }
 4116        };
 117    }
 118
 119    /// <summary>
 120    /// Builds a JSON fallback handler that can be used if no script/inline handler is provided.
 121    /// </summary>
 122    /// <param name="useProblemDetails">Whether to use RFC 7807 ProblemDetails in the JSON fallback.</param>
 123    /// <param name="includeDetailsInDevelopment">Whether to include exception details in development.</param>
 124    /// <param name="compress">Whether to compress the JSON response.</param>
 125    /// <returns></returns>
 126    public void UseJsonExceptionHandler(bool useProblemDetails, bool includeDetailsInDevelopment, bool compress = false)
 127    {
 1128        ExceptionHandler = async httpContext =>
 1129        {
 1130            // If the response already started, let the server bubble up
 1131            if (httpContext.Response.HasStarted)
 1132            {
 1133                // optional: log here
 0134                return;
 1135            }
 1136
 1137            var env = httpContext.RequestServices.GetService(typeof(IHostEnvironment)) as IHostEnvironment;
 1138            var feature = httpContext.Features.Get<IExceptionHandlerFeature>();
 1139            var ex = feature?.Error;
 1140
 1141#if NET9_0_OR_GREATER
 1142            var status = StatusCodeSelector?.Invoke(ex ?? new Exception("Unhandled exception"))
 1143                         ?? StatusCodes.Status500InternalServerError;
 1144#else
 1145            var status = LegacyStatusCodeSelector?.Invoke(ex ?? new Exception("Unhandled exception"))
 1146                         ?? StatusCodes.Status500InternalServerError;
 1147#endif
 1148
 1149            var kestrunContext = new KestrunContext(Host, httpContext);
 1150
 1151            // Always set the HTTP status first
 1152            kestrunContext.Response.StatusCode = status;
 1153
 1154            // Choose the right media type
 1155            var contentType = useProblemDetails
 1156                ? "application/problem+json; charset=utf-8"
 1157                : "application/json; charset=utf-8";
 1158
 1159            // No-cache for error payloads
 1160            httpContext.Response.Headers.CacheControl = "no-store, no-cache, must-revalidate";
 1161            httpContext.Response.Headers.Pragma = "no-cache";
 1162            httpContext.Response.Headers.Expires = "0";
 1163
 1164            // Build body
 1165            var body = new Dictionary<string, object?>
 1166            {
 1167                ["status"] = status,
 1168                ["traceId"] = System.Diagnostics.Activity.Current?.Id ?? httpContext.TraceIdentifier,
 1169                ["instance"] = httpContext.Request.Path.HasValue ? httpContext.Request.Path.Value : "/"
 1170            };
 1171
 1172            if (useProblemDetails)
 1173            {
 1174                body["type"] = "about:blank"; // you can swap for a doc URL later
 1175                body["title"] = status switch
 1176                {
 0177                    400 => "Bad Request",
 0178                    401 => "Unauthorized",
 0179                    403 => "Forbidden",
 0180                    404 => "Not Found",
 1181                    _ => "Internal Server Error"
 1182                };
 1183                body["detail"] = includeDetailsInDevelopment && EnvironmentHelper.IsDevelopment()
 1184                                    ? ex?.ToString()
 1185                                    : ex?.Message;
 1186            }
 1187            else
 1188            {
 0189                body["error"] = true;
 0190                body["message"] = (includeDetailsInDevelopment && EnvironmentHelper.IsDevelopment())
 0191                                    ? ex?.ToString()
 0192                                    : ex?.Message;
 1193            }
 1194
 1195            // Note: `compress` means "compact JSON". Pretty-print when not compressing.
 1196            // Adjust the `5` (maxDepth) if your WriteJsonResponseAsync uses it that way.
 1197            await kestrunContext.Response.WriteJsonResponseAsync(
 1198                body,
 1199                5,
 1200                compress,       // compact when true
 1201                status,
 1202                contentType
 1203            );
 1204
 1205            await kestrunContext.Response.ApplyTo(httpContext.Response);
 2206        };
 1207    }
 208}