< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.Models.KestrunResponse
Assembly: Kestrun
File(s): /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Models/KestrunResponse.cs
Tag: Kestrun/Kestrun@ca54e35c77799b76774b3805b6f075cdbc0c5fbe
Line coverage
82%
Covered lines: 487
Uncovered lines: 103
Coverable lines: 590
Total lines: 1620
Line coverage: 82.5%
Branch coverage
72%
Covered branches: 257
Total branches: 354
Branch coverage: 72.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 09/08/2025 - 20:34:03 Line coverage: 89.3% (404/452) Branch coverage: 73.8% (192/260) Total lines: 1231 Tag: Kestrun/Kestrun@3790ee5884494a7a2a829344a47743e0bf492e7209/13/2025 - 17:19:56 Line coverage: 83.4% (404/484) Branch coverage: 66.2% (192/290) Total lines: 1304 Tag: Kestrun/Kestrun@ea635f1ee1937c260a89d1a43a3c203cd8767c7b09/14/2025 - 21:23:16 Line coverage: 88.3% (409/463) Branch coverage: 72.9% (194/266) Total lines: 1279 Tag: Kestrun/Kestrun@c9d2f0b3dd164d7dc0dc2407a9f006293d92422309/16/2025 - 16:28:42 Line coverage: 88.1% (410/465) Branch coverage: 71.6% (195/272) Total lines: 1282 Tag: Kestrun/Kestrun@d5c0d6132e97ca542441289c02a4c9e9d0364d4910/13/2025 - 16:52:37 Line coverage: 86.5% (425/491) Branch coverage: 71% (213/300) Total lines: 1347 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e12/12/2025 - 17:27:19 Line coverage: 84.7% (433/511) Branch coverage: 68.7% (216/314) Total lines: 1423 Tag: Kestrun/Kestrun@826bf9dcf9db118c5de4c78a3259bce9549f0dcd12/18/2025 - 21:41:58 Line coverage: 84% (438/521) Branch coverage: 68.6% (217/316) Total lines: 1449 Tag: Kestrun/Kestrun@0d738bf294e6281b936d031e1979d928007495ff01/02/2026 - 00:16:25 Line coverage: 78.1% (450/576) Branch coverage: 65% (221/340) Total lines: 1578 Tag: Kestrun/Kestrun@8405dc23b786b9d436fba0d65fb80baa4171e1d001/12/2026 - 18:03:06 Line coverage: 80.9% (466/576) Branch coverage: 67.6% (230/340) Total lines: 1578 Tag: Kestrun/Kestrun@956332ccc921363590dccd99d5707fb20b50966b01/15/2026 - 23:50:39 Line coverage: 80.9% (468/578) Branch coverage: 67.4% (228/338) Total lines: 1583 Tag: Kestrun/Kestrun@2d823cb7ceae127151c8880ca073ffbb9c6322aa01/17/2026 - 04:33:35 Line coverage: 82.5% (487/590) Branch coverage: 72.5% (257/354) Total lines: 1620 Tag: Kestrun/Kestrun@aca34ea8d284564e2f9f6616dc937668dce926ba 09/08/2025 - 20:34:03 Line coverage: 89.3% (404/452) Branch coverage: 73.8% (192/260) Total lines: 1231 Tag: Kestrun/Kestrun@3790ee5884494a7a2a829344a47743e0bf492e7209/13/2025 - 17:19:56 Line coverage: 83.4% (404/484) Branch coverage: 66.2% (192/290) Total lines: 1304 Tag: Kestrun/Kestrun@ea635f1ee1937c260a89d1a43a3c203cd8767c7b09/14/2025 - 21:23:16 Line coverage: 88.3% (409/463) Branch coverage: 72.9% (194/266) Total lines: 1279 Tag: Kestrun/Kestrun@c9d2f0b3dd164d7dc0dc2407a9f006293d92422309/16/2025 - 16:28:42 Line coverage: 88.1% (410/465) Branch coverage: 71.6% (195/272) Total lines: 1282 Tag: Kestrun/Kestrun@d5c0d6132e97ca542441289c02a4c9e9d0364d4910/13/2025 - 16:52:37 Line coverage: 86.5% (425/491) Branch coverage: 71% (213/300) Total lines: 1347 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e12/12/2025 - 17:27:19 Line coverage: 84.7% (433/511) Branch coverage: 68.7% (216/314) Total lines: 1423 Tag: Kestrun/Kestrun@826bf9dcf9db118c5de4c78a3259bce9549f0dcd12/18/2025 - 21:41:58 Line coverage: 84% (438/521) Branch coverage: 68.6% (217/316) Total lines: 1449 Tag: Kestrun/Kestrun@0d738bf294e6281b936d031e1979d928007495ff01/02/2026 - 00:16:25 Line coverage: 78.1% (450/576) Branch coverage: 65% (221/340) Total lines: 1578 Tag: Kestrun/Kestrun@8405dc23b786b9d436fba0d65fb80baa4171e1d001/12/2026 - 18:03:06 Line coverage: 80.9% (466/576) Branch coverage: 67.6% (230/340) Total lines: 1578 Tag: Kestrun/Kestrun@956332ccc921363590dccd99d5707fb20b50966b01/15/2026 - 23:50:39 Line coverage: 80.9% (468/578) Branch coverage: 67.4% (228/338) Total lines: 1583 Tag: Kestrun/Kestrun@2d823cb7ceae127151c8880ca073ffbb9c6322aa01/17/2026 - 04:33:35 Line coverage: 82.5% (487/590) Branch coverage: 72.5% (257/354) Total lines: 1620 Tag: Kestrun/Kestrun@aca34ea8d284564e2f9f6616dc937668dce926ba

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_CallbackPlan()100%11100%
get_Logger()100%210%
get_MapRouteOptions()100%210%
get_KrContext()100%11100%
get_Host()100%210%
.cctor()100%11100%
.ctor(...)75%44100%
get_Context()100%210%
get_StatusCode()100%11100%
get_Headers()100%11100%
get_ContentType()100%11100%
get_Body()100%11100%
get_RedirectUrl()100%11100%
get_Cookies()100%11100%
get_Encoding()100%11100%
get_ContentDisposition()100%11100%
get_Request()100%11100%
get_AcceptCharset()100%11100%
get_BodyAsyncThreshold()100%11100%
get_CacheControl()100%11100%
GetSafeCurrentDirectoryOrBaseDirectory()100%1128.57%
GetSafeCurrentDirectoryForLogging()100%11100%
GetHeader(...)0%620%
IsTextBasedContentType(...)93.75%161692.85%
AddCallbackParameters(...)0%4260%
WriteFileResponse(...)75%161696.55%
WriteJsonResponse(...)100%11100%
WriteJsonResponseAsync()100%11100%
WriteJsonResponse(...)100%210%
WriteJsonResponseAsync()100%44100%
WriteJsonResponse(...)100%11100%
WriteJsonResponseAsync()100%22100%
WriteCborResponseAsync()75%44100%
WriteCborResponse(...)100%210%
WriteBsonResponseAsync()75%44100%
WriteBsonResponse(...)100%210%
WriteResponse(...)100%210%
WriteResponseAsync()62.5%12861.11%
SelectResponseMediaType(...)75%8884.61%
GetMediaTypeOrNull(...)50%4475%
WriteByMediaTypeAsync(...)60%1564058.33%
WriteCsvResponse(...)100%88100%
WriteCsvResponseAsync()100%44100%
WriteYamlResponse(...)100%11100%
WriteYamlResponseAsync()75%44100%
WriteXmlResponse(...)100%11100%
WriteXmlResponseAsync()87.5%88100%
WriteTextResponse(...)100%11100%
WriteTextResponseAsync()83.33%6685.71%
WriteFormUrlEncodedResponse(...)100%11100%
WriteFormUrlEncodedResponseAsync()100%22100%
WriteRedirectResponse(...)83.33%6692.3%
WriteBinaryResponse(...)75%44100%
WriteStreamResponse(...)100%22100%
get_Error()100%11100%
get_Details()100%11100%
get_Exception()100%11100%
get_StackTrace()100%11100%
get_Status()100%11100%
get_Reason()100%11100%
get_Timestamp()100%11100%
get_Path()100%11100%
get_Method()100%11100%
WriteErrorResponseAsync()62.5%8895%
WriteErrorResponse(...)100%11100%
WriteErrorResponseAsync()75%88100%
WriteErrorResponse(...)100%11100%
WriteFormattedErrorResponseAsync()100%2020100%
RenderInlineTemplate(...)64.28%151481.81%
RenderInline(...)85.71%141493.75%
TryResolveValue(...)56.25%271665.21%
RevalidateCache(...)100%210%
WriteHtmlResponseAsync()66.66%6683.33%
WriteHtmlResponseAsync()100%210%
WriteHtmlResponse(...)100%210%
WriteHtmlResponseFromFileAsync()75%4475%
WriteHtmlResponse(...)100%210%
WriteHtmlResponseFromFile(...)100%210%
WriteStatusOnly(...)100%11100%
ApplyTo()81.25%191678.57%
TryEnqueueCallbacks()42.85%561440%
EnsureContentType(...)100%88100%
EnsureStatus(...)100%22100%
ApplyCachingHeaders(...)100%22100%
ApplyContentDispositionHeader(...)78.57%141488.88%
ApplyHeadersAndCookies(...)87.5%8885.71%
WriteBodyAsync()59.09%242285.36%

File(s)

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Models/KestrunResponse.cs

#LineLine coverage
 1
 2using System.Diagnostics.CodeAnalysis;
 3using System.Xml.Linq;
 4using System.Text.Json;
 5using System.Text.Json.Serialization;
 6using Kestrun.Utilities.Json;
 7using Microsoft.AspNetCore.StaticFiles;
 8using System.Text;
 9using Serilog;
 10using Serilog.Events;
 11using System.Buffers;
 12using Microsoft.Extensions.FileProviders;
 13using Microsoft.AspNetCore.WebUtilities;
 14using System.Net;
 15using MongoDB.Bson;
 16using Kestrun.Utilities;
 17using System.Collections;
 18using CsvHelper.Configuration;
 19using System.Globalization;
 20using CsvHelper;
 21using System.Reflection;
 22using Microsoft.Net.Http.Headers;
 23using Kestrun.Utilities.Yaml;
 24using Kestrun.Hosting.Options;
 25using Kestrun.Callback;
 26
 27namespace Kestrun.Models;
 28
 29/// <summary>
 30/// Represents an HTTP response in the Kestrun framework, providing methods to write various content types and manage he
 31/// </summary>
 32/// <remarks>
 33/// Initializes a new instance of the <see cref="KestrunResponse"/> class with the specified request and optional body a
 34/// </remarks>
 35public class KestrunResponse
 36{
 37    /// <summary>
 38    /// Flag indicating whether callbacks have already been enqueued.
 39    /// </summary>
 40    internal int CallbacksEnqueuedFlag; // 0 = no, 1 = yes
 41
 42    /// <summary>
 43    ///     Gets the list of callback requests associated with this response.
 44    /// </summary>
 17845    public List<CallBackExecutionPlan> CallbackPlan { get; } = [];
 46
 047    private Serilog.ILogger Logger => KrContext.Host.Logger;
 48    /// <summary>
 49    /// Gets the route options associated with this response.
 50    /// </summary>
 051    public MapRouteOptions MapRouteOptions => KrContext.MapRouteOptions;
 52    /// <summary>
 53    /// Gets the associated KestrunContext for this response.
 54    /// </summary>
 59155    public required KestrunContext KrContext { get; init; }
 56
 57    /// <summary>
 58    /// Gets the KestrunHost associated with this response.
 59    /// </summary>
 060    public Hosting.KestrunHost Host => KrContext.Host;
 61    /// <summary>
 62    /// A set of MIME types that are considered text-based for response content.
 63    /// </summary>
 164    public static readonly HashSet<string> TextBasedMimeTypes =
 165    new(StringComparer.OrdinalIgnoreCase)
 166    {
 167        "application/json",
 168        "application/xml",
 169        "application/javascript",
 170        "application/xhtml+xml",
 171        "application/x-www-form-urlencoded",
 172        "application/yaml",
 173        "application/graphql"
 174    };
 75
 76    /// <summary>
 77    /// Initializes a new instance of the <see cref="KestrunResponse"/> class.
 78    /// </summary>
 79    /// <param name="krContext">The associated <see cref="KestrunContext"/> for this response.</param>
 80    /// <param name="bodyAsyncThreshold">The threshold in bytes for using async body write operations. Defaults to 8192.
 81    [SetsRequiredMembers]
 14682    public KestrunResponse(KestrunContext krContext, int bodyAsyncThreshold = 8192)
 83    {
 14684        KrContext = krContext;
 14685        BodyAsyncThreshold = bodyAsyncThreshold;
 14686        Request = KrContext.Request ?? throw new ArgumentNullException(nameof(KrContext));
 14687        AcceptCharset = KrContext.Request.Headers.TryGetValue("Accept-Charset", out var value) ? Encoding.GetEncoding(va
 14688        StatusCode = KrContext.HttpContext.Response.StatusCode;
 14689    }
 90
 91    /// <summary>
 92    /// Gets the <see cref="HttpContext"/> associated with the response.
 93    /// </summary>
 094    public HttpContext Context => KrContext.HttpContext;
 95    /// <summary>
 96    /// Gets or sets the HTTP status code for the response.
 97    /// </summary>
 30998    public int StatusCode { get; set; }
 99    /// <summary>
 100    /// Gets or sets the collection of HTTP headers for the response.
 101    /// </summary>
 216102    public Dictionary<string, string> Headers { get; set; } = [];
 103    /// <summary>
 104    /// Gets or sets the MIME content type of the response.
 105    /// </summary>
 417106    public string? ContentType { get; set; } = "text/plain";
 107    /// <summary>
 108    /// Gets or sets the body of the response, which can be a string, byte array, stream, or file info.
 109    /// </summary>
 231110    public object? Body { get; set; }
 111    /// <summary>
 112    /// Gets or sets the URL to redirect the response to, if an HTTP redirect is required.
 113    /// </summary>
 61114    public string? RedirectUrl { get; set; } // For HTTP redirects
 115    /// <summary>
 116    /// Gets or sets the list of Set-Cookie header values for the response.
 117    /// </summary>
 31118    public List<string>? Cookies { get; set; } // For Set-Cookie headers
 119
 120    /// <summary>
 121    /// Text encoding for textual MIME types.
 122    /// </summary>
 187123    public Encoding Encoding { get; set; } = Encoding.UTF8;
 124
 125    /// <summary>
 126    /// Content-Disposition header value.
 127    /// </summary>
 186128    public ContentDispositionOptions ContentDisposition { get; set; } = new ContentDispositionOptions();
 129    /// <summary>
 130    /// Gets the associated KestrunRequest for this response.
 131    /// </summary>
 188132    public required KestrunRequest Request { get; init; }
 133
 134    /// <summary>
 135    /// Global text encoding for all responses. Defaults to UTF-8.
 136    /// </summary>
 173137    public required Encoding AcceptCharset { get; init; }
 138
 139    /// <summary>
 140    /// If the response body is larger than this threshold (in bytes), async write will be used.
 141    /// </summary>
 146142    public required int BodyAsyncThreshold { get; init; }
 143
 144    /// <summary>
 145    /// Cache-Control header value for the response.
 146    /// </summary>
 31147    public CacheControlHeaderValue? CacheControl { get; set; }
 148
 149    #region Constructors
 150    #endregion
 151
 152    #region Helpers
 153    private static string GetSafeCurrentDirectoryOrBaseDirectory()
 154    {
 155        try
 156        {
 2157            return Directory.GetCurrentDirectory();
 158        }
 0159        catch (Exception ex) when (ex is IOException
 0160                                   or UnauthorizedAccessException
 0161                                   or DirectoryNotFoundException
 0162                                   or FileNotFoundException)
 163        {
 164            // On Unix/macOS, getcwd() can throw if the process CWD was deleted.
 165            // We use AppContext.BaseDirectory as a stable fallback to avoid crashing in diagnostics
 166            // and when resolving relative paths.
 0167            return AppContext.BaseDirectory;
 168        }
 2169    }
 170
 2171    private static string GetSafeCurrentDirectoryForLogging() => GetSafeCurrentDirectoryOrBaseDirectory();
 172
 173    /// <summary>
 174    /// Retrieves the value of the specified header from the response headers.
 175    /// </summary>
 176    /// <param name="key">The name of the header to retrieve.</param>
 177    /// <returns>The value of the header if found; otherwise, null.</returns>
 0178    public string? GetHeader(string key) => Headers.TryGetValue(key, out var value) ? value : null;
 179
 180    /// <summary>
 181    /// Determines whether the specified content type is text-based or supports a charset.
 182    /// </summary>
 183    /// <param name="type">The MIME content type to check.</param>
 184    /// <returns>True if the content type is text-based; otherwise, false.</returns>
 185    public static bool IsTextBasedContentType(string type)
 186    {
 34187        if (Log.IsEnabled(LogEventLevel.Debug))
 188        {
 33189            Log.Debug("Checking if content type is text-based: {ContentType}", type);
 190        }
 191
 192        // Check if the content type is text-based or has a charset
 34193        if (string.IsNullOrEmpty(type))
 194        {
 1195            return false;
 196        }
 197
 33198        if (type.StartsWith("text/", StringComparison.OrdinalIgnoreCase))
 199        {
 20200            return true;
 201        }
 13202        if (type == "application/x-www-form-urlencoded")
 203        {
 0204            return true;
 205        }
 206
 207        // Include structured types using XML or JSON suffixes
 13208        if (type.EndsWith("xml", StringComparison.OrdinalIgnoreCase) ||
 13209            type.EndsWith("json", StringComparison.OrdinalIgnoreCase) ||
 13210            type.EndsWith("yaml", StringComparison.OrdinalIgnoreCase) ||
 13211            type.EndsWith("csv", StringComparison.OrdinalIgnoreCase))
 212        {
 4213            return true;
 214        }
 215
 216        // Common application types where charset makes sense
 9217        return TextBasedMimeTypes.Contains(type);
 218    }
 219    /// <summary>
 220    /// Adds callback parameters for the specified callback ID, body, and parameters.
 221    /// </summary>
 222    /// <param name="callbackId">The identifier for the callback</param>
 223    /// <param name="bodyParameterName">The name of the body parameter, if any</param>
 224    /// <param name="parameters">The parameters for the callback</param>
 225    public void AddCallbackParameters(string callbackId, string? bodyParameterName, Dictionary<string, object?> paramete
 226    {
 0227        if (MapRouteOptions.CallbackPlan is null || MapRouteOptions.CallbackPlan.Count == 0)
 228        {
 0229            return;
 230        }
 0231        var plan = MapRouteOptions.CallbackPlan.FirstOrDefault(p => p.CallbackId == callbackId);
 0232        if (plan is null)
 233        {
 0234            Logger.Warning("CallbackPlan '{id}' not found.", callbackId);
 0235            return;
 236        }
 237        // Create a new execution plan
 0238        var newExecutionPlan = new CallBackExecutionPlan(
 0239            CallbackId: callbackId,
 0240            Plan: plan,
 0241            BodyParameterName: bodyParameterName,
 0242            Parameters: parameters
 0243        );
 244
 0245        CallbackPlan.Add(newExecutionPlan);
 0246    }
 247    #endregion
 248
 249    #region  Response Writers
 250    /// <summary>
 251    /// Writes a file response with the specified file path, content type, and HTTP status code.
 252    /// </summary>
 253    /// <param name="filePath">The path to the file to be sent in the response.</param>
 254    /// <param name="contentType">The MIME type of the file content.</param>
 255    /// <param name="statusCode">The HTTP status code for the response.</param>
 256    public void WriteFileResponse(
 257        string? filePath,
 258        string? contentType,
 259        int statusCode = StatusCodes.Status200OK
 260    )
 261    {
 2262        if (Log.IsEnabled(LogEventLevel.Debug))
 263        {
 2264            Log.Debug("Writing file response,FilePath={FilePath} StatusCode={StatusCode}, ContentType={ContentType}, Cur
 2265                filePath, statusCode, contentType, GetSafeCurrentDirectoryForLogging());
 266        }
 267
 2268        if (string.IsNullOrEmpty(filePath))
 269        {
 0270            throw new ArgumentException("File path cannot be null or empty.", nameof(filePath));
 271        }
 272
 273        // IMPORTANT:
 274        // - Path.GetFullPath(relative) uses the process CWD.
 275        // - If the CWD is missing/deleted (can occur in CI/test scenarios), GetFullPath can fail.
 276        // Resolve relative paths against a safe, existing base directory instead.
 2277        var fullPath = Path.IsPathRooted(filePath)
 2278            ? Path.GetFullPath(filePath)
 2279            : Path.GetFullPath(filePath, GetSafeCurrentDirectoryOrBaseDirectory());
 280
 2281        if (!File.Exists(fullPath))
 282        {
 1283            StatusCode = StatusCodes.Status404NotFound;
 1284            Body = $"File not found: {filePath}";
 1285            ContentType = $"text/plain; charset={Encoding.WebName}";
 1286            return;
 287        }
 288
 289        // 2. Extract the directory to use as the "root"
 1290        var directory = Path.GetDirectoryName(fullPath)
 1291                       ?? throw new InvalidOperationException("Could not determine directory from file path");
 292
 1293        if (Log.IsEnabled(LogEventLevel.Debug))
 294        {
 1295            Log.Debug("Serving file: {FilePath}", fullPath);
 296        }
 297
 298        // Create a physical file provider for the directory
 1299        var physicalProvider = new PhysicalFileProvider(directory);
 1300        var fi = physicalProvider.GetFileInfo(Path.GetFileName(fullPath));
 1301        var provider = new FileExtensionContentTypeProvider();
 1302        contentType ??= provider.TryGetContentType(fullPath, out var ct)
 1303                ? ct
 1304                : "application/octet-stream";
 1305        Body = fi;
 306
 307        // headers & metadata
 1308        StatusCode = statusCode;
 1309        ContentType = contentType;
 1310        Log.Debug("File response prepared: FileName={FileName}, Length={Length}, ContentType={ContentType}",
 1311            fi.Name, fi.Length, ContentType);
 1312    }
 313
 314    /// <summary>
 315    /// Writes a JSON response with the specified input object and HTTP status code.
 316    /// </summary>
 317    /// <param name="inputObject">The object to be converted to JSON.</param>
 318    /// <param name="statusCode">The HTTP status code for the response.</param>
 6319    public void WriteJsonResponse(object? inputObject, int statusCode = StatusCodes.Status200OK) => WriteJsonResponseAsy
 320
 321    /// <summary>
 322    /// Asynchronously writes a JSON response with the specified input object and HTTP status code.
 323    /// </summary>
 324    /// <param name="inputObject">The object to be converted to JSON.</param>
 325    /// <param name="statusCode">The HTTP status code for the response.</param>
 326    /// <param name="contentType">The MIME type of the response content.</param>
 6327    public async Task WriteJsonResponseAsync(object? inputObject, int statusCode = StatusCodes.Status200OK, string? cont
 328
 329    /// <summary>
 330    /// Writes a JSON response using the specified input object and serializer settings.
 331    /// </summary>
 332    /// <param name="inputObject">The object to be converted to JSON.</param>
 333    /// <param name="serializerOptions">The options to use for JSON serialization.</param>
 334    /// <param name="statusCode">The HTTP status code for the response.</param>
 335    /// <param name="contentType">The MIME type of the response content.</param>
 0336    public void WriteJsonResponse(object? inputObject, JsonSerializerOptions serializerOptions, int statusCode = StatusC
 337
 338    /// <summary>
 339    /// Asynchronously writes a JSON response using the specified input object and serializer settings.
 340    /// </summary>
 341    /// <param name="inputObject">The object to be converted to JSON.</param>
 342    /// <param name="serializerOptions">The options to use for JSON serialization.</param>
 343    /// <param name="statusCode">The HTTP status code for the response.</param>
 344    /// <param name="contentType">The MIME type of the response content.</param>
 345    public async Task WriteJsonResponseAsync(object? inputObject, JsonSerializerOptions serializerOptions, int statusCod
 346    {
 16347        if (Log.IsEnabled(LogEventLevel.Debug))
 348        {
 16349            Log.Debug("Writing JSON response (async), StatusCode={StatusCode}, ContentType={ContentType}", statusCode, c
 350        }
 351
 16352        ArgumentNullException.ThrowIfNull(serializerOptions);
 353
 16354        var sanitizedPayload = PayloadSanitizer.Sanitize(inputObject);
 32355        Body = await Task.Run(() => JsonSerializer.Serialize(sanitizedPayload, serializerOptions));
 16356        ContentType = string.IsNullOrEmpty(contentType) ? $"application/json; charset={Encoding.WebName}" : contentType;
 16357        StatusCode = statusCode;
 16358    }
 359    /// <summary>
 360    /// Writes a JSON response with the specified input object, serialization depth, compression option, status code, an
 361    /// </summary>
 362    /// <param name="inputObject">The object to be converted to JSON.</param>
 363    /// <param name="depth">The maximum depth for JSON serialization.</param>
 364    /// <param name="compress">Whether to compress the JSON output (no indentation).</param>
 365    /// <param name="statusCode">The HTTP status code for the response.</param>
 366    /// <param name="contentType">The MIME type of the response content.</param>
 1367    public void WriteJsonResponse(object? inputObject, int depth, bool compress, int statusCode = StatusCodes.Status200O
 368
 369    /// <summary>
 370    /// Asynchronously writes a JSON response with the specified input object, serialization depth, compression option, 
 371    /// </summary>
 372    /// <param name="inputObject">The object to be converted to JSON.</param>
 373    /// <param name="depth">The maximum depth for JSON serialization.</param>
 374    /// <param name="compress">Whether to compress the JSON output (no indentation).</param>
 375    /// <param name="statusCode">The HTTP status code for the response.</param>
 376    /// <param name="contentType">The MIME type of the response content.</param>
 377    public async Task WriteJsonResponseAsync(object? inputObject, int depth, bool compress, int statusCode = StatusCodes
 378    {
 16379        if (Log.IsEnabled(LogEventLevel.Debug))
 380        {
 16381            Log.Debug("Writing JSON response (async), StatusCode={StatusCode}, ContentType={ContentType}, Depth={Depth},
 16382                statusCode, contentType, depth, compress);
 383        }
 384
 16385        var serializerOptions = new JsonSerializerOptions
 16386        {
 16387            WriteIndented = !compress,
 16388            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
 16389            DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
 16390            ReferenceHandler = ReferenceHandler.IgnoreCycles,
 16391            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
 16392            MaxDepth = depth
 16393        };
 394
 16395        await WriteJsonResponseAsync(inputObject, serializerOptions: serializerOptions, statusCode: statusCode, contentT
 16396    }
 397    /// <summary>
 398    /// Writes a CBOR response (binary, efficient, not human-readable).
 399    /// </summary>
 400    public async Task WriteCborResponseAsync(object? inputObject, int statusCode = StatusCodes.Status200OK, string? cont
 401    {
 2402        if (Log.IsEnabled(LogEventLevel.Debug))
 403        {
 2404            Log.Debug("Writing CBOR response, StatusCode={StatusCode}, ContentType={ContentType}", statusCode, contentTy
 405        }
 406
 407        // Serialize to CBOR using PeterO.Cbor
 4408        Body = await Task.Run(() => inputObject != null
 4409            ? PeterO.Cbor.CBORObject.FromObject(inputObject).EncodeToBytes()
 4410            : []);
 2411        ContentType = string.IsNullOrEmpty(contentType) ? "application/cbor" : contentType;
 2412        StatusCode = statusCode;
 2413    }
 414
 415    /// <summary>
 416    /// Writes a CBOR response (binary, efficient, not human-readable).
 417    /// </summary>
 418    /// <param name="inputObject">The object to be converted to CBOR.</param>
 419    /// <param name="statusCode">The HTTP status code for the response.</param>
 420    /// <param name="contentType">The MIME type of the response content.</param>
 0421    public void WriteCborResponse(object? inputObject, int statusCode = StatusCodes.Status200OK, string? contentType = n
 422
 423    /// <summary>
 424    /// Asynchronously writes a BSON response with the specified input object, status code, and content type.
 425    /// </summary>
 426    /// <param name="inputObject">The object to be converted to BSON.</param>
 427    /// <param name="statusCode">The HTTP status code for the response.</param>
 428    /// <param name="contentType">The MIME type of the response content.</param>
 429    public async Task WriteBsonResponseAsync(object? inputObject, int statusCode = StatusCodes.Status200OK, string? cont
 430    {
 1431        if (Log.IsEnabled(LogEventLevel.Debug))
 432        {
 1433            Log.Debug("Writing BSON response, StatusCode={StatusCode}, ContentType={ContentType}", statusCode, contentTy
 434        }
 435
 436        // Serialize to BSON (as byte[])
 2437        Body = await Task.Run(() => inputObject != null ? inputObject.ToBson() : []);
 1438        ContentType = string.IsNullOrEmpty(contentType) ? "application/bson" : contentType;
 1439        StatusCode = statusCode;
 1440    }
 441
 442    /// <summary>
 443    /// Writes a BSON response with the specified input object, status code, and content type.
 444    /// </summary>
 445    /// <param name="inputObject">The object to be converted to BSON.</param>
 446    /// <param name="statusCode">The HTTP status code for the response.</param>
 447    /// <param name="contentType">The MIME type of the response content.</param>
 0448    public void WriteBsonResponse(object? inputObject, int statusCode = StatusCodes.Status200OK, string? contentType = n
 449
 450    /// <summary>
 451    /// Writes a response with the specified input object and HTTP status code.
 452    /// Chooses the response format based on the Accept header or defaults to text/plain.
 453    /// </summary>
 454    /// <param name="inputObject">The object to be sent in the response body.</param>
 455    /// <param name="statusCode">The HTTP status code for the response.</param>
 0456    public void WriteResponse(object? inputObject, int statusCode = StatusCodes.Status200OK) => WriteResponseAsync(input
 457
 458    /// <summary>
 459    /// Asynchronously writes a response with the specified input object and HTTP status code.
 460    /// Chooses the response format based on the Accept header or defaults to text/plain.
 461    /// </summary>
 462    /// <param name="inputObject">The object to be sent in the response body.</param>
 463    /// <param name="statusCode">The HTTP status code for the response.</param>
 464    /// <returns>A task that represents the asynchronous write operation.</returns>
 465    public async Task WriteResponseAsync(object? inputObject, int statusCode = StatusCodes.Status200OK)
 466    {
 5467        if (Log.IsEnabled(LogEventLevel.Debug))
 468        {
 5469            Log.Debug("Writing response, StatusCode={StatusCode}", statusCode);
 470        }
 471
 5472        Body = inputObject;
 473        try
 474        {
 5475            string? acceptHeader = null;
 5476            _ = Request?.Headers.TryGetValue(HeaderNames.Accept, out acceptHeader);
 477            // Pick best media type from Accept, default to text/plain
 5478            var selected = SelectResponseMediaType(acceptHeader, defaultType: KrContext.MapRouteOptions.DefaultResponseC
 479
 5480            if (selected is null)
 481            {
 0482                statusCode = StatusCodes.Status406NotAcceptable;
 0483                await WriteErrorResponseAsync("No acceptable media type found.", statusCode);
 0484                return;
 485            }
 486
 5487            if (Log.IsEnabled(LogEventLevel.Verbose))
 488            {
 0489                Log.Verbose("Selected response media type={MediaType}", selected);
 490            }
 491
 492            // Dispatch based on selected media type
 5493            await WriteByMediaTypeAsync(selected, inputObject, statusCode);
 5494        }
 0495        catch (Exception ex)
 496        {
 0497            Log.Error("Error in WriteResponseAsync: {Message}", ex.Message);
 0498            await WriteErrorResponseAsync($"Internal server error: {ex.Message}", StatusCodes.Status500InternalServerErr
 499        }
 5500    }
 501
 502    /// <summary>
 503    /// Selects the most appropriate response media type based on the Accept header.
 504    /// </summary>
 505    /// <param name="acceptHeader">The value of the Accept header from the request.</param>
 506    /// <param name="defaultType">The default media type to use if no match is found. Defaults to "text/plain".</param>
 507    /// <returns>The selected media type as a string.</returns>
 508    /// <remarks>
 509    /// This method parses the Accept header, orders the media types by quality factor,
 510    /// and selects the first supported media type. If none are supported, it returns the default type.
 511    /// </remarks>
 512    private static string? SelectResponseMediaType(string? acceptHeader, string? defaultType = "text/plain")
 513    {
 5514        if (string.IsNullOrWhiteSpace(acceptHeader))
 515        {
 0516            return defaultType;
 517        }
 518        // Parse and order by quality factor (q=)
 5519        var acceptValues = MediaTypeHeaderValue
 5520            .ParseList(acceptHeader.Split(','))
 11521            .OrderByDescending(v => v.Quality ?? 1.0);
 522        // Try to find a supported media type
 15523        foreach (var v in acceptValues)
 524        {
 5525            var mediaType = GetMediaTypeOrNull(v);
 5526            if (mediaType is not null)
 527            {
 528                // Map to canonical media type if needed
 5529                var mapped = MediaTypeHelper.Canonicalize(mediaType);
 5530                if (mapped is not null)
 531                {
 5532                    return mapped;
 533                }
 534            }
 535        }
 536        // No supported media type found; return default
 0537        return defaultType;
 5538    }
 539
 540    /// <summary>
 541    /// Gets the media type from the MediaTypeHeaderValue or null if not present.
 542    /// </summary>
 543    /// <param name="v"> The MediaTypeHeaderValue instance to extract the media type from.</param>
 544    /// <returns>The media type as a string if present; otherwise, null.</returns>
 545    private static string? GetMediaTypeOrNull(MediaTypeHeaderValue v)
 546    {
 5547        if (!v.MediaType.HasValue)
 548        {
 0549            return null;
 550        }
 551        // Trim whitespace
 5552        var s = v.MediaType.Value.Trim();
 553        // Return null for empty strings
 5554        return s.Length == 0 ? null : s;
 555    }
 556
 557    /// <summary>
 558    /// Writes a response based on the specified media type.
 559    /// </summary>
 560    /// <param name="mediaType">The media type to use for the response.</param>
 561    /// <param name="inputObject">The object to be written in the response body.</param>
 562    /// <param name="statusCode">The HTTP status code for the response.</param>
 563    /// <returns>A Task representing the asynchronous operation.</returns>
 564    private Task WriteByMediaTypeAsync(string mediaType, object? inputObject, int statusCode)
 565    {
 566        // If you want, set Response.ContentType here once, centrally.
 5567        ContentType = mediaType;
 568
 5569        return mediaType switch
 5570        {
 3571            "application/json" => WriteJsonResponseAsync(inputObject, statusCode, mediaType),
 0572            "application/yaml" => WriteYamlResponseAsync(inputObject, statusCode, mediaType),
 0573            "application/xml" => WriteXmlResponseAsync(inputObject, statusCode, mediaType),
 0574            "application/bson" => WriteBsonResponseAsync(inputObject, statusCode, mediaType),
 0575            "application/cbor" => WriteCborResponseAsync(inputObject, statusCode, mediaType),
 1576            "text/csv" => WriteCsvResponseAsync(inputObject, statusCode, mediaType),
 0577            "application/x-www-form-urlencoded" => WriteFormUrlEncodedResponseAsync(inputObject, statusCode),
 1578            _ => WriteTextResponseAsync(inputObject?.ToString() ?? string.Empty, statusCode),
 5579        };
 580    }
 581
 582    /// <summary>
 583    /// Writes a CSV response with the specified input object, status code, content type, and optional CsvConfiguration.
 584    /// </summary>
 585    /// <param name="inputObject">The object to be converted to CSV.</param>
 586    /// <param name="statusCode">The HTTP status code for the response.</param>
 587    /// <param name="contentType">The MIME type of the response content.</param>
 588    /// <param name="config">An optional CsvConfiguration to customize CSV output.</param>
 589    public void WriteCsvResponse(
 590            object? inputObject,
 591            int statusCode = StatusCodes.Status200OK,
 592            string? contentType = null,
 593            CsvConfiguration? config = null)
 594    {
 2595        Action<CsvConfiguration>? tweaker = null;
 596
 2597        if (config is not null)
 598        {
 1599            tweaker = target =>
 1600            {
 90601                foreach (var prop in typeof(CsvConfiguration)
 1602                     .GetProperties(BindingFlags.Public | BindingFlags.Instance))
 1603                {
 44604                    if (prop.CanRead && prop.CanWrite)
 1605                    {
 44606                        var value = prop.GetValue(config);
 44607                        prop.SetValue(target, value);
 1608                    }
 1609                }
 2610            };
 611        }
 2612        WriteCsvResponseAsync(inputObject, statusCode, contentType, tweaker).GetAwaiter().GetResult();
 2613    }
 614
 615    /// <summary>
 616    /// Asynchronously writes a CSV response with the specified input object, status code, content type, and optional co
 617    /// </summary>
 618    /// <param name="inputObject">The object to be converted to CSV.</param>
 619    /// <param name="statusCode">The HTTP status code for the response.</param>
 620    /// <param name="contentType">The MIME type of the response content.</param>
 621    /// <param name="tweak">An optional action to tweak the CsvConfiguration.</param>
 622    public async Task WriteCsvResponseAsync(
 623        object? inputObject,
 624        int statusCode = StatusCodes.Status200OK,
 625        string? contentType = null,
 626        Action<CsvConfiguration>? tweak = null)
 627    {
 4628        if (Log.IsEnabled(LogEventLevel.Debug))
 629        {
 4630            Log.Debug("Writing CSV response (async), StatusCode={StatusCode}, ContentType={ContentType}",
 4631                      statusCode, contentType);
 632        }
 633
 634        // Serialize inside a background task so heavy reflection never blocks the caller
 4635        Body = await Task.Run(() =>
 4636        {
 4637            var cfg = new CsvConfiguration(CultureInfo.InvariantCulture)
 4638            {
 4639                HasHeaderRecord = true,
 4640                NewLine = Environment.NewLine
 4641            };
 4642            tweak?.Invoke(cfg);                         // let the caller flirt with the config
 4643
 4644            using var sw = new StringWriter();
 4645            using var csv = new CsvWriter(sw, cfg);
 4646
 4647            // CsvHelper insists on an enumerable; wrap single objects so it stays happy
 4648            if (inputObject is IEnumerable records and not string)
 4649            {
 4650                csv.WriteRecords(records);              // whole collections (IEnumerable<T>)
 4651            }
 0652            else if (inputObject is not null)
 4653            {
 0654                csv.WriteRecords([inputObject]); // lone POCO
 4655            }
 4656            else
 4657            {
 0658                csv.WriteHeader<object>();              // nothing? write only headers for an empty file
 4659            }
 4660
 4661            return sw.ToString();
 8662        }).ConfigureAwait(false);
 663
 4664        ContentType = string.IsNullOrEmpty(contentType)
 4665            ? $"text/csv; charset={Encoding.WebName}"
 4666            : contentType;
 4667        StatusCode = statusCode;
 4668    }
 669    /// <summary>
 670    /// Writes a YAML response with the specified input object, status code, and content type.
 671    /// </summary>
 672    /// <param name="inputObject">The object to be converted to YAML.</param>
 673    /// <param name="statusCode">The HTTP status code for the response.</param>
 674    /// <param name="contentType">The MIME type of the response content.</param>
 1675    public void WriteYamlResponse(object? inputObject, int statusCode = StatusCodes.Status200OK, string? contentType = n
 676
 677    /// <summary>
 678    /// Asynchronously writes a YAML response with the specified input object, status code, and content type.
 679    /// </summary>
 680    /// <param name="inputObject">The object to be converted to YAML.</param>
 681    /// <param name="statusCode">The HTTP status code for the response.</param>
 682    /// <param name="contentType">The MIME type of the response content.</param>
 683    public async Task WriteYamlResponseAsync(object? inputObject, int statusCode = StatusCodes.Status200OK, string? cont
 684    {
 3685        if (Log.IsEnabled(LogEventLevel.Debug))
 686        {
 3687            Log.Debug("Writing YAML response (async), StatusCode={StatusCode}, ContentType={ContentType}", statusCode, c
 688        }
 689
 6690        Body = await Task.Run(() => YamlHelper.ToYaml(inputObject));
 3691        ContentType = string.IsNullOrEmpty(contentType) ? $"application/yaml; charset={Encoding.WebName}" : contentType;
 3692        StatusCode = statusCode;
 3693    }
 694
 695    /// <summary>
 696    /// Writes an XML response with the specified input object, status code, and content type.
 697    /// </summary>
 698    /// <param name="inputObject">The object to be converted to XML.</param>
 699    /// <param name="statusCode">The HTTP status code for the response.</param>
 700    /// <param name="contentType">The MIME type of the response content.</param>
 701    /// <param name="rootElementName">Optional custom XML root element name. Defaults to <c>Response</c>.</param>
 702    /// <param name="compress">If true, emits compact XML (no indentation); if false (default) output is human readable.
 703    public void WriteXmlResponse(object? inputObject, int statusCode = StatusCodes.Status200OK, string? contentType = nu
 6704        => WriteXmlResponseAsync(inputObject, statusCode, contentType, rootElementName, compress).GetAwaiter().GetResult
 705
 706    /// <summary>
 707    /// Asynchronously writes an XML response with the specified input object, status code, and content type.
 708    /// </summary>
 709    /// <param name="inputObject">The object to be converted to XML.</param>
 710    /// <param name="statusCode">The HTTP status code for the response.</param>
 711    /// <param name="contentType">The MIME type of the response content.</param>
 712    /// <param name="rootElementName">Optional custom XML root element name. Defaults to <c>Response</c>.</param>
 713    /// <param name="compress">If true, emits compact XML (no indentation); if false (default) output is human readable.
 714    public async Task WriteXmlResponseAsync(object? inputObject, int statusCode = StatusCodes.Status200OK, string? conte
 715    {
 8716        if (Log.IsEnabled(LogEventLevel.Debug))
 717        {
 8718            Log.Debug("Writing XML response (async), StatusCode={StatusCode}, ContentType={ContentType}", statusCode, co
 719        }
 720
 8721        var root = string.IsNullOrWhiteSpace(rootElementName) ? "Response" : rootElementName.Trim();
 16722        var xml = await Task.Run(() => XmlHelper.ToXml(root, inputObject));
 8723        var saveOptions = compress ? SaveOptions.DisableFormatting : SaveOptions.None;
 16724        Body = await Task.Run(() => xml.ToString(saveOptions));
 8725        ContentType = string.IsNullOrEmpty(contentType) ? $"application/xml; charset={Encoding.WebName}" : contentType;
 8726        StatusCode = statusCode;
 8727    }
 728    /// <summary>
 729    /// Writes a text response with the specified input object, status code, and content type.
 730    /// </summary>
 731    /// <param name="inputObject">The object to be converted to a text response.</param>
 732    /// <param name="statusCode">The HTTP status code for the response.</param>
 733    /// <param name="contentType">The MIME type of the response content.</param>
 734    public void WriteTextResponse(object? inputObject, int statusCode = StatusCodes.Status200OK, string? contentType = n
 8735        WriteTextResponseAsync(inputObject, statusCode, contentType).GetAwaiter().GetResult();
 736
 737    /// <summary>
 738    /// Asynchronously writes a text response with the specified input object, status code, and content type.
 739    /// </summary>
 740    /// <param name="inputObject">The object to be converted to a text response.</param>
 741    /// <param name="statusCode">The HTTP status code for the response.</param>
 742    /// <param name="contentType">The MIME type of the response content.</param>
 743    public async Task WriteTextResponseAsync(object? inputObject, int statusCode = StatusCodes.Status200OK, string? cont
 744    {
 35745        if (Log.IsEnabled(LogEventLevel.Debug))
 746        {
 34747            Log.Debug("Writing text response (async), StatusCode={StatusCode}, ContentType={ContentType}", statusCode, c
 748        }
 749
 35750        if (inputObject is null)
 751        {
 0752            throw new ArgumentNullException(nameof(inputObject), "Input object cannot be null for text response.");
 753        }
 754
 70755        Body = await Task.Run(() => inputObject?.ToString() ?? string.Empty);
 35756        ContentType = string.IsNullOrEmpty(contentType) ? $"text/plain; charset={Encoding.WebName}" : contentType;
 35757        StatusCode = statusCode;
 35758    }
 759
 760    /// <summary>
 761    /// Writes a form-urlencoded response with the specified input object, status code, and optional content type.
 762    /// Automatically converts the input object to a Dictionary{string, string} using <see cref="ObjectToDictionaryConve
 763    /// </summary>
 764    /// <param name="inputObject">The object to be converted to form-urlencoded data. Can be a dictionary, enumerable, o
 765    /// <param name="statusCode">The HTTP status code for the response. Defaults to 200 OK.</param>
 766    public void WriteFormUrlEncodedResponse(object? inputObject, int statusCode = StatusCodes.Status200OK) =>
 8767        WriteFormUrlEncodedResponseAsync(inputObject, statusCode).GetAwaiter().GetResult();
 768
 769    /// <summary>
 770    /// Asynchronously writes a form-urlencoded response with the specified input object, status code, and optional cont
 771    /// Automatically converts the input object to a Dictionary{string, string} using <see cref="ObjectToDictionaryConve
 772    /// </summary>
 773    /// <param name="inputObject">The object to be converted to form-urlencoded data. Can be a dictionary, enumerable, o
 774    /// <param name="statusCode">The HTTP status code for the response. Defaults to 200 OK.</param>
 775    public async Task WriteFormUrlEncodedResponseAsync(object? inputObject, int statusCode = StatusCodes.Status200OK)
 776    {
 11777        if (inputObject is null)
 778        {
 2779            throw new ArgumentNullException(nameof(inputObject), "Input object cannot be null for form-urlencoded respon
 780        }
 781
 9782        var dictionary = ObjectToDictionaryConverter.ToDictionary(inputObject);
 9783        var formContent = new FormUrlEncodedContent(dictionary);
 9784        var encodedString = await formContent.ReadAsStringAsync();
 785
 9786        await WriteTextResponseAsync(encodedString, statusCode, "application/x-www-form-urlencoded");
 9787    }
 788
 789    /// <summary>
 790    /// Writes an HTTP redirect response with the specified URL and optional message.
 791    /// </summary>
 792    /// <param name="url">The URL to redirect to.</param>
 793    /// <param name="message">An optional message to include in the response body.</param>
 794    public void WriteRedirectResponse(string url, string? message = null)
 795    {
 6796        if (Log.IsEnabled(LogEventLevel.Debug))
 797        {
 5798            Log.Debug("Writing redirect response, StatusCode={StatusCode}, Location={Location}", StatusCode, url);
 799        }
 800
 6801        if (string.IsNullOrEmpty(url))
 802        {
 0803            throw new ArgumentNullException(nameof(url), "URL cannot be null for redirect response.");
 804        }
 805        // framework hook
 6806        RedirectUrl = url;
 807
 808        // HTTP status + Location header
 6809        StatusCode = StatusCodes.Status302Found;
 6810        Headers["Location"] = url;
 811
 6812        if (message is not null)
 813        {
 814            // include a body
 1815            Body = message;
 1816            ContentType = $"text/plain; charset={Encoding.WebName}";
 817        }
 818        else
 819        {
 820            // no body: clear any existing body/headers
 5821            Body = null;
 822            //ContentType = null;
 5823            _ = Headers.Remove("Content-Length");
 824        }
 5825    }
 826
 827    /// <summary>
 828    /// Writes a binary response with the specified data, status code, and content type.
 829    /// </summary>
 830    /// <param name="data">The binary data to send in the response.</param>
 831    /// <param name="statusCode">The HTTP status code for the response.</param>
 832    /// <param name="contentType">The MIME type of the response content.</param>
 833    public void WriteBinaryResponse(byte[] data, int statusCode = StatusCodes.Status200OK, string contentType = "applica
 834    {
 1835        if (Log.IsEnabled(LogEventLevel.Debug))
 836        {
 1837            Log.Debug("Writing binary response, StatusCode={StatusCode}, ContentType={ContentType}", statusCode, content
 838        }
 839
 1840        Body = data ?? throw new ArgumentNullException(nameof(data), "Data cannot be null for binary response.");
 1841        ContentType = contentType;
 1842        StatusCode = statusCode;
 1843    }
 844    /// <summary>
 845    /// Writes a stream response with the specified stream, status code, and content type.
 846    /// </summary>
 847    /// <param name="stream">The stream to send in the response.</param>
 848    /// <param name="statusCode">The HTTP status code for the response.</param>
 849    /// <param name="contentType">The MIME type of the response content.</param>
 850    public void WriteStreamResponse(Stream stream, int statusCode = StatusCodes.Status200OK, string contentType = "appli
 851    {
 3852        if (Log.IsEnabled(LogEventLevel.Debug))
 853        {
 3854            Log.Debug("Writing stream response, StatusCode={StatusCode}, ContentType={ContentType}", statusCode, content
 855        }
 856
 3857        Body = stream;
 3858        ContentType = contentType;
 3859        StatusCode = statusCode;
 3860    }
 861    #endregion
 862
 863    #region Error Responses
 864    /// <summary>
 865    /// Structured payload for error responses.
 866    /// </summary>
 867    internal record ErrorPayload
 868    {
 26869        public string Error { get; init; } = default!;
 27870        public string? Details { get; init; }
 29871        public string? Exception { get; init; }
 28872        public string? StackTrace { get; init; }
 52873        public int Status { get; init; }
 26874        public string Reason { get; init; } = default!;
 26875        public string Timestamp { get; init; } = default!;
 20876        public string? Path { get; init; }
 20877        public string? Method { get; init; }
 878    }
 879
 880    /// <summary>
 881    /// Write an error response with a custom message.
 882    /// Chooses JSON/YAML/XML/plain-text based on override → Accept → default JSON.
 883    /// </summary>
 884    public async Task WriteErrorResponseAsync(
 885        string message,
 886        int statusCode = StatusCodes.Status500InternalServerError,
 887        string? contentType = null,
 888        string? details = null)
 889    {
 10890        if (Log.IsEnabled(LogEventLevel.Debug))
 891        {
 10892            Log.Debug("Writing error response, StatusCode={StatusCode}, ContentType={ContentType}, Message={Message}",
 10893                statusCode, contentType, message);
 894        }
 895
 10896        if (string.IsNullOrWhiteSpace(message))
 897        {
 0898            throw new ArgumentNullException(nameof(message));
 899        }
 900
 10901        Log.Warning("Writing error response with status {StatusCode}: {Message}", statusCode, message);
 902
 10903        var payload = new ErrorPayload
 10904        {
 10905            Error = message,
 10906            Details = details,
 10907            Exception = null,
 10908            StackTrace = null,
 10909            Status = statusCode,
 10910            Reason = ReasonPhrases.GetReasonPhrase(statusCode),
 10911            Timestamp = DateTime.UtcNow.ToString("o"),
 10912            Path = Request?.Path,
 10913            Method = Request?.Method
 10914        };
 915
 10916        await WriteFormattedErrorResponseAsync(payload, contentType);
 10917    }
 918
 919    /// <summary>
 920    /// Writes an error response with a custom message.
 921    /// Chooses JSON/YAML/XML/plain-text based on override → Accept → default JSON.
 922    /// </summary>
 923    /// <param name="message">The error message to include in the response.</param>
 924    /// <param name="statusCode">The HTTP status code for the response.</param>
 925    /// <param name="contentType">The MIME type of the response content.</param>
 926    /// <param name="details">Optional details to include in the response.</param>
 927    public void WriteErrorResponse(
 928      string message,
 929      int statusCode = StatusCodes.Status500InternalServerError,
 930      string? contentType = null,
 1931      string? details = null) => WriteErrorResponseAsync(message, statusCode, contentType, details).GetAwaiter().GetResu
 932
 933    /// <summary>
 934    /// Asynchronously writes an error response based on an exception.
 935    /// Chooses JSON/YAML/XML/plain-text based on override → Accept → default JSON.
 936    /// </summary>
 937    /// <param name="ex">The exception to report.</param>
 938    /// <param name="statusCode">The HTTP status code for the response.</param>
 939    /// <param name="contentType">The MIME type of the response content.</param>
 940    /// <param name="includeStack">Whether to include the stack trace in the response.</param>
 941    public async Task WriteErrorResponseAsync(
 942        Exception ex,
 943        int statusCode = StatusCodes.Status500InternalServerError,
 944        string? contentType = null,
 945        bool includeStack = true)
 946    {
 3947        if (Log.IsEnabled(LogEventLevel.Debug))
 948        {
 3949            Log.Debug("Writing error response from exception, StatusCode={StatusCode}, ContentType={ContentType}, Includ
 3950                statusCode, contentType, includeStack);
 951        }
 952
 3953        ArgumentNullException.ThrowIfNull(ex);
 954
 3955        Log.Warning(ex, "Writing error response with status {StatusCode}", statusCode);
 956
 3957        var payload = new ErrorPayload
 3958        {
 3959            Error = ex.Message,
 3960            Details = null,
 3961            Exception = ex.GetType().Name,
 3962            StackTrace = includeStack ? ex.ToString() : null,
 3963            Status = statusCode,
 3964            Reason = ReasonPhrases.GetReasonPhrase(statusCode),
 3965            Timestamp = DateTime.UtcNow.ToString("o"),
 3966            Path = Request?.Path,
 3967            Method = Request?.Method
 3968        };
 969
 3970        await WriteFormattedErrorResponseAsync(payload, contentType);
 3971    }
 972    /// <summary>
 973    /// Writes an error response based on an exception.
 974    /// Chooses JSON/YAML/XML/plain-text based on override → Accept → default JSON.
 975    /// </summary>
 976    /// <param name="ex">The exception to report.</param>
 977    /// <param name="statusCode">The HTTP status code for the response.</param>
 978    /// <param name="contentType">The MIME type of the response content.</param>
 979    /// <param name="includeStack">Whether to include the stack trace in the response.</param>
 980    public void WriteErrorResponse(
 981            Exception ex,
 982            int statusCode = StatusCodes.Status500InternalServerError,
 983            string? contentType = null,
 1984            bool includeStack = true) => WriteErrorResponseAsync(ex, statusCode, contentType, includeStack).GetAwaiter()
 985
 986    /// <summary>
 987    /// Internal dispatcher: serializes the payload according to the chosen content-type.
 988    /// </summary>
 989    private async Task WriteFormattedErrorResponseAsync(ErrorPayload payload, string? contentType = null)
 990    {
 13991        if (Log.IsEnabled(LogEventLevel.Debug))
 992        {
 13993            Log.Debug("Writing formatted error response, ContentType={ContentType}, Status={Status}", contentType, paylo
 994        }
 995
 13996        if (string.IsNullOrWhiteSpace(contentType))
 997        {
 11998            _ = Request.Headers.TryGetValue("Accept", out var acceptHeader);
 11999            contentType = (acceptHeader ?? "text/plain")
 111000                                 .ToLowerInvariant();
 1001        }
 131002        if (contentType.Contains("json"))
 1003        {
 31004            await WriteJsonResponseAsync(payload, payload.Status);
 1005        }
 101006        else if (contentType.Contains("yaml") || contentType.Contains("yml"))
 1007        {
 21008            await WriteYamlResponseAsync(payload, payload.Status);
 1009        }
 81010        else if (contentType.Contains("xml"))
 1011        {
 21012            await WriteXmlResponseAsync(payload, payload.Status);
 1013        }
 1014        else
 1015        {
 1016            // Plain-text fallback
 61017            var lines = new List<string>
 61018                {
 61019                    $"Status: {payload.Status} ({payload.Reason})",
 61020                    $"Error: {payload.Error}",
 61021                    $"Time: {payload.Timestamp}"
 61022                };
 1023
 61024            if (!string.IsNullOrWhiteSpace(payload.Details))
 1025            {
 11026                lines.Add("Details:\n" + payload.Details);
 1027            }
 1028
 61029            if (!string.IsNullOrWhiteSpace(payload.Exception))
 1030            {
 31031                lines.Add($"Exception: {payload.Exception}");
 1032            }
 1033
 61034            if (!string.IsNullOrWhiteSpace(payload.StackTrace))
 1035            {
 21036                lines.Add("StackTrace:\n" + payload.StackTrace);
 1037            }
 1038
 61039            var text = string.Join("\n", lines);
 61040            await WriteTextResponseAsync(text, payload.Status, "text/plain");
 1041        }
 131042    }
 1043
 1044    #endregion
 1045    #region HTML Response Helpers
 1046
 1047    /// <summary>
 1048    /// Renders a template string by replacing placeholders in the format {{key}} with corresponding values from the pro
 1049    /// </summary>
 1050    /// <param name="template">The template string containing placeholders.</param>
 1051    /// <param name="vars">A dictionary of variables to replace in the template.</param>
 1052    /// <returns>The rendered string with placeholders replaced by variable values.</returns>
 1053    private static string RenderInlineTemplate(
 1054     string template,
 1055     IReadOnlyDictionary<string, object?> vars)
 1056    {
 21057        if (Log.IsEnabled(LogEventLevel.Debug))
 1058        {
 21059            Log.Debug("Rendering inline template, TemplateLength={TemplateLength}, VarsCount={VarsCount}",
 21060                      template?.Length ?? 0, vars?.Count ?? 0);
 1061        }
 1062
 21063        if (string.IsNullOrEmpty(template))
 1064        {
 01065            return string.Empty;
 1066        }
 1067
 21068        if (vars is null || vars.Count == 0)
 1069        {
 01070            return template;
 1071        }
 1072
 21073        var render = RenderInline(template, vars);
 1074
 21075        if (Log.IsEnabled(LogEventLevel.Debug))
 1076        {
 21077            Log.Debug("Rendered template length: {RenderedLength}", render.Length);
 1078        }
 1079
 21080        return render;
 1081    }
 1082
 1083    /// <summary>
 1084    /// Renders a template string by replacing placeholders in the format {{key}} with corresponding values from the pro
 1085    /// </summary>
 1086    /// <param name="template">The template string containing placeholders.</param>
 1087    /// <param name="vars">A dictionary of variables to replace in the template.</param>
 1088    /// <returns>The rendered string with placeholders replaced by variable values.</returns>
 1089    private static string RenderInline(string template, IReadOnlyDictionary<string, object?> vars)
 1090    {
 21091        var sb = new StringBuilder(template.Length);
 1092
 1093        // Iterate through the template
 21094        var i = 0;
 391095        while (i < template.Length)
 1096        {
 1097            // opening “{{”
 371098            if (template[i] == '{' && i + 1 < template.Length && template[i + 1] == '{')
 1099            {
 31100                var start = i + 2;                                        // after “{{”
 31101                var end = template.IndexOf("}}", start, StringComparison.Ordinal);
 1102
 31103                if (end > start)                                          // found closing “}}”
 1104                {
 31105                    var rawKey = template[start..end].Trim();
 1106
 31107                    if (TryResolveValue(rawKey, vars, out var value) && value is not null)
 1108                    {
 31109                        _ = sb.Append(value); // append resolved value
 1110                    }
 1111                    else
 1112                    {
 01113                        _ = sb.Append("{{").Append(rawKey).Append("}}");      // leave it as-is if unknown
 1114                    }
 1115
 31116                    i = end + 2;    // jump past the “}}”
 31117                    continue;
 1118                }
 1119            }
 1120
 1121            // ordinary character
 341122            _ = sb.Append(template[i]);
 341123            i++; // move to the next character
 1124        }
 21125        return sb.ToString();
 1126    }
 1127
 1128    /// <summary>
 1129    /// Resolves a dotted path like “Request.Path” through nested dictionaries
 1130    /// and/or object properties (case-insensitive).
 1131    /// </summary>
 1132    private static bool TryResolveValue(
 1133        string path,
 1134        IReadOnlyDictionary<string, object?> root,
 1135        out object? value)
 1136    {
 31137        value = null;
 1138
 31139        if (string.IsNullOrWhiteSpace(path))
 1140        {
 01141            return false;
 1142        }
 1143
 31144        object? current = root;
 161145        foreach (var segment in path.Split('.'))
 1146        {
 51147            if (current is null)
 1148            {
 01149                return false;
 1150            }
 1151
 1152            // ① Handle dictionary look-ups (IReadOnlyDictionary or IDictionary)
 51153            if (current is IReadOnlyDictionary<string, object?> roDict)
 1154            {
 31155                if (!roDict.TryGetValue(segment, out current))
 1156                {
 01157                    return false;
 1158                }
 1159
 1160                continue;
 1161            }
 1162
 21163            if (current is IDictionary dict)
 1164            {
 01165                if (!dict.Contains(segment))
 1166                {
 01167                    return false;
 1168                }
 1169
 01170                current = dict[segment];
 01171                continue;
 1172            }
 1173
 1174            // ② Handle property look-ups via reflection
 21175            var prop = current.GetType().GetProperty(
 21176                segment,
 21177                BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
 1178
 21179            if (prop is null)
 1180            {
 01181                return false;
 1182            }
 1183
 21184            current = prop.GetValue(current);
 1185        }
 1186
 31187        value = current;
 31188        return true;
 1189    }
 1190
 1191    /// <summary>
 1192    /// Attempts to revalidate the cache based on ETag and Last-Modified headers.
 1193    /// If the resource is unchanged, sets the response status to 304 Not Modified.
 1194    /// Returns true if a 304 response was written, false otherwise.
 1195    /// </summary>
 1196    /// <param name="payload">The payload to validate.</param>
 1197    /// <param name="etag">The ETag header value.</param>
 1198    /// <param name="weakETag">Indicates if the ETag is a weak ETag.</param>
 1199    /// <param name="lastModified">The Last-Modified header value.</param>
 1200    /// <returns>True if a 304 response was written, false otherwise.</returns>
 1201    public bool RevalidateCache(object? payload,
 1202       string? etag = null,
 1203       bool weakETag = false,
 01204       DateTimeOffset? lastModified = null) => CacheRevalidation.TryWrite304(Context, payload, etag, weakETag, lastModif
 1205
 1206    /// <summary>
 1207    /// Asynchronously writes an HTML response, rendering the provided template string and replacing placeholders with v
 1208    /// </summary>
 1209    /// <param name="template">The HTML template string containing placeholders.</param>
 1210    /// <param name="vars">A dictionary of variables to replace in the template.</param>
 1211    /// <param name="statusCode">The HTTP status code for the response.</param>
 1212    public async Task WriteHtmlResponseAsync(
 1213        string template,
 1214        IReadOnlyDictionary<string, object?>? vars,
 1215        int statusCode = 200)
 1216    {
 21217        if (Log.IsEnabled(LogEventLevel.Debug))
 1218        {
 21219            Log.Debug("Writing HTML response (async), StatusCode={StatusCode}, TemplateLength={TemplateLength}", statusC
 1220        }
 1221
 21222        if (vars is null || vars.Count == 0)
 1223        {
 01224            await WriteTextResponseAsync(template, statusCode, "text/html");
 1225        }
 1226        else
 1227        {
 21228            await WriteTextResponseAsync(RenderInlineTemplate(template, vars), statusCode, "text/html");
 1229        }
 21230    }
 1231
 1232    /// <summary>
 1233    /// Asynchronously writes an HTML response, rendering the provided template byte array and replacing placeholders wi
 1234    /// </summary>
 1235    /// <param name="template">The HTML template byte array.</param>
 1236    /// <param name="vars">A dictionary of variables to replace in the template.</param>
 1237    /// <param name="statusCode">The HTTP status code for the response.</param>
 1238    /// <returns>A task representing the asynchronous operation.</returns>
 1239    public async Task WriteHtmlResponseAsync(
 1240    byte[] template,
 1241    IReadOnlyDictionary<string, object?>? vars,
 01242    int statusCode = 200) => await WriteHtmlResponseAsync(Encoding.GetString(template), vars, statusCode);
 1243
 1244    /// <summary>
 1245    /// Writes an HTML response, rendering the provided template byte array and replacing placeholders with values from 
 1246    /// </summary>
 1247    /// <param name="template">The HTML template byte array.</param>
 1248    /// <param name="vars">A dictionary of variables to replace in the template.</param>
 1249    /// <param name="statusCode">The HTTP status code for the response.</param>
 1250    public void WriteHtmlResponse(
 1251         byte[] template,
 1252         IReadOnlyDictionary<string, object?>? vars,
 01253         int statusCode = 200) => WriteHtmlResponseAsync(Encoding.GetString(template), vars, statusCode).GetAwaiter().Ge
 1254
 1255    /// <summary>
 1256    /// Asynchronously reads an HTML file, merges in placeholders from the provided dictionary, and writes the result as
 1257    /// </summary>
 1258    /// <param name="filePath">The path to the HTML file to read.</param>
 1259    /// <param name="vars">A dictionary of variables to replace in the template.</param>
 1260    /// <param name="statusCode">The HTTP status code for the response.</param>
 1261    public async Task WriteHtmlResponseFromFileAsync(
 1262        string filePath,
 1263        IReadOnlyDictionary<string, object?> vars,
 1264        int statusCode = 200)
 1265    {
 11266        if (Log.IsEnabled(LogEventLevel.Debug))
 1267        {
 11268            Log.Debug("Writing HTML response from file (async), FilePath={FilePath}, StatusCode={StatusCode}", filePath,
 1269        }
 1270
 11271        if (!File.Exists(filePath))
 1272        {
 01273            WriteTextResponse($"<!-- File not found: {filePath} -->", 404, "text/html");
 01274            return;
 1275        }
 1276
 11277        var template = await File.ReadAllTextAsync(filePath);
 11278        WriteHtmlResponseAsync(template, vars, statusCode).GetAwaiter().GetResult();
 11279    }
 1280
 1281    /// <summary>
 1282    /// Renders the given HTML string with placeholders and writes it as a response.
 1283    /// </summary>
 1284    /// <param name="template">The HTML template string containing placeholders.</param>
 1285    /// <param name="vars">A dictionary of variables to replace in the template.</param>
 1286    /// <param name="statusCode">The HTTP status code for the response.</param>
 1287    public void WriteHtmlResponse(
 1288        string template,
 1289        IReadOnlyDictionary<string, object?>? vars,
 01290        int statusCode = 200) => WriteHtmlResponseAsync(template, vars, statusCode).GetAwaiter().GetResult();
 1291
 1292    /// <summary>
 1293    /// Reads an .html file, merges in placeholders, and writes it.
 1294    /// </summary>
 1295    public void WriteHtmlResponseFromFile(
 1296        string filePath,
 1297        IReadOnlyDictionary<string, object?> vars,
 01298        int statusCode = 200) => WriteHtmlResponseFromFileAsync(filePath, vars, statusCode).GetAwaiter().GetResult();
 1299
 1300    /// <summary>
 1301    /// Writes only the specified HTTP status code, clearing any body or content type.
 1302    /// </summary>
 1303    /// <param name="statusCode">The HTTP status code to write.</param>
 1304    public void WriteStatusOnly(int statusCode)
 1305    {
 1306        // Clear any body indicators so StatusCodePages can run
 11307        ContentType = null;
 11308        StatusCode = statusCode;
 11309        Body = null;
 11310    }
 1311    #endregion
 1312
 1313    #region Apply to HttpResponse
 1314
 1315    /// <summary>
 1316    /// Applies the current KestrunResponse to the specified HttpResponse, setting status, headers, cookies, and writing
 1317    /// </summary>
 1318    /// <param name="response">The HttpResponse to apply the response to.</param>
 1319    /// <returns>A task representing the asynchronous operation.</returns>
 1320    public async Task ApplyTo(HttpResponse response)
 1321    {
 311322        if (Log.IsEnabled(LogEventLevel.Debug))
 1323        {
 301324            Log.Debug("Applying KestrunResponse to HttpResponse, StatusCode={StatusCode}, ContentType={ContentType}, Bod
 301325                StatusCode, ContentType, Body?.GetType().Name ?? "null");
 1326        }
 1327
 311328        if (response.StatusCode == StatusCodes.Status304NotModified)
 1329        {
 01330            if (Log.IsEnabled(LogEventLevel.Debug))
 1331            {
 01332                Log.Debug("Response already has status code 304 Not Modified, skipping ApplyTo");
 1333            }
 01334            return;
 1335        }
 311336        if (!string.IsNullOrEmpty(RedirectUrl))
 1337        {
 21338            response.Redirect(RedirectUrl);
 21339            return;
 1340        }
 1341
 1342        try
 1343        {
 291344            EnsureStatus(response);
 291345            ApplyHeadersAndCookies(response);
 291346            ApplyCachingHeaders(response);
 1347
 291348            await TryEnqueueCallbacks();
 1349
 291350            if (Body is not null)
 1351            {
 251352                EnsureContentType(response);
 251353                ApplyContentDispositionHeader(response);
 251354                await WriteBodyAsync(response).ConfigureAwait(false);
 1355            }
 1356            else
 1357            {
 41358                response.ContentType = null;
 41359                response.ContentLength = null;
 41360                if (Log.IsEnabled(LogEventLevel.Debug))
 1361                {
 41362                    Log.Debug("Status-only: HasStarted={HasStarted} CL={CL} CT='{CT}'",
 41363                        response.HasStarted, response.ContentLength, response.ContentType);
 1364                }
 1365            }
 291366        }
 01367        catch (Exception ex)
 1368        {
 01369            Console.WriteLine($"Error applying response: {ex.Message}");
 1370            // Optionally, you can log the exception or handle it as needed
 01371            throw;
 1372        }
 311373    }
 1374
 1375    /// <summary>
 1376    /// Attempts to enqueue any registered callback requests using the ICallbackDispatcher service.
 1377    /// </summary>
 1378    private async ValueTask TryEnqueueCallbacks()
 1379    {
 291380        if (CallbackPlan.Count == 0)
 1381        {
 281382            return;
 1383        }
 1384
 1385        // Prevent multiple enqueues
 11386        if (Interlocked.Exchange(ref CallbacksEnqueuedFlag, 1) == 1)
 1387        {
 01388            return;
 1389        }
 1390
 11391        var log = KrContext.Host.Logger;
 11392        if (log.IsEnabled(LogEventLevel.Information))
 1393        {
 11394            log.Information("Enqueuing {Count} callbacks for dispatch.", CallbackPlan.Count);
 1395        }
 1396
 1397        try
 1398        {
 11399            var httpCtx = KrContext.HttpContext;
 1400
 1401            // Resolve DI services while request is alive
 11402            var dispatcher = httpCtx.RequestServices.GetService<ICallbackDispatcher>();
 11403            if (dispatcher is null)
 1404            {
 11405                log.Warning("Callbacks present but no ICallbackDispatcher registered. Count={Count}", CallbackPlan.Count
 11406                return;
 1407            }
 1408
 01409            var urlResolver = httpCtx.RequestServices.GetRequiredService<ICallbackUrlResolver>();
 01410            var serializer = httpCtx.RequestServices.GetRequiredService<ICallbackBodySerializer>();
 01411            var options = httpCtx.RequestServices.GetService<CallbackDispatchOptions>() ?? new CallbackDispatchOptions()
 1412
 01413            foreach (var plan in CallbackPlan)
 1414            {
 1415                try
 1416                {
 01417                    var req = CallbackRequestFactory.FromPlan(plan, KrContext, urlResolver, serializer, options);
 1418
 01419                    if (log.IsEnabled(LogEventLevel.Debug))
 1420                    {
 01421                        log.Debug("Enqueue callback. CallbackId={CallbackId} Url={Url}", req.CallbackId, req.TargetUrl);
 1422                    }
 1423
 01424                    await dispatcher.EnqueueAsync(req, CancellationToken.None).ConfigureAwait(false);
 01425                }
 01426                catch (Exception ex)
 1427                {
 01428                    log.Error(ex, "Failed to enqueue callback. CallbackId={CallbackId}", plan.CallbackId);
 01429                }
 01430            }
 01431        }
 01432        catch (Exception ex)
 1433        {
 01434            log.Error(ex, "Unexpected error while scheduling callbacks.");
 01435        }
 291436    }
 1437
 1438    /// <summary>
 1439    /// Ensures the HTTP response has the correct status code and content type.
 1440    /// </summary>
 1441    /// <param name="response">The HTTP response to apply the status and content type to.</param>
 1442    private void EnsureContentType(HttpResponse response)
 1443    {
 251444        if (ContentType != response.ContentType)
 1445        {
 251446            if (!string.IsNullOrEmpty(ContentType) &&
 251447                IsTextBasedContentType(ContentType) &&
 251448                !ContentType.Contains("charset=", StringComparison.OrdinalIgnoreCase))
 1449            {
 51450                ContentType = ContentType.TrimEnd(';') + $"; charset={AcceptCharset.WebName}";
 1451            }
 251452            response.ContentType = ContentType;
 1453        }
 251454    }
 1455
 1456    /// <summary>
 1457    /// Ensures the HTTP response has the correct status code.
 1458    /// </summary>
 1459    /// <param name="response">The HTTP response to apply the status code to.</param>
 1460    private void EnsureStatus(HttpResponse response)
 1461    {
 291462        if (StatusCode != response.StatusCode)
 1463        {
 21464            response.StatusCode = StatusCode;
 1465        }
 291466    }
 1467
 1468    /// <summary>
 1469    /// Adds caching headers to the response based on the provided CacheControlHeaderValue options.
 1470    /// </summary>
 1471    /// <param name="response">The HTTP response to apply caching headers to.</param>
 1472    /// <exception cref="ArgumentNullException">Thrown when options is null.</exception>
 1473    public void ApplyCachingHeaders(HttpResponse response)
 1474    {
 291475        if (CacheControl is not null)
 1476        {
 11477            response.Headers.CacheControl = CacheControl.ToString();
 1478        }
 291479    }
 1480
 1481    /// <summary>
 1482    /// Applies the Content-Disposition header to the HTTP response.
 1483    /// </summary>
 1484    /// <param name="response">The HTTP response to apply the header to.</param>
 1485    private void ApplyContentDispositionHeader(HttpResponse response)
 1486    {
 251487        if (ContentDisposition.Type == ContentDispositionType.NoContentDisposition)
 1488        {
 231489            return;
 1490        }
 1491
 21492        if (Log.IsEnabled(LogEventLevel.Debug))
 1493        {
 21494            Log.Debug("Setting Content-Disposition header, Type={Type}, FileName={FileName}",
 21495                      ContentDisposition.Type, ContentDisposition.FileName);
 1496        }
 1497
 21498        var dispositionValue = ContentDisposition.Type switch
 21499        {
 21500            ContentDispositionType.Attachment => "attachment",
 01501            ContentDispositionType.Inline => "inline",
 01502            _ => throw new InvalidOperationException("Invalid Content-Disposition type")
 21503        };
 1504
 21505        if (string.IsNullOrEmpty(ContentDisposition.FileName) && Body is IFileInfo fi)
 1506        {
 1507            // default filename: use the file's name
 11508            ContentDisposition.FileName = fi.Name;
 1509        }
 1510
 21511        if (!string.IsNullOrEmpty(ContentDisposition.FileName))
 1512        {
 21513            var escapedFileName = WebUtility.UrlEncode(ContentDisposition.FileName);
 21514            dispositionValue += $"; filename=\"{escapedFileName}\"";
 1515        }
 1516
 21517        response.Headers.Append("Content-Disposition", dispositionValue);
 21518    }
 1519
 1520    /// <summary>
 1521    /// Applies headers and cookies to the HTTP response.
 1522    /// </summary>
 1523    /// <param name="response">The HTTP response to apply the headers and cookies to.</param>
 1524    private void ApplyHeadersAndCookies(HttpResponse response)
 1525    {
 291526        if (Headers is not null)
 1527        {
 581528            foreach (var kv in Headers)
 1529            {
 01530                response.Headers[kv.Key] = kv.Value;
 1531            }
 1532        }
 291533        if (Cookies is not null)
 1534        {
 61535            foreach (var cookie in Cookies)
 1536            {
 21537                response.Headers.Append("Set-Cookie", cookie);
 1538            }
 1539        }
 291540    }
 1541
 1542    /// <summary>
 1543    /// Writes the response body to the HTTP response.
 1544    /// </summary>
 1545    /// <param name="response">The HTTP response to write to.</param>
 1546    /// <returns>A task representing the asynchronous operation.</returns>
 1547    private async Task WriteBodyAsync(HttpResponse response)
 1548    {
 251549        var bodyValue = Body; // capture to avoid nullability warnings when mutated in default
 1550        switch (bodyValue)
 1551        {
 1552            case IFileInfo fileInfo:
 11553                Log.Debug("Sending file {FileName} (Length={Length})", fileInfo.Name, fileInfo.Length);
 11554                response.ContentLength = fileInfo.Length;
 11555                response.Headers.LastModified = fileInfo.LastModified.ToString("R");
 11556                await response.SendFileAsync(
 11557                    file: fileInfo,
 11558                    offset: 0,
 11559                    count: fileInfo.Length,
 11560                    cancellationToken: response.HttpContext.RequestAborted
 11561                );
 11562                break;
 1563
 1564            case byte[] bytes:
 11565                response.ContentLength = bytes.LongLength;
 11566                await response.Body.WriteAsync(bytes, response.HttpContext.RequestAborted);
 11567                await response.Body.FlushAsync(response.HttpContext.RequestAborted);
 11568                break;
 1569
 1570            case Stream stream:
 21571                var seekable = stream.CanSeek;
 21572                Log.Debug("Sending stream (seekable={Seekable}, len={Len})",
 21573                          seekable, seekable ? stream.Length : -1);
 1574
 21575                if (seekable)
 1576                {
 11577                    response.ContentLength = stream.Length;
 11578                    stream.Position = 0;
 1579                }
 1580                else
 1581                {
 11582                    response.ContentLength = null;
 1583                }
 1584
 1585                const int BufferSize = 64 * 1024; // 64 KB
 21586                var buffer = ArrayPool<byte>.Shared.Rent(BufferSize);
 1587                try
 1588                {
 1589                    int bytesRead;
 41590                    while ((bytesRead = await stream.ReadAsync(buffer.AsMemory(0, BufferSize), response.HttpContext.Requ
 1591                    {
 21592                        await response.Body.WriteAsync(buffer.AsMemory(0, bytesRead), response.HttpContext.RequestAborte
 1593                    }
 21594                }
 1595                finally
 1596                {
 21597                    ArrayPool<byte>.Shared.Return(buffer);
 1598                }
 21599                await response.Body.FlushAsync(response.HttpContext.RequestAborted);
 21600                break;
 1601
 1602            case string str:
 211603                var data = AcceptCharset.GetBytes(str);
 211604                response.ContentLength = data.Length;
 211605                await response.Body.WriteAsync(data, response.HttpContext.RequestAborted);
 211606                await response.Body.FlushAsync(response.HttpContext.RequestAborted);
 211607                break;
 1608
 1609            default:
 01610                var bodyType = bodyValue?.GetType().Name ?? "null";
 01611                Body = "Unsupported body type: " + bodyType;
 01612                Log.Warning("Unsupported body type: {BodyType}", bodyType);
 01613                response.StatusCode = StatusCodes.Status500InternalServerError;
 01614                response.ContentType = "text/plain; charset=utf-8";
 01615                response.ContentLength = Body.ToString()?.Length ?? null;
 1616                break;
 1617        }
 251618    }
 1619    #endregion
 1620}

Methods/Properties

get_CallbackPlan()
get_Logger()
get_MapRouteOptions()
get_KrContext()
get_Host()
.cctor()
.ctor(Kestrun.Models.KestrunContext,System.Int32)
get_Context()
get_StatusCode()
get_Headers()
get_ContentType()
get_Body()
get_RedirectUrl()
get_Cookies()
get_Encoding()
get_ContentDisposition()
get_Request()
get_AcceptCharset()
get_BodyAsyncThreshold()
get_CacheControl()
GetSafeCurrentDirectoryOrBaseDirectory()
GetSafeCurrentDirectoryForLogging()
GetHeader(System.String)
IsTextBasedContentType(System.String)
AddCallbackParameters(System.String,System.String,System.Collections.Generic.Dictionary`2<System.String,System.Object>)
WriteFileResponse(System.String,System.String,System.Int32)
WriteJsonResponse(System.Object,System.Int32)
WriteJsonResponseAsync()
WriteJsonResponse(System.Object,System.Text.Json.JsonSerializerOptions,System.Int32,System.String)
WriteJsonResponseAsync()
WriteJsonResponse(System.Object,System.Int32,System.Boolean,System.Int32,System.String)
WriteJsonResponseAsync()
WriteCborResponseAsync()
WriteCborResponse(System.Object,System.Int32,System.String)
WriteBsonResponseAsync()
WriteBsonResponse(System.Object,System.Int32,System.String)
WriteResponse(System.Object,System.Int32)
WriteResponseAsync()
SelectResponseMediaType(System.String,System.String)
GetMediaTypeOrNull(Microsoft.Net.Http.Headers.MediaTypeHeaderValue)
WriteByMediaTypeAsync(System.String,System.Object,System.Int32)
WriteCsvResponse(System.Object,System.Int32,System.String,CsvHelper.Configuration.CsvConfiguration)
WriteCsvResponseAsync()
WriteYamlResponse(System.Object,System.Int32,System.String)
WriteYamlResponseAsync()
WriteXmlResponse(System.Object,System.Int32,System.String,System.String,System.Boolean)
WriteXmlResponseAsync()
WriteTextResponse(System.Object,System.Int32,System.String)
WriteTextResponseAsync()
WriteFormUrlEncodedResponse(System.Object,System.Int32)
WriteFormUrlEncodedResponseAsync()
WriteRedirectResponse(System.String,System.String)
WriteBinaryResponse(System.Byte[],System.Int32,System.String)
WriteStreamResponse(System.IO.Stream,System.Int32,System.String)
get_Error()
get_Details()
get_Exception()
get_StackTrace()
get_Status()
get_Reason()
get_Timestamp()
get_Path()
get_Method()
WriteErrorResponseAsync()
WriteErrorResponse(System.String,System.Int32,System.String,System.String)
WriteErrorResponseAsync()
WriteErrorResponse(System.Exception,System.Int32,System.String,System.Boolean)
WriteFormattedErrorResponseAsync()
RenderInlineTemplate(System.String,System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.Object>)
RenderInline(System.String,System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.Object>)
TryResolveValue(System.String,System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.Object>,System.Object&)
RevalidateCache(System.Object,System.String,System.Boolean,System.Nullable`1<System.DateTimeOffset>)
WriteHtmlResponseAsync()
WriteHtmlResponseAsync()
WriteHtmlResponse(System.Byte[],System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.Object>,System.Int32)
WriteHtmlResponseFromFileAsync()
WriteHtmlResponse(System.String,System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.Object>,System.Int32)
WriteHtmlResponseFromFile(System.String,System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.Object>,System.Int32)
WriteStatusOnly(System.Int32)
ApplyTo()
TryEnqueueCallbacks()
EnsureContentType(Microsoft.AspNetCore.Http.HttpResponse)
EnsureStatus(Microsoft.AspNetCore.Http.HttpResponse)
ApplyCachingHeaders(Microsoft.AspNetCore.Http.HttpResponse)
ApplyContentDispositionHeader(Microsoft.AspNetCore.Http.HttpResponse)
ApplyHeadersAndCookies(Microsoft.AspNetCore.Http.HttpResponse)
WriteBodyAsync()