< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.Hosting.KestrunHostMapExtensions
Assembly: Kestrun
File(s): /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Hosting/KestrunHostMapExtensions.cs
Tag: Kestrun/Kestrun@2d87023b37eb91155071c91dd3d6a2eeb3004705
Line coverage
71%
Covered lines: 286
Uncovered lines: 116
Coverable lines: 402
Total lines: 1148
Line coverage: 71.1%
Branch coverage
81%
Covered branches: 170
Total branches: 209
Branch coverage: 81.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 08/26/2025 - 01:25:22 Line coverage: 55.1% (140/254) Branch coverage: 61.6% (66/107) Total lines: 716 Tag: Kestrun/Kestrun@07f821172e5dc3657f1be7e6818f18d6721cf38a09/01/2025 - 04:08:24 Line coverage: 54.6% (140/256) Branch coverage: 60.5% (66/109) Total lines: 722 Tag: Kestrun/Kestrun@d6f26a131219b7a7fcb4e129af3193ec2ec4892910/13/2025 - 16:52:37 Line coverage: 62.5% (281/449) Branch coverage: 77.7% (164/211) Total lines: 1260 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e10/15/2025 - 21:27:26 Line coverage: 71.1% (286/402) Branch coverage: 81.3% (170/209) Total lines: 1149 Tag: Kestrun/Kestrun@c33ec02a85e4f8d6061aeaab5a5e8c3a8b66559410/15/2025 - 22:47:55 Line coverage: 71.1% (286/402) Branch coverage: 81.3% (170/209) Total lines: 1148 Tag: Kestrun/Kestrun@f97c41150c4de89829eca919cc8b9b7e7df3df8e 08/26/2025 - 01:25:22 Line coverage: 55.1% (140/254) Branch coverage: 61.6% (66/107) Total lines: 716 Tag: Kestrun/Kestrun@07f821172e5dc3657f1be7e6818f18d6721cf38a09/01/2025 - 04:08:24 Line coverage: 54.6% (140/256) Branch coverage: 60.5% (66/109) Total lines: 722 Tag: Kestrun/Kestrun@d6f26a131219b7a7fcb4e129af3193ec2ec4892910/13/2025 - 16:52:37 Line coverage: 62.5% (281/449) Branch coverage: 77.7% (164/211) Total lines: 1260 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e10/15/2025 - 21:27:26 Line coverage: 71.1% (286/402) Branch coverage: 81.3% (170/209) Total lines: 1149 Tag: Kestrun/Kestrun@c33ec02a85e4f8d6061aeaab5a5e8c3a8b66559410/15/2025 - 22:47:55 Line coverage: 71.1% (286/402) Branch coverage: 81.3% (170/209) Total lines: 1148 Tag: Kestrun/Kestrun@f97c41150c4de89829eca919cc8b9b7e7df3df8e

Metrics

File(s)

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Hosting/KestrunHostMapExtensions.cs

#LineLine coverage
 1using System.Net;
 2using System.Text.RegularExpressions;
 3using Kestrun.Hosting.Options;
 4using Kestrun.Languages;
 5using Kestrun.Models;
 6using Kestrun.Runtime;
 7using Kestrun.Scripting;
 8using Kestrun.TBuilder;
 9using Kestrun.Utilities;
 10using Microsoft.AspNetCore.Antiforgery;
 11using Microsoft.AspNetCore.Authorization;
 12using Microsoft.Extensions.Options;
 13using Serilog.Events;
 14
 15namespace Kestrun.Hosting;
 16
 17/// <summary>
 18/// Provides extension methods for mapping routes and handlers to the KestrunHost.
 19/// </summary>
 20public static partial class KestrunHostMapExtensions
 21{
 22    /// <summary>
 23    /// Public utility facade for endpoint specification parsing. This provides a stable API surface
 24    /// over the internal helper logic used by host route constraint processing.
 25    /// </summary>
 26    public static class EndpointSpecParser
 27    {
 28        /// <summary>
 29        /// Parses an endpoint specification into host, port and optional HTTPS flag.
 30        /// </summary>
 31        /// <param name="spec">Specification string. See <see cref="TryParseEndpointSpec"/> for accepted formats.</param
 32        /// <param name="host">Resolved host when successful, otherwise empty string.</param>
 33        /// <param name="port">Resolved port when successful, otherwise 0.</param>
 34        /// <param name="https">True for https, false for http, null when unspecified (host:port form).</param>
 35        /// <returns><c>true</c> if parsing succeeds; otherwise <c>false</c>.</returns>
 36        public static bool TryParse(string spec, out string host, out int port, out bool? https)
 1637            => TryParseEndpointSpec(spec, out host, out port, out https);
 38    }
 39    /// <summary>
 40    /// Represents a delegate that handles a Kestrun request with the provided context.
 41    /// </summary>
 42    /// <param name="Context">The context for the Kestrun request.</param>
 43    /// <returns>A task representing the asynchronous operation.</returns>
 44    public delegate Task KestrunHandler(KestrunContext Context);
 45
 46    /// <summary>
 47    /// Adds a native route to the KestrunHost for the specified pattern and HTTP verb.
 48    /// </summary>
 49    /// <param name="host">The KestrunHost instance.</param>
 50    /// <param name="pattern">The route pattern.</param>
 51    /// <param name="httpVerb">The HTTP verb for the route.</param>
 52    /// <param name="handler">The handler to execute for the route.</param>
 53    /// <param name="requireSchemes">Optional array of authorization schemes required for the route.</param>
 54    /// <param name="map">The endpoint convention builder for further configuration.</param>
 55    /// <returns>The KestrunHost instance for chaining.</returns>
 56    public static KestrunHost AddMapRoute(this KestrunHost host, string pattern, HttpVerb httpVerb, KestrunHandler handl
 257    host.AddMapRoute(pattern: pattern, httpVerbs: [httpVerb], handler: handler, out map, requireSchemes: requireSchemes)
 58
 59    /// <summary>
 60    /// Adds a native route to the KestrunHost for the specified pattern and HTTP verbs.
 61    /// </summary>
 62    /// <param name="host">The KestrunHost instance.</param>
 63    /// <param name="pattern">The route pattern.</param>
 64    /// <param name="httpVerbs">The HTTP verbs for the route.</param>
 65    /// <param name="handler">The handler to execute for the route.</param>
 66    /// <param name="requireSchemes">Optional array of authorization schemes required for the route.</param>
 67    /// <param name="map">The endpoint convention builder for further configuration.</param>
 68    /// <returns>The KestrunHost instance for chaining.</returns>
 69    public static KestrunHost AddMapRoute(this KestrunHost host, string pattern, IEnumerable<HttpVerb> httpVerbs, Kestru
 70    out IEndpointConventionBuilder? map, string[]? requireSchemes = null)
 71    {
 272        return host.AddMapRoute(new MapRouteOptions
 273        {
 274            Pattern = pattern,
 275            HttpVerbs = [.. httpVerbs],
 276            ScriptCode = new LanguageOptions
 277            {
 278                Language = ScriptLanguage.Native,
 279            },
 280            RequireSchemes = requireSchemes ?? [] // No authorization by default
 281        }, handler, out map);
 82    }
 83
 84    /// <summary>
 85    /// Adds a native route to the KestrunHost using the specified MapRouteOptions and handler.
 86    /// </summary>
 87    /// <param name="host">The KestrunHost instance.</param>
 88    /// <param name="options">The MapRouteOptions containing route configuration.</param>
 89    /// <param name="handler">The handler to execute for the route.</param>
 90    /// <param name="map">The endpoint convention builder for further configuration.</param>
 91    /// <returns>The KestrunHost instance for chaining.</returns>
 92    public static KestrunHost AddMapRoute(this KestrunHost host, MapRouteOptions options, KestrunHandler handler, out IE
 93    {
 294        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 95        {
 196            host.Logger.Debug("AddMapRoute called with options={Options}", options);
 97        }
 98        // Ensure the WebApplication is initialized
 299        if (host.App is null)
 100        {
 0101            throw new InvalidOperationException("WebApplication is not initialized. Call EnableConfiguration first.");
 102        }
 103
 104        // Validate options
 1105        if (string.IsNullOrWhiteSpace(options.Pattern))
 106        {
 0107            throw new ArgumentException("Pattern cannot be null or empty.", nameof(options.Pattern));
 108        }
 109
 2110        string[] methods = [.. options.HttpVerbs.Select(v => v.ToMethodString())];
 1111        map = host.App.MapMethods(options.Pattern, methods, async context =>
 1112         {
 1113             // 🔒 CSRF validation only for the current request when that verb is unsafe (unless disabled)
 0114             if (ShouldValidateCsrf(options, context))
 1115             {
 0116                 if (!await TryValidateAntiforgeryAsync(context))
 1117                 {
 0118                     return; // already responded 400
 1119                 }
 1120             }
 0121             var req = await KestrunRequest.NewRequest(context);
 0122             var res = new KestrunResponse(req);
 0123             KestrunContext kestrunContext = new(host, req, res, context);
 0124             await handler(kestrunContext);
 0125             await res.ApplyTo(context.Response);
 1126         });
 127
 1128        host.AddMapOptions(map, options);
 129
 1130        host.Logger.Information("Added native route: {Pattern} with methods: {Methods}", options.Pattern, string.Join(",
 131        // Add to the feature queue for later processing
 1132        host.FeatureQueue.Add(host => host.AddMapRoute(options));
 1133        return host;
 134    }
 135
 136
 137    /// <summary>
 138    /// Adds a route to the KestrunHost that executes a script block for the specified HTTP verb and pattern.
 139    /// </summary>
 140    /// <param name="host">The KestrunHost instance.</param>
 141    /// <param name="pattern">The route pattern.</param>
 142    /// <param name="httpVerbs">The HTTP verb for the route.</param>
 143    /// <param name="scriptBlock">The script block to execute.</param>
 144    /// <param name="language">The scripting language to use (default is PowerShell).</param>
 145    /// <param name="requireSchemes">Optional array of authorization schemes required for the route.</param>
 146    /// <param name="arguments">Optional dictionary of arguments to pass to the script.</param>
 147    /// <returns>The KestrunHost instance for chaining.</returns>
 148    public static KestrunHost AddMapRoute(this KestrunHost host, string pattern, HttpVerb httpVerbs, string scriptBlock,
 149                                     string[]? requireSchemes = null,
 150                                 Dictionary<string, object?>? arguments = null)
 151    {
 11152        arguments ??= [];
 11153        return host.AddMapRoute(new MapRouteOptions
 11154        {
 11155            Pattern = pattern,
 11156            HttpVerbs = [httpVerbs],
 11157            ScriptCode = new LanguageOptions
 11158            {
 11159                Code = scriptBlock,
 11160                Language = language,
 11161                Arguments = arguments ?? [] // No additional arguments by default
 11162            },
 11163            RequireSchemes = requireSchemes ?? [], // No authorization by default
 11164        });
 165    }
 166
 167    /// <summary>
 168    /// Adds a route to the KestrunHost that executes a script block for the specified HTTP verbs and pattern.
 169    /// </summary>
 170    /// <param name="host">The KestrunHost instance.</param>
 171    /// <param name="pattern">The route pattern.</param>
 172    /// <param name="httpVerbs">The HTTP verbs for the route.</param>
 173    /// <param name="scriptBlock">The script block to execute.</param>
 174    /// <param name="language">The scripting language to use (default is PowerShell).</param>
 175    /// <param name="requireSchemes">Optional array of authorization schemes required for the route.</param>
 176    /// <param name="arguments">Optional dictionary of arguments to pass to the script.</param>
 177    /// <returns>The KestrunHost instance for chaining.</returns>
 178    public static KestrunHost AddMapRoute(this KestrunHost host, string pattern,
 179                                IEnumerable<HttpVerb> httpVerbs,
 180                                string scriptBlock,
 181                                ScriptLanguage language = ScriptLanguage.PowerShell,
 182                                string[]? requireSchemes = null,
 183                                 Dictionary<string, object?>? arguments = null)
 184    {
 1185        return host.AddMapRoute(new MapRouteOptions
 1186        {
 1187            Pattern = pattern,
 1188            HttpVerbs = [.. httpVerbs],
 1189            ScriptCode = new LanguageOptions
 1190            {
 1191                Code = scriptBlock,
 1192                Language = language,
 1193                Arguments = arguments ?? [] // No additional arguments by default
 1194            },
 1195            RequireSchemes = requireSchemes ?? [], // No authorization by default
 1196        });
 197    }
 198
 199    /// <summary>
 200    /// Adds a route to the KestrunHost using the specified MapRouteOptions.
 201    /// </summary>
 202    /// <param name="host">The KestrunHost instance.</param>
 203    /// <param name="options">The MapRouteOptions containing route configuration.</param>
 204    /// <returns>The KestrunHost instance for chaining.</returns>
 205    public static KestrunHost AddMapRoute(this KestrunHost host, MapRouteOptions options)
 206    {
 31207        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 208        {
 22209            host.Logger.Debug("AddMapRoute called with pattern={Pattern}, language={Language}, method={Methods}", option
 210        }
 31211        if (host.IsConfigured)
 212        {
 30213            _ = CreateMapRoute(host, options);
 214        }
 215        else
 216        {
 1217            _ = host.Use(app =>
 1218            {
 1219                _ = CreateMapRoute(host, options);
 2220            });
 221        }
 25222        return host; // for chaining
 223    }
 224
 225    /// <summary>
 226    /// Adds a route to the KestrunHost using the specified MapRouteOptions.
 227    /// </summary>
 228    /// <param name="host">The KestrunHost instance.</param>
 229    /// <param name="options">The MapRouteOptions containing route configuration.</param>
 230    /// <returns>The IEndpointConventionBuilder for the created route.</returns>
 231    private static IEndpointConventionBuilder CreateMapRoute(KestrunHost host, MapRouteOptions options)
 232    {
 31233        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 234        {
 22235            host.Logger.Debug("AddMapRoute called with pattern={Pattern}, language={Language}, method={Methods}", option
 236        }
 237
 238        try
 239        {
 240            // Validate options and get normalized route options
 31241            if (!ValidateRouteOptions(host, options, out var routeOptions))
 242            {
 0243                return null!; // Route already exists and should be skipped
 244            }
 245
 30246            var logger = host.Logger.ForContext("Route", routeOptions.Pattern);
 247
 248            // Compile the script once – return a RequestDelegate
 30249            var compiled = CompileScript(host, options.ScriptCode, logger);
 250
 251            // Create and register the route
 30252            return CreateAndRegisterRoute(host, routeOptions, compiled);
 253        }
 0254        catch (CompilationErrorException ex)
 255        {
 256            // Log the detailed compilation errors
 0257            host.Logger.Error($"Failed to add route '{options.Pattern}' due to compilation errors:");
 0258            host.Logger.Error(ex.GetDetailedErrorMessage());
 259
 260            // Re-throw with additional context
 0261            throw new InvalidOperationException(
 0262                $"Failed to compile {options.ScriptCode.Language} script for route '{options.Pattern}'. {ex.GetErrors().
 0263                ex);
 264        }
 6265        catch (Exception ex)
 266        {
 6267            throw new InvalidOperationException(
 6268                $"Failed to add route '{options.Pattern}' with method '{string.Join(", ", options.HttpVerbs)}' using {op
 6269                ex);
 270        }
 25271    }
 272
 273
 274    /// <summary>
 275    /// Validates the host and options for adding a map route.
 276    /// </summary>
 277    /// <param name="host">The KestrunHost instance.</param>
 278    /// <param name="options">The MapRouteOptions to validate.</param>
 279    /// <param name="routeOptions">The validated route options with defaults applied.</param>
 280    /// <returns>True if validation passes and route should be added; false if duplicate route should be skipped.</retur
 281    /// <exception cref="InvalidOperationException">Thrown when WebApplication is not initialized or route already exist
 282    /// <exception cref="ArgumentException">Thrown when required options are invalid.</exception>
 283    internal static bool ValidateRouteOptions(KestrunHost host, MapRouteOptions options, out MapRouteOptions routeOption
 284    {
 285        // Ensure the WebApplication is initialized
 40286        if (host.App is null)
 287        {
 0288            throw new InvalidOperationException("WebApplication is not initialized. Call EnableConfiguration first.");
 289        }
 290
 291        // Validate options
 39292        if (string.IsNullOrWhiteSpace(options.Pattern))
 293        {
 2294            throw new ArgumentException("Pattern cannot be null or empty.", nameof(options.Pattern));
 295        }
 296
 297        // Validate code
 37298        if (string.IsNullOrWhiteSpace(options.ScriptCode.Code))
 299        {
 2300            throw new ArgumentException("ScriptBlock cannot be null or empty.", nameof(options.ScriptCode.Code));
 301        }
 302
 35303        routeOptions = options;
 35304        if (options.HttpVerbs.Count == 0)
 305        {
 306            // Create a new RouteOptions with HttpVerbs set to [HttpVerb.Get]
 2307            routeOptions = options with { HttpVerbs = [HttpVerb.Get] };
 308        }
 309
 35310        if (MapExists(host, routeOptions.Pattern, routeOptions.HttpVerbs))
 311        {
 3312            var msg = $"Route '{routeOptions.Pattern}' with method(s) {string.Join(", ", routeOptions.HttpVerbs)} alread
 3313            if (options.ThrowOnDuplicate)
 314            {
 2315                throw new InvalidOperationException(msg);
 316            }
 317
 1318            host.Logger.Warning(msg);
 1319            return false; // Skip this route
 320        }
 321
 32322        return true; // Continue with route creation
 323    }
 324
 325    /// <summary>
 326    /// Compiles the script code for the specified language.
 327    /// </summary>
 328    /// <param name="host">The KestrunHost instance.</param>
 329    /// <param name="options">The language options containing the script code and language.</param>
 330    /// <param name="logger">The Serilog logger to use for compilation.</param>
 331    /// <returns>A compiled RequestDelegate that can handle HTTP requests.</returns>
 332    /// <exception cref="NotSupportedException">Thrown when the script language is not supported.</exception>
 333    internal static RequestDelegate CompileScript(this KestrunHost host, LanguageOptions options, Serilog.ILogger logger
 334    {
 37335        return options.Language switch
 37336        {
 1337            ScriptLanguage.PowerShell => PowerShellDelegateBuilder.Build(options.Code!, logger, options.Arguments),
 33338            ScriptLanguage.CSharp => CSharpDelegateBuilder.Build(host, options.Code!, options.Arguments, options.ExtraIm
 2339            ScriptLanguage.VBNet => VBNetDelegateBuilder.Build(host, options.Code!, options.Arguments, options.ExtraImpo
 0340            ScriptLanguage.FSharp => FSharpDelegateBuilder.Build(options.Code!, logger), // F# scripting not implemented
 0341            ScriptLanguage.Python => PyDelegateBuilder.Build(options.Code!, logger),
 0342            ScriptLanguage.JavaScript => JScriptDelegateBuilder.Build(options.Code!, logger),
 1343            _ => throw new NotSupportedException(options.Language.ToString())
 37344        };
 345    }
 346
 347    /// <summary>
 348    /// Creates and registers a route with the specified options and compiled handler.
 349    /// </summary>
 350    /// <param name="host">The KestrunHost instance.</param>
 351    /// <param name="routeOptions">The validated route options.</param>
 352    /// <param name="compiled">The compiled script delegate.</param>
 353    /// <returns>An IEndpointConventionBuilder for further configuration.</returns>
 354    internal static IEndpointConventionBuilder CreateAndRegisterRoute(KestrunHost host, MapRouteOptions routeOptions, Re
 355    {
 356        // Wrap with CSRF validation
 357        async Task handler(HttpContext ctx)
 358        {
 11359            if (ShouldValidateCsrf(routeOptions, ctx))
 360            {
 0361                if (!await TryValidateAntiforgeryAsync(ctx))
 362                {
 0363                    return; // already responded 400
 364                }
 365            }
 11366            await compiled(ctx);
 6367        }
 368
 67369        string[] methods = [.. routeOptions.HttpVerbs.Select(v => v.ToMethodString())];
 32370        var map = host.App!.MapMethods(routeOptions.Pattern!, methods, handler).WithLanguage(routeOptions.ScriptCode.Lan
 371
 32372        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 373        {
 23374            host.Logger.Debug("Mapped route: {Pattern} with methods: {Methods}", routeOptions.Pattern, string.Join(", ",
 375        }
 376
 32377        host.AddMapOptions(map, routeOptions);
 378
 144379        foreach (var method in routeOptions.HttpVerbs.Select(v => v.ToMethodString()))
 380        {
 30381            host._registeredRoutes[(routeOptions.Pattern!, method)] = routeOptions;
 382        }
 383
 27384        host.Logger.Information("Added route: {Pattern} with methods: {Methods}", routeOptions.Pattern, string.Join(", "
 27385        return map;
 386    }
 387
 388    /// <summary>
 389    /// Adds additional mapping options to the route.
 390    /// </summary>
 391    /// <param name="host">The Kestrun host.</param>
 392    /// <param name="map">The endpoint convention builder.</param>
 393    /// <param name="options">The mapping options.</param>
 394    internal static void AddMapOptions(this KestrunHost host, IEndpointConventionBuilder map, MapRouteOptions options)
 395    {
 35396        ApplyShortCircuit(host, map, options);
 35397        ApplyAnonymous(host, map, options);
 35398        DisableAntiforgery(host, map, options);
 35399        DisableResponseCompression(host, map, options);
 35400        ApplyRateLimiting(host, map, options);
 35401        ApplyAuthSchemes(host, map, options);
 34402        ApplyPolicies(host, map, options);
 33403        ApplyCors(host, map, options);
 33404        ApplyOpenApiMetadata(host, map, options);
 33405        ApplyRequiredHost(host, map, options);
 30406    }
 407
 408    /// <summary>
 409    /// Tries to parse an endpoint specification string into its components: host, port, and HTTPS flag.
 410    /// </summary>
 411    /// <param name="spec">The endpoint specification string.</param>
 412    /// <param name="host">The host component.</param>
 413    /// <param name="port">The port component.</param>
 414    /// <param name="https">
 415    /// Indicates HTTPS (<c>true</c>) or HTTP (<c>false</c>) when the scheme is explicitly specified via a full URL.
 416    /// For host:port forms where no scheme information is available the value is <c>null</c>.
 417    /// </param>
 418    /// <returns>
 419    /// <c>true</c> if parsing succeeds; otherwise <c>false</c> and <paramref name="host"/> will be <c>string.Empty</c> 
 420    /// </returns>
 421    /// <remarks>
 422    /// Accepted formats (in priority order):
 423    /// <list type="bullet">
 424    /// <item><description>Full URL: <c>https://host:port</c>, <c>http://host:port</c>, IPv6 literal allowed in brackets
 425    /// <item><description>Bracketed IPv6 host &amp; port: <c>[::1]:5000</c>, <c>[2001:db8::1]:8080</c>.</description></
 426    /// <item><description>Host or IPv4 with port: <c>localhost:5000</c>, <c>127.0.0.1:8080</c>, <c>example.com:443</c>.
 427    /// </list>
 428    /// Unsupported / rejected examples: non http(s) schemes (e.g. <c>ftp://</c>), missing port in host:port form, empty
 429    /// </remarks>
 430    public static bool TryParseEndpointSpec(string spec, out string host, out int port, out bool? https)
 431    {
 171432        host = ""; port = 0; https = null;
 433
 57434        if (string.IsNullOrWhiteSpace(spec))
 435        {
 5436            return false;
 437        }
 438
 439        // 1. Try full URL form first
 52440        if (TryParseUrlSpec(spec, out host, out port, out https))
 441        {
 16442            return true;
 443        }
 444
 445        // 2. Bracketed IPv6 literal with port: [::1]:5000
 36446        if (TryParseBracketedIpv6Spec(spec, out host, out port))
 447        {
 4448            return true; // https stays null (not specified)
 449        }
 450
 451        // 3. Regular host:port (hostname, IPv4, or raw IPv6 w/out brackets not supported here)
 32452        if (TryParseHostPortSpec(spec, out host, out port))
 453        {
 12454            return true; // https stays null (not specified)
 455        }
 456
 457        // No match
 60458        host = ""; port = 0; https = null;
 20459        return false;
 460    }
 461
 462    /// <summary>
 463    /// Tries to parse a full URL endpoint specification.
 464    /// </summary>
 465    /// <param name="spec">The endpoint specification string.</param>
 466    /// <param name="host">The parsed host component.</param>
 467    /// <param name="port">The parsed port component.</param>
 468    /// <param name="https">The parsed HTTPS flag.</param>
 469    /// <returns><c>true</c> if parsing succeeded; otherwise <c>false</c>.</returns>
 470    private static bool TryParseUrlSpec(string spec, out string host, out int port, out bool? https)
 471    {
 156472        host = ""; port = 0; https = null;
 473        // Fast rejection for an explicitly empty port (e.g. "https://localhost:" or "http://[::1]:")
 474        // Uri.TryCreate will happily parse these and supply the default scheme port (80/443),
 475        // which would make us treat an intentionally empty port as a valid implicit port.
 476        // The accepted formats require either no colon at all (implicit default) OR a colon followed by digits.
 477        // Therefore pattern: scheme:// host-part : end-of-string (no digits after colon) should be rejected.
 52478        if (EmptyPortDetectionRegex().IsMatch(spec))
 479        {
 2480            return false;
 481        }
 50482        if (!Uri.TryCreate(spec, UriKind.Absolute, out var uri))
 483        {
 22484            return false;
 485        }
 28486        if (!(uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase) ||
 28487              uri.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase)))
 488        {
 12489            return false; // Not http/https → let other parsers try
 490        }
 16491        if (uri.Authority.EndsWith(':'))
 492        {
 0493            return false; // reject empty port like https://localhost:
 494        }
 16495        host = uri.Host;
 16496        port = uri.Port;
 16497        https = uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)
 16498            ? true
 16499            : uri.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase)
 16500                ? false
 16501                : null;
 16502        return !string.IsNullOrWhiteSpace(host) && IsValidPort(port);
 503    }
 504
 505    /// <summary>
 506    /// Tries to parse a bracketed IPv6 endpoint specification.
 507    /// </summary>
 508    /// <param name="spec">The endpoint specification string.</param>
 509    /// <param name="host">The parsed host component.</param>
 510    /// <param name="port">The parsed port component.</param>
 511    /// <returns><c>true</c> if parsing succeeded; otherwise <c>false</c>.</returns>
 512    private static bool TryParseBracketedIpv6Spec(string spec, out string host, out int port)
 513    {
 72514        host = ""; port = 0;
 36515        var m = BracketedIpv6SpecMatcher().Match(spec);
 36516        if (!m.Success)
 517        {
 32518            return false;
 519        }
 4520        host = m.Groups[1].Value;
 4521        if (!int.TryParse(m.Groups[2].Value, out port) || !IsValidPort(port))
 522        {
 0523            host = ""; port = 0; return false;
 524        }
 4525        return !string.IsNullOrWhiteSpace(host);
 526    }
 527
 528    /// <summary>
 529    /// Tries to parse a host:port endpoint specification.
 530    /// </summary>
 531    /// <param name="spec">The endpoint specification string.</param>
 532    /// <param name="host">The parsed host component.</param>
 533    /// <param name="port">The parsed port component.</param>
 534    /// <returns><c>true</c> if parsing succeeded; otherwise <c>false</c>.</returns>
 535    private static bool TryParseHostPortSpec(string spec, out string host, out int port)
 536    {
 64537        host = ""; port = 0;
 32538        var m = HostPortSpecMatcher().Match(spec);
 32539        if (!m.Success)
 540        {
 17541            return false;
 542        }
 15543        host = m.Groups[1].Value;
 15544        if (!int.TryParse(m.Groups[2].Value, out port) || !IsValidPort(port))
 545        {
 9546            host = ""; port = 0; return false;
 547        }
 12548        return !string.IsNullOrWhiteSpace(host);
 549    }
 550    private const int MIN_PORT = 1;
 551    private const int MAX_PORT = 65535;
 552
 553    /// <summary>
 554    /// Validates that the port number is within the acceptable range (1-65535).
 555    /// </summary>
 556    /// <param name="port">The port number to validate.</param>
 557    /// <returns><c>true</c> if the port number is valid; otherwise, <c>false</c>.</returns>
 35558    private static bool IsValidPort(int port) => port is >= MIN_PORT and <= MAX_PORT;
 559
 560    /// <summary>
 561    /// Formats the host and port for use in RequireHost, adding brackets for IPv6 literals.
 562    /// </summary>
 563    /// <param name="host">The host component.</param>
 564    /// <param name="port">The port component.</param>
 565    /// <returns>The formatted host and port string.</returns>
 566    internal static string ToRequireHost(string host, int port) =>
 18567        IsIPv6Address(host) ? $"[{host}]:{port}" : $"{host}:{port}"; // IPv6 literals must be bracketed in RequireHost
 568
 569    /// <summary>
 570    /// Determines if the given host string is an IPv6 address.
 571    /// </summary>
 572    /// <param name="host">The host string to check.</param>
 573    /// <returns>True if the host is an IPv6 address; otherwise, false.</returns>
 18574    private static bool IsIPv6Address(string host) => IPAddress.TryParse(host, out var ip) && ip.AddressFamily == System
 575
 576    /// <summary>
 577    /// Applies required hosts to the route based on the specified endpoints in the options.
 578    /// </summary>
 579    /// <param name="host">The Kestrun host.</param>
 580    /// <param name="map">The endpoint convention builder.</param>
 581    /// <param name="options">The mapping options.</param>
 582    /// <exception cref="ArgumentException">Thrown when the specified endpoints are invalid.</exception>
 583    internal static void ApplyRequiredHost(this KestrunHost host, IEndpointConventionBuilder map, MapRouteOptions option
 584    {
 33585        if (options.Endpoints is not { Length: > 0 })
 586        {
 25587            return;
 588        }
 589
 8590        var listeners = host.Options.Listeners;
 8591        var require = new List<string>();
 8592        var errs = new List<string>();
 593
 38594        foreach (var spec in options.Endpoints)
 595        {
 11596            if (!TryParseEndpointSpec(spec, out var eh, out var ep, out var eHttps))
 597            {
 2598                errs.Add($"'{spec}' must be 'host:port' or 'http(s)://host:port'.");
 2599                continue;
 600            }
 601
 602            // Is the host a numeric IP?
 9603            var isNumericHost = IPAddress.TryParse(eh, out var endpointIp);
 604
 605            // Find a compatible listener: same port, scheme (if specified), and IP match if numeric host.
 9606            var match = listeners.FirstOrDefault(l =>
 19607                l.Port == ep &&
 19608                (eHttps is null || l.UseHttps == eHttps.Value) &&
 19609                (!isNumericHost ||
 19610                 l.IPAddress.Equals(endpointIp) ||
 19611                 l.IPAddress.Equals(IPAddress.Any) ||
 19612                 l.IPAddress.Equals(IPAddress.IPv6Any)));
 613
 9614            if (match is null)
 615            {
 2616                errs.Add($"'{spec}' doesn't match any configured listener. " +
 4617                         $"Known: {string.Join(", ", listeners.Select(l => l.ToString()))}");
 2618                continue;
 619            }
 620
 7621            require.Add(ToRequireHost(eh, ep));
 622        }
 623
 8624        if (errs.Count > 0)
 625        {
 3626            throw new InvalidOperationException("Invalid Endpoints:" + Environment.NewLine + "  - " + string.Join(Enviro
 627        }
 5628        if (require.Count > 0)
 629        {
 5630            host.Logger.Verbose("Applying required hosts: {RequiredHosts} to route: {Pattern}",
 5631                string.Join(", ", require), options.Pattern);
 5632            _ = map.RequireHost([.. require]);
 633        }
 5634    }
 635
 636    /// <summary>
 637    /// Applies the same route conventions used by the AddMapRoute helpers to an arbitrary endpoint.
 638    /// </summary>
 639    /// <param name="host">The Kestrun host used for validation (auth schemes/policies).</param>
 640    /// <param name="builder">The endpoint convention builder to decorate.</param>
 641    /// <param name="configure">Delegate to configure a fresh <see cref="MapRouteOptions"/> instance. Only applicable pr
 642    /// <remarks>
 643    /// This is useful when you map endpoints manually via <c>app.MapGet</c>/<c>MapPost</c> and still want consistent be
 644    /// (auth, CORS, rate limiting, antiforgery disable, OpenAPI metadata, short-circuiting) without re-implementing log
 645    /// Validation notes:
 646    ///  - Pattern, Code are ignored if not relevant.
 647    ///  - Authentication schemes and policies are validated against the host registry.
 648    ///  - OpenAPI metadata is applied only when non-empty.
 649    /// </remarks>
 650    /// <returns>The original <paramref name="builder"/> for fluent chaining.</returns>
 651    public static IEndpointConventionBuilder ApplyKestrunConventions(this KestrunHost host, IEndpointConventionBuilder b
 652    {
 1653        ArgumentNullException.ThrowIfNull(host);
 1654        ArgumentNullException.ThrowIfNull(builder);
 1655        ArgumentNullException.ThrowIfNull(configure);
 656
 657        // Start with an empty options record (only convention-related fields will matter)
 1658        var options = new MapRouteOptions
 1659        {
 1660            Pattern = string.Empty,
 1661            HttpVerbs = [],
 1662            ScriptCode = new LanguageOptions
 1663            {
 1664                Language = ScriptLanguage.Native,
 1665                Code = string.Empty
 1666            }
 1667        };
 1668        configure(options);
 669
 670        // Reuse internal helper (kept internal to avoid accidental misuse) for actual application
 1671        host.AddMapOptions(builder, options);
 1672        return builder;
 673    }
 674
 675    /// <summary>
 676    /// Applies short-circuiting behavior to the route.
 677    /// </summary>
 678    /// <param name="host">The Kestrun host.</param>
 679    /// <param name="map">The endpoint convention builder.</param>
 680    /// <param name="options">The mapping options.</param>
 681    private static void ApplyShortCircuit(KestrunHost host, IEndpointConventionBuilder map, MapRouteOptions options)
 682    {
 35683        if (!options.ShortCircuit)
 684        {
 35685            return;
 686        }
 687
 0688        host.Logger.Verbose("Short-circuiting route: {Pattern} with status code: {StatusCode}", options.Pattern, options
 0689        if (options.ShortCircuitStatusCode is null)
 690        {
 0691            throw new ArgumentException("ShortCircuitStatusCode must be set if ShortCircuit is true.", nameof(options.Sh
 692        }
 693
 0694        _ = map.ShortCircuit(options.ShortCircuitStatusCode);
 0695    }
 696
 697    /// <summary>
 698    /// Applies anonymous access behavior to the route.
 699    /// </summary>
 700    /// <param name="host">The Kestrun host.</param>
 701    /// <param name="map">The endpoint convention builder.</param>
 702    /// <param name="options">The mapping options.</param>
 703    private static void ApplyAnonymous(KestrunHost host, IEndpointConventionBuilder map, MapRouteOptions options)
 704    {
 35705        if (options.AllowAnonymous)
 706        {
 0707            host.Logger.Verbose("Allowing anonymous access for route: {Pattern}", options.Pattern);
 0708            _ = map.AllowAnonymous();
 709        }
 710        else
 711        {
 35712            host.Logger.Debug("No anonymous access allowed for route: {Pattern}", options.Pattern);
 713        }
 35714    }
 715
 716    /// <summary>
 717    /// Disables anti-forgery behavior to the route.
 718    /// </summary>
 719    /// <param name="host">The Kestrun host.</param>
 720    /// <param name="map">The endpoint convention builder.</param>
 721    /// <param name="options">The mapping options.</param>
 722    private static void DisableAntiforgery(KestrunHost host, IEndpointConventionBuilder map, MapRouteOptions options)
 723    {
 35724        if (!options.DisableAntiforgery)
 725        {
 35726            return;
 727        }
 728
 0729        _ = map.DisableAntiforgery();
 0730        host.Logger.Verbose("CSRF protection disabled for route: {Pattern}", options.Pattern);
 0731    }
 732
 733    /// <summary>
 734    /// Disables response compression for the route.
 735    /// </summary>
 736    /// <param name="host">The Kestrun host.</param>
 737    /// <param name="map">The endpoint convention builder.</param>
 738    /// <param name="options">The mapping options.</param>
 739    private static void DisableResponseCompression(KestrunHost host, IEndpointConventionBuilder map, MapRouteOptions opt
 740    {
 35741        if (!options.DisableResponseCompression)
 742        {
 34743            return;
 744        }
 745
 1746        _ = map.DisableResponseCompression();
 1747        host.Logger.Verbose("Response compression disabled for route: {Pattern}", options.Pattern);
 1748    }
 749    /// <summary>
 750    /// Applies rate limiting behavior to the route.
 751    /// </summary>
 752    /// <param name="host">The Kestrun host.</param>
 753    /// <param name="map">The endpoint convention builder.</param>
 754    /// <param name="options">The mapping options.</param>
 755    private static void ApplyRateLimiting(KestrunHost host, IEndpointConventionBuilder map, MapRouteOptions options)
 756    {
 35757        if (string.IsNullOrWhiteSpace(options.RateLimitPolicyName))
 758        {
 35759            return;
 760        }
 761
 0762        host.Logger.Verbose("Applying rate limit policy: {RateLimitPolicyName} to route: {Pattern}", options.RateLimitPo
 0763        _ = map.RequireRateLimiting(options.RateLimitPolicyName);
 0764    }
 765
 766    /// <summary>
 767    /// Applies authentication schemes to the route.
 768    /// </summary>
 769    /// <param name="host">The Kestrun host.</param>
 770    /// <param name="map">The endpoint convention builder.</param>
 771    /// <param name="options">The mapping options.</param>
 772    private static void ApplyAuthSchemes(KestrunHost host, IEndpointConventionBuilder map, MapRouteOptions options)
 773    {
 35774        if (options.RequireSchemes is { Length: > 0 })
 775        {
 7776            foreach (var schema in options.RequireSchemes)
 777            {
 2778                if (!host.HasAuthScheme(schema))
 779                {
 1780                    throw new ArgumentException($"Authentication scheme '{schema}' is not registered.", nameof(options.R
 781                }
 782            }
 1783            host.Logger.Verbose("Requiring authorization for route: {Pattern} with policies: {Policies}", options.Patter
 1784            _ = map.RequireAuthorization(new AuthorizeAttribute
 1785            {
 1786                AuthenticationSchemes = string.Join(',', options.RequireSchemes)
 1787            });
 788        }
 789        else
 790        {
 33791            host.Logger.Debug("No authorization required for route: {Pattern}", options.Pattern);
 792        }
 33793    }
 794
 795    /// <summary>
 796    /// Applies authorization policies to the route.
 797    /// </summary>
 798    /// <param name="host">The Kestrun host.</param>
 799    /// <param name="map">The endpoint convention builder.</param>
 800    /// <param name="options">The mapping options.</param>
 801    private static void ApplyPolicies(KestrunHost host, IEndpointConventionBuilder map, MapRouteOptions options)
 802    {
 34803        if (options.RequirePolicies is { Length: > 0 })
 804        {
 7805            foreach (var policy in options.RequirePolicies)
 806            {
 2807                if (!host.HasAuthPolicy(policy))
 808                {
 1809                    throw new ArgumentException($"Authorization policy '{policy}' is not registered.", nameof(options.Re
 810                }
 811            }
 1812            _ = map.RequireAuthorization(options.RequirePolicies);
 813        }
 814        else
 815        {
 32816            host.Logger.Debug("No authorization policies required for route: {Pattern}", options.Pattern);
 817        }
 32818    }
 819    /// <summary>
 820    /// Applies CORS behavior to the route.
 821    /// </summary>
 822    /// <param name="host">The Kestrun host.</param>
 823    /// <param name="map">The endpoint convention builder.</param>
 824    /// <param name="options">The mapping options.</param>
 825    private static void ApplyCors(KestrunHost host, IEndpointConventionBuilder map, MapRouteOptions options)
 826    {
 33827        if (!string.IsNullOrWhiteSpace(options.CorsPolicyName))
 828        {
 0829            host.Logger.Verbose("Applying CORS policy: {CorsPolicyName} to route: {Pattern}", options.CorsPolicyName, op
 0830            _ = map.RequireCors(options.CorsPolicyName);
 831        }
 832        else
 833        {
 33834            host.Logger.Debug("No CORS policy applied for route: {Pattern}", options.Pattern);
 835        }
 33836    }
 837
 838    /// <summary>
 839    /// Applies OpenAPI metadata to the route.
 840    /// </summary>
 841    /// <param name="host">The Kestrun host.</param>
 842    /// <param name="map">The endpoint convention builder.</param>
 843    /// <param name="options">The mapping options.</param>
 844    private static void ApplyOpenApiMetadata(KestrunHost host, IEndpointConventionBuilder map, MapRouteOptions options)
 845    {
 33846        if (!string.IsNullOrEmpty(options.OpenAPI.OperationId))
 847        {
 0848            host.Logger.Verbose("Adding OpenAPI metadata for route: {Pattern} with OperationId: {OperationId}", options.
 0849            _ = map.WithName(options.OpenAPI.OperationId);
 850        }
 851
 33852        if (!string.IsNullOrWhiteSpace(options.OpenAPI.Summary))
 853        {
 0854            host.Logger.Verbose("Adding OpenAPI summary for route: {Pattern} with Summary: {Summary}", options.Pattern, 
 0855            _ = map.WithSummary(options.OpenAPI.Summary);
 856        }
 857
 33858        if (!string.IsNullOrWhiteSpace(options.OpenAPI.Description))
 859        {
 0860            host.Logger.Verbose("Adding OpenAPI description for route: {Pattern} with Description: {Description}", optio
 0861            _ = map.WithDescription(options.OpenAPI.Description);
 862        }
 863
 33864        if (options.OpenAPI.Tags.Length > 0)
 865        {
 0866            host.Logger.Verbose("Adding OpenAPI tags for route: {Pattern} with Tags: {Tags}", options.Pattern, string.Jo
 0867            _ = map.WithTags(options.OpenAPI.Tags);
 868        }
 869
 33870        if (!string.IsNullOrWhiteSpace(options.OpenAPI.GroupName))
 871        {
 0872            host.Logger.Verbose("Adding OpenAPI group name for route: {Pattern} with GroupName: {GroupName}", options.Pa
 0873            _ = map.WithGroupName(options.OpenAPI.GroupName);
 874        }
 33875    }
 876
 877    /// <summary>
 878    /// Adds an HTML template route to the KestrunHost for the specified pattern and HTML file path.
 879    /// </summary>
 880    /// <param name="host">The KestrunHost instance.</param>
 881    /// <param name="pattern">The route pattern.</param>
 882    /// <param name="htmlFilePath">The path to the HTML template file.</param>
 883    /// <param name="requireSchemes">Optional array of authorization schemes required for the route.</param>
 884    /// <returns>An IEndpointConventionBuilder for further configuration.</returns>
 885    public static IEndpointConventionBuilder AddHtmlTemplateRoute(this KestrunHost host, string pattern, string htmlFile
 886    {
 0887        return host.AddHtmlTemplateRoute(new MapRouteOptions
 0888        {
 0889            Pattern = pattern,
 0890            HttpVerbs = [HttpVerb.Get],
 0891            RequireSchemes = requireSchemes ?? [] // No authorization by default
 0892        }, htmlFilePath);
 893    }
 894
 895    /// <summary>
 896    /// Adds an HTML template route to the KestrunHost using the specified MapRouteOptions and HTML file path.
 897    /// </summary>
 898    /// <param name="host">The KestrunHost instance.</param>
 899    /// <param name="options">The MapRouteOptions containing route configuration.</param>
 900    /// <param name="htmlFilePath">The path to the HTML template file.</param>
 901    /// <returns>An IEndpointConventionBuilder for further configuration.</returns>
 902    public static IEndpointConventionBuilder AddHtmlTemplateRoute(this KestrunHost host, MapRouteOptions options, string
 903    {
 3904        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 905        {
 2906            host.Logger.Debug("Adding HTML template route: {Pattern}", options.Pattern);
 907        }
 908
 3909        if (options.HttpVerbs.Count != 0 &&
 3910            (options.HttpVerbs.Count > 1 || options.HttpVerbs.First() != HttpVerb.Get))
 911        {
 1912            host.Logger.Error("HTML template routes only support GET requests. Provided HTTP verbs: {HttpVerbs}", string
 1913            throw new ArgumentException("HTML template routes only support GET requests.", nameof(options.HttpVerbs));
 914        }
 2915        if (string.IsNullOrWhiteSpace(htmlFilePath) || !File.Exists(htmlFilePath))
 916        {
 1917            host.Logger.Error("HTML file path is null, empty, or does not exist: {HtmlFilePath}", htmlFilePath);
 1918            throw new FileNotFoundException("HTML file not found.", htmlFilePath);
 919        }
 920
 1921        if (string.IsNullOrWhiteSpace(options.Pattern))
 922        {
 0923            host.Logger.Error("Pattern cannot be null or empty.");
 0924            throw new ArgumentException("Pattern cannot be null or empty.", nameof(options.Pattern));
 925        }
 926
 1927        _ = host.AddMapRoute(options.Pattern, HttpVerb.Get, async (ctx) =>
 1928          {
 1929              // ② Build your variables map
 0930              var vars = new Dictionary<string, object?>();
 0931              _ = VariablesMap.GetVariablesMap(ctx, ref vars);
 1932
 0933              await ctx.Response.WriteHtmlResponseFromFileAsync(htmlFilePath, vars, ctx.Response.StatusCode);
 1934          }, out var map);
 1935        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 936        {
 1937            host.Logger.Debug("Mapped HTML template route: {Pattern} to file: {HtmlFilePath}", options.Pattern, htmlFile
 938        }
 1939        if (map is null)
 940        {
 0941            throw new InvalidOperationException("Failed to create HTML template route.");
 942        }
 1943        AddMapOptions(host, map, options);
 1944        return map;
 945    }
 946
 947    /// <summary>
 948    /// Checks if a route with the specified pattern and optional HTTP method exists in the KestrunHost.
 949    /// </summary>
 950    /// <param name="host">The KestrunHost instance.</param>
 951    /// <param name="pattern">The route pattern to check.</param>
 952    /// <param name="verbs">The optional HTTP method to check for the route.</param>
 953    /// <returns>True if the route exists; otherwise, false.</returns>
 954    public static bool MapExists(this KestrunHost host, string pattern, IEnumerable<HttpVerb> verbs)
 955    {
 80956        var methodSet = verbs.Select(v => v.ToMethodString()).ToHashSet(StringComparer.OrdinalIgnoreCase);
 39957        return host._registeredRoutes.Keys
 11958            .Where(k => string.Equals(k.Pattern, pattern, StringComparison.OrdinalIgnoreCase))
 48959            .Any(k => methodSet.Contains(k.Method));
 960    }
 961
 962    /// <summary>
 963    /// Checks if a route with the specified pattern and optional HTTP method exists in the KestrunHost.
 964    /// </summary>
 965    /// <param name="host">The KestrunHost instance.</param>
 966    /// <param name="pattern">The route pattern to check.</param>
 967    /// <param name="verb">The optional HTTP method to check for the route.</param>
 968    /// <returns>True if the route exists; otherwise, false.</returns>
 969    public static bool MapExists(this KestrunHost host, string pattern, HttpVerb verb) =>
 9970        host._registeredRoutes.ContainsKey((pattern, verb.ToMethodString()));
 971
 972
 973    /// <summary>
 974    /// Retrieves the <see cref="MapRouteOptions"/> associated with a given route pattern and HTTP verb, if registered.
 975    /// </summary>
 976    /// <param name="host">The <see cref="KestrunHost"/> instance to search for registered routes.</param>
 977    /// <param name="pattern">The route pattern to look up (e.g. <c>"/hello"</c>).</param>
 978    /// <param name="verb">The HTTP verb to match (e.g. <see cref="HttpVerb.Get"/>).</param>
 979    /// <returns>
 980    /// The <see cref="MapRouteOptions"/> instance for the specified route if found; otherwise, <c>null</c>.
 981    /// </returns>
 982    /// <remarks>
 983    /// This method checks the internal route registry and returns the route options if the pattern and verb
 984    /// combination was previously added via <c>AddMapRoute</c>.
 985    /// This lookup is case-insensitive for both the pattern and method.
 986    /// </remarks>
 987    /// <example>
 988    /// <code>
 989    /// var options = host.GetMapRouteOptions("/hello", HttpVerb.Get);
 990    /// if (options != null)
 991    /// {
 992    ///     Console.WriteLine($"Route language: {options.Language}");
 993    /// }
 994    /// </code>
 995    /// </example>
 996    public static MapRouteOptions? GetMapRouteOptions(this KestrunHost host, string pattern, HttpVerb verb)
 997    {
 4998        return host._registeredRoutes.TryGetValue((pattern, verb.ToMethodString()), out var options)
 4999            ? options
 41000            : null;
 1001    }
 1002
 1003    /// <summary>
 1004    /// Adds a GET endpoint that issues the antiforgery cookie and returns a JSON payload:
 1005    /// { token: "...", headerName: "X-CSRF-TOKEN" }.
 1006    /// The endpoint itself is exempt from antiforgery validation.
 1007    /// </summary>
 1008    /// <param name="host">The KestrunHost instance.</param>
 1009    /// <param name="pattern">The route path to expose (default "/csrf-token").</param>
 1010    /// <returns>IEndpointConventionBuilder for further configuration.</returns>
 1011    public static IEndpointConventionBuilder AddAntiforgeryTokenRoute(
 1012    this KestrunHost host,
 1013    string pattern = "/csrf-token")
 1014    {
 01015        ArgumentException.ThrowIfNullOrWhiteSpace(pattern);
 01016        if (host.App is null)
 1017        {
 01018            throw new InvalidOperationException("WebApplication is not initialized. Call EnableConfiguration first.");
 1019        }
 01020        var options = new MapRouteOptions
 01021        {
 01022            Pattern = pattern,
 01023            HttpVerbs = [HttpVerb.Get],
 01024            ScriptCode = new LanguageOptions
 01025            {
 01026                Language = ScriptLanguage.Native
 01027            },
 01028            DisableAntiforgery = true,
 01029            AllowAnonymous = true,
 01030        };
 1031        // OpenAPI = new() { Summary = "Get CSRF token", Description = "Returns antiforgery request token and header nam
 1032
 1033        // Map directly and write directly (no KestrunResponse.ApplyTo)
 01034        var map = host.App.MapMethods(options.Pattern, [HttpMethods.Get], async context =>
 01035        {
 01036            var af = context.RequestServices.GetRequiredService<IAntiforgery>();
 01037            var opts = context.RequestServices.GetRequiredService<IOptions<AntiforgeryOptions>>();
 01038
 01039            var tokens = af.GetAndStoreTokens(context);
 01040
 01041            // Strongly discourage caches (proxies/browsers) from storing this payload
 01042            context.Response.Headers.CacheControl = "no-store, no-cache, must-revalidate";
 01043            context.Response.Headers.Pragma = "no-cache";
 01044            context.Response.Headers.Expires = "0";
 01045
 01046            context.Response.ContentType = "application/json";
 01047            await context.Response.WriteAsJsonAsync(new
 01048            {
 01049                token = tokens.RequestToken,
 01050                headerName = opts.Value.HeaderName // may be null if not configured
 01051            });
 01052        });
 1053
 1054        // Apply your pipeline metadata (this adds DisableAntiforgery, CORS, rate limiting, OpenAPI, etc.)
 01055        host.AddMapOptions(map, options);
 1056
 1057        // (Optional) track in your registry for consistency / duplicate checks
 01058        host._registeredRoutes[(options.Pattern, HttpMethods.Get)] = options;
 1059
 01060        host.Logger.Information("Added token endpoint: {Pattern} (GET)", options.Pattern);
 01061        return map;
 1062    }
 1063
 1064    private static bool IsUnsafeVerb(HttpVerb v)
 41065        => v is HttpVerb.Post or HttpVerb.Put or HttpVerb.Patch or HttpVerb.Delete;
 1066
 1067    private static bool IsUnsafeMethod(string method)
 191068        => HttpMethods.IsPost(method) || HttpMethods.IsPut(method) || HttpMethods.IsPatch(method) || HttpMethods.IsDelet
 1069
 1070    // New precise helper: only validate for the actual incoming request method when that method is unsafe and antiforge
 1071    private static bool ShouldValidateCsrf(MapRouteOptions o, HttpContext ctx)
 1072    {
 201073        if (o.DisableAntiforgery)
 1074        {
 11075            return false;
 1076        }
 191077        if (!IsUnsafeMethod(ctx.Request.Method))
 1078        {
 141079            return false; // Safe verb (GET/HEAD/OPTIONS) -> skip
 1080        }
 1081        // Ensure the route was actually configured for this unsafe verb (defensive; normally true inside mapped delegat
 171082        return o.HttpVerbs.Any(v => string.Equals(v.ToMethodString(), ctx.Request.Method, StringComparison.OrdinalIgnore
 1083    }
 1084
 1085    private static async Task<bool> TryValidateAntiforgeryAsync(HttpContext ctx)
 1086    {
 01087        var af = ctx.RequestServices.GetService<IAntiforgery>();
 01088        if (af is null)
 1089        {
 01090            return true; // antiforgery not configured → do nothing
 1091        }
 1092
 1093        try
 1094        {
 01095            await af.ValidateRequestAsync(ctx);
 01096            return true;
 1097        }
 01098        catch (AntiforgeryValidationException ex)
 1099        {
 1100            // short-circuit with RFC 9110 problem+json
 01101            ctx.Response.StatusCode = StatusCodes.Status400BadRequest;
 01102            ctx.Response.ContentType = "application/problem+json";
 01103            await ctx.Response.WriteAsJsonAsync(new
 01104            {
 01105                type = "https://datatracker.ietf.org/doc/html/rfc9110#section-15.5.1",
 01106                title = "Antiforgery validation failed",
 01107                status = 400,
 01108                detail = ex.Message
 01109            });
 01110            return false;
 1111        }
 01112    }
 1113
 1114    /// <summary>
 1115    /// Matches a bracketed IPv6 host:port specification in the format "[ipv6]:port", where:
 1116    /// - ipv6 is a valid IPv6 address (e.g. "::1", "2001:0db8:85a3:0000:0000:8a2e:0370:7334")
 1117    /// - port is a numeric value between 1 and 65535
 1118    /// Examples of valid inputs:
 1119    ///   "[::1]:80"
 1120    ///   "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:443"
 1121    /// </summary>
 1122    [GeneratedRegex(@"^\[([^\]]+)\]:(\d+)$")]
 1123    private static partial Regex BracketedIpv6SpecMatcher();
 1124
 1125    /// <summary>
 1126    /// Matches a host:port specification in the format "host:port", where:
 1127    /// - host can be any string excluding ':' (to avoid confusion with IPv6 addresses)
 1128    /// - port is a numeric value between 1 and 65535
 1129    /// Examples of valid inputs:
 1130    ///   "example.com:80"
 1131    ///   "localhost:443"
 1132    ///   "[::1]:8080"  (IPv6 address in brackets)
 1133    /// </summary>
 1134    [GeneratedRegex(@"^([^:]+):(\d+)$")]
 1135    private static partial Regex HostPortSpecMatcher();
 1136
 1137
 1138    /// <summary>
 1139    /// Matches a URL that starts with "http://" or "https://", followed by a host (excluding '/', '?', or '#'), and end
 1140    /// Examples of valid inputs:
 1141    ///   "http://example.com:"
 1142    ///   "https://localhost:"
 1143    ///   "https://my-server:8080:"
 1144    /// </summary>
 1145    [GeneratedRegex(@"^https?://[^/\?#]+:$", RegexOptions.IgnoreCase, "en-US")]
 1146    private static partial Regex EmptyPortDetectionRegex();
 1147}
 1148

Methods/Properties

TryParse(System.String,System.String&,System.Int32&,System.Nullable`1<System.Boolean>&)
AddMapRoute(Kestrun.Hosting.KestrunHost,System.String,Kestrun.Utilities.HttpVerb,Kestrun.Hosting.KestrunHostMapExtensions/KestrunHandler,Microsoft.AspNetCore.Builder.IEndpointConventionBuilder&,System.String[])
AddMapRoute(Kestrun.Hosting.KestrunHost,System.String,System.Collections.Generic.IEnumerable`1<Kestrun.Utilities.HttpVerb>,Kestrun.Hosting.KestrunHostMapExtensions/KestrunHandler,Microsoft.AspNetCore.Builder.IEndpointConventionBuilder&,System.String[])
AddMapRoute(Kestrun.Hosting.KestrunHost,Kestrun.Hosting.Options.MapRouteOptions,Kestrun.Hosting.KestrunHostMapExtensions/KestrunHandler,Microsoft.AspNetCore.Builder.IEndpointConventionBuilder&)
AddMapRoute(Kestrun.Hosting.KestrunHost,System.String,Kestrun.Utilities.HttpVerb,System.String,Kestrun.Scripting.ScriptLanguage,System.String[],System.Collections.Generic.Dictionary`2<System.String,System.Object>)
AddMapRoute(Kestrun.Hosting.KestrunHost,System.String,System.Collections.Generic.IEnumerable`1<Kestrun.Utilities.HttpVerb>,System.String,Kestrun.Scripting.ScriptLanguage,System.String[],System.Collections.Generic.Dictionary`2<System.String,System.Object>)
AddMapRoute(Kestrun.Hosting.KestrunHost,Kestrun.Hosting.Options.MapRouteOptions)
CreateMapRoute(Kestrun.Hosting.KestrunHost,Kestrun.Hosting.Options.MapRouteOptions)
ValidateRouteOptions(Kestrun.Hosting.KestrunHost,Kestrun.Hosting.Options.MapRouteOptions,Kestrun.Hosting.Options.MapRouteOptions&)
CompileScript(Kestrun.Hosting.KestrunHost,Kestrun.Hosting.Options.LanguageOptions,Serilog.ILogger)
handler()
CreateAndRegisterRoute(Kestrun.Hosting.KestrunHost,Kestrun.Hosting.Options.MapRouteOptions,Microsoft.AspNetCore.Http.RequestDelegate)
AddMapOptions(Kestrun.Hosting.KestrunHost,Microsoft.AspNetCore.Builder.IEndpointConventionBuilder,Kestrun.Hosting.Options.MapRouteOptions)
TryParseEndpointSpec(System.String,System.String&,System.Int32&,System.Nullable`1<System.Boolean>&)
TryParseUrlSpec(System.String,System.String&,System.Int32&,System.Nullable`1<System.Boolean>&)
TryParseBracketedIpv6Spec(System.String,System.String&,System.Int32&)
TryParseHostPortSpec(System.String,System.String&,System.Int32&)
IsValidPort(System.Int32)
ToRequireHost(System.String,System.Int32)
IsIPv6Address(System.String)
ApplyRequiredHost(Kestrun.Hosting.KestrunHost,Microsoft.AspNetCore.Builder.IEndpointConventionBuilder,Kestrun.Hosting.Options.MapRouteOptions)
ApplyKestrunConventions(Kestrun.Hosting.KestrunHost,Microsoft.AspNetCore.Builder.IEndpointConventionBuilder,System.Action`1<Kestrun.Hosting.Options.MapRouteOptions>)
ApplyShortCircuit(Kestrun.Hosting.KestrunHost,Microsoft.AspNetCore.Builder.IEndpointConventionBuilder,Kestrun.Hosting.Options.MapRouteOptions)
ApplyAnonymous(Kestrun.Hosting.KestrunHost,Microsoft.AspNetCore.Builder.IEndpointConventionBuilder,Kestrun.Hosting.Options.MapRouteOptions)
DisableAntiforgery(Kestrun.Hosting.KestrunHost,Microsoft.AspNetCore.Builder.IEndpointConventionBuilder,Kestrun.Hosting.Options.MapRouteOptions)
DisableResponseCompression(Kestrun.Hosting.KestrunHost,Microsoft.AspNetCore.Builder.IEndpointConventionBuilder,Kestrun.Hosting.Options.MapRouteOptions)
ApplyRateLimiting(Kestrun.Hosting.KestrunHost,Microsoft.AspNetCore.Builder.IEndpointConventionBuilder,Kestrun.Hosting.Options.MapRouteOptions)
ApplyAuthSchemes(Kestrun.Hosting.KestrunHost,Microsoft.AspNetCore.Builder.IEndpointConventionBuilder,Kestrun.Hosting.Options.MapRouteOptions)
ApplyPolicies(Kestrun.Hosting.KestrunHost,Microsoft.AspNetCore.Builder.IEndpointConventionBuilder,Kestrun.Hosting.Options.MapRouteOptions)
ApplyCors(Kestrun.Hosting.KestrunHost,Microsoft.AspNetCore.Builder.IEndpointConventionBuilder,Kestrun.Hosting.Options.MapRouteOptions)
ApplyOpenApiMetadata(Kestrun.Hosting.KestrunHost,Microsoft.AspNetCore.Builder.IEndpointConventionBuilder,Kestrun.Hosting.Options.MapRouteOptions)
AddHtmlTemplateRoute(Kestrun.Hosting.KestrunHost,System.String,System.String,System.String[])
AddHtmlTemplateRoute(Kestrun.Hosting.KestrunHost,Kestrun.Hosting.Options.MapRouteOptions,System.String)
MapExists(Kestrun.Hosting.KestrunHost,System.String,System.Collections.Generic.IEnumerable`1<Kestrun.Utilities.HttpVerb>)
MapExists(Kestrun.Hosting.KestrunHost,System.String,Kestrun.Utilities.HttpVerb)
GetMapRouteOptions(Kestrun.Hosting.KestrunHost,System.String,Kestrun.Utilities.HttpVerb)
AddAntiforgeryTokenRoute(Kestrun.Hosting.KestrunHost,System.String)
IsUnsafeVerb(Kestrun.Utilities.HttpVerb)
IsUnsafeMethod(System.String)
ShouldValidateCsrf(Kestrun.Hosting.Options.MapRouteOptions,Microsoft.AspNetCore.Http.HttpContext)
TryValidateAntiforgeryAsync()