< 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@9d3a582b2d63930269564a7591aa77ef297cadeb
Line coverage
89%
Covered lines: 404
Uncovered lines: 48
Coverable lines: 452
Total lines: 1231
Line coverage: 89.3%
Branch coverage
73%
Covered branches: 192
Total branches: 260
Branch coverage: 73.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)75%44100%
.cctor()100%11100%
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%
GetHeader(...)0%620%
DetermineContentType(...)75%44100%
IsTextBasedContentType(...)100%1010100%
WriteFileResponse(...)78.57%141496.29%
WriteJsonResponse(...)100%11100%
WriteJsonResponseAsync()100%11100%
WriteJsonResponse(...)100%210%
WriteJsonResponseAsync()75%44100%
WriteJsonResponse(...)100%210%
WriteJsonResponseAsync()75%44100%
WriteCborResponseAsync()75%44100%
WriteCborResponse(...)100%210%
WriteBsonResponseAsync()75%44100%
WriteBsonResponse(...)100%210%
WriteResponseAsync()30%171058.33%
WriteResponse(...)100%210%
WriteCsvResponse(...)100%88100%
WriteCsvResponseAsync()75%44100%
WriteYamlResponse(...)100%11100%
WriteYamlResponseAsync()75%44100%
WriteXmlResponse(...)100%11100%
WriteXmlResponseAsync()75%44100%
WriteTextResponse(...)100%11100%
WriteTextResponseAsync()83.33%6685.71%
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%
WriteHtmlResponseAsync()66.66%6683.33%
WriteHtmlResponseFromFileAsync()75%4475%
WriteHtmlResponse(...)100%210%
WriteHtmlResponseFromFile(...)100%210%
ApplyTo()100%111081.25%
EnsureStatusAndContentType(...)100%66100%
ApplyContentDispositionHeader(...)78.57%141488.88%
ApplyHeadersAndCookies(...)50%13857.14%
WriteBodyAsync()59.09%242285.36%

File(s)

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

#LineLine coverage
 1
 2using System.Xml.Linq;
 3using Newtonsoft.Json;
 4using Newtonsoft.Json.Serialization;
 5using Microsoft.AspNetCore.StaticFiles;
 6using System.Text;
 7using Serilog;
 8using Serilog.Events;
 9using System.Buffers;
 10using Microsoft.Extensions.FileProviders;
 11using Microsoft.AspNetCore.WebUtilities;
 12using System.Net;
 13using MongoDB.Bson;
 14using Kestrun.Utilities;
 15using System.Collections;
 16using CsvHelper.Configuration;
 17using System.Globalization;
 18using CsvHelper;
 19using System.Reflection;
 20
 21namespace Kestrun.Models;
 22
 23/// <summary>
 24/// Represents an HTTP response in the Kestrun framework, providing methods to write various content types and manage he
 25/// </summary>
 26/// <remarks>
 27/// Initializes a new instance of the <see cref="KestrunResponse"/> class with the specified request and optional body a
 28/// </remarks>
 29/// <param name="request">The associated <see cref="KestrunRequest"/> for this response.</param>
 30/// <param name="bodyAsyncThreshold">The threshold in bytes for using async body write operations. Defaults to 8192.</pa
 7231public class KestrunResponse(KestrunRequest request, int bodyAsyncThreshold = 8192)
 32{
 33    /// <summary>
 34    /// A set of MIME types that are considered text-based for response content.
 35    /// </summary>
 136    public static readonly HashSet<string> TextBasedMimeTypes =
 137    new(StringComparer.OrdinalIgnoreCase)
 138    {
 139        "application/json",
 140        "application/xml",
 141        "application/javascript",
 142        "application/xhtml+xml",
 143        "application/x-www-form-urlencoded",
 144        "application/yaml",
 145        "application/graphql"
 146    };
 47
 48    /// <summary>
 49    /// Gets or sets the HTTP status code for the response.
 50    /// </summary>
 16251    public int StatusCode { get; set; } = 200;
 52    /// <summary>
 53    /// Gets or sets the collection of HTTP headers for the response.
 54    /// </summary>
 11255    public Dictionary<string, string> Headers { get; set; } = [];
 56    /// <summary>
 57    /// Gets or sets the MIME content type of the response.
 58    /// </summary>
 21959    public string ContentType { get; set; } = "text/plain";
 60    /// <summary>
 61    /// Gets or sets the body of the response, which can be a string, byte array, stream, or file info.
 62    /// </summary>
 12063    public object? Body { get; set; }
 64    /// <summary>
 65    /// Gets or sets the URL to redirect the response to, if an HTTP redirect is required.
 66    /// </summary>
 3567    public string? RedirectUrl { get; set; } // For HTTP redirects
 68    /// <summary>
 69    /// Gets or sets the list of Set-Cookie header values for the response.
 70    /// </summary>
 1571    public List<string>? Cookies { get; set; } // For Set-Cookie headers
 72
 73
 74    /// <summary>
 75    /// Text encoding for textual MIME types.
 76    /// </summary>
 9677    public Encoding Encoding { get; set; } = Encoding.UTF8;
 78
 79    /// <summary>
 80    /// Content-Disposition header value.
 81    /// </summary>
 10282    public ContentDispositionOptions ContentDisposition { get; set; } = new ContentDispositionOptions();
 83    /// <summary>
 84    /// Gets the associated KestrunRequest for this response.
 85    /// </summary>
 10786    public KestrunRequest Request { get; private set; } = request ?? throw new ArgumentNullException(nameof(request));
 87
 88    /// <summary>
 89    /// Global text encoding for all responses. Defaults to UTF-8.
 90    /// </summary>
 8691    public Encoding AcceptCharset { get; private set; } = request.Headers.TryGetValue("Accept-Charset", out var value) ?
 92
 93    /// <summary>
 94    /// If the response body is larger than this threshold (in bytes), async write will be used.
 95    /// </summary>
 7296    public int BodyAsyncThreshold { get; set; } = bodyAsyncThreshold;
 97
 98    #region Constructors
 99    #endregion
 100
 101    #region Helpers
 102    /// <summary>
 103    /// Retrieves the value of the specified header from the response headers.
 104    /// </summary>
 105    /// <param name="key">The name of the header to retrieve.</param>
 106    /// <returns>The value of the header if found; otherwise, null.</returns>
 0107    public string? GetHeader(string key) => Headers.TryGetValue(key, out var value) ? value : null;
 108
 109    private string DetermineContentType(string? contentType, string defaultType = "text/plain")
 110    {
 1111        if (string.IsNullOrWhiteSpace(contentType))
 112        {
 1113            _ = Request.Headers.TryGetValue("Accept", out var acceptHeader);
 1114            contentType = (acceptHeader ?? defaultType)
 1115                                 .ToLowerInvariant();
 116        }
 117
 1118        return contentType;
 119    }
 120
 121    /// <summary>
 122    /// Determines whether the specified content type is text-based or supports a charset.
 123    /// </summary>
 124    /// <param name="type">The MIME content type to check.</param>
 125    /// <returns>True if the content type is text-based; otherwise, false.</returns>
 126    public static bool IsTextBasedContentType(string type)
 127    {
 24128        if (Log.IsEnabled(LogEventLevel.Debug))
 129        {
 24130            Log.Debug("Checking if content type is text-based: {ContentType}", type);
 131        }
 132
 133        // Check if the content type is text-based or has a charset
 24134        if (string.IsNullOrEmpty(type))
 135        {
 1136            return false;
 137        }
 138
 23139        if (type.StartsWith("text/", StringComparison.OrdinalIgnoreCase))
 140        {
 14141            return true;
 142        }
 143
 144        // Include structured types using XML or JSON suffixes
 9145        if (type.EndsWith("+xml", StringComparison.OrdinalIgnoreCase) ||
 9146            type.EndsWith("+json", StringComparison.OrdinalIgnoreCase))
 147        {
 2148            return true;
 149        }
 150
 151        // Common application types where charset makes sense
 7152        return TextBasedMimeTypes.Contains(type);
 153    }
 154    #endregion
 155
 156    #region  Response Writers
 157    /// <summary>
 158    /// Writes a file response with the specified file path, content type, and HTTP status code.
 159    /// </summary>
 160    /// <param name="filePath">The path to the file to be sent in the response.</param>
 161    /// <param name="contentType">The MIME type of the file content.</param>
 162    /// <param name="statusCode">The HTTP status code for the response.</param>
 163    public void WriteFileResponse(
 164        string? filePath,
 165        string? contentType,
 166        int statusCode = StatusCodes.Status200OK
 167    )
 168    {
 2169        if (Log.IsEnabled(LogEventLevel.Debug))
 170        {
 2171            Log.Debug("Writing file response,FilePath={FilePath} StatusCode={StatusCode}, ContentType={ContentType}, Cur
 2172                filePath, statusCode, contentType, Directory.GetCurrentDirectory());
 173        }
 174
 2175        if (string.IsNullOrEmpty(filePath))
 176        {
 0177            throw new ArgumentException("File path cannot be null or empty.", nameof(filePath));
 178        }
 179
 2180        if (!File.Exists(filePath))
 181        {
 1182            StatusCode = StatusCodes.Status404NotFound;
 1183            Body = $"File not found: {filePath}";
 1184            ContentType = $"text/plain; charset={Encoding.WebName}";
 1185            return;
 186        }
 187        // 1. Make sure you have an absolute file path
 1188        var fullPath = Path.GetFullPath(filePath);
 189
 190        // 2. Extract the directory to use as the "root"
 1191        var directory = Path.GetDirectoryName(fullPath)
 1192                       ?? throw new InvalidOperationException("Could not determine directory from file path");
 193
 1194        if (Log.IsEnabled(LogEventLevel.Debug))
 195        {
 1196            Log.Debug("Serving file: {FilePath}", fullPath);
 197        }
 198
 199        // Create a physical file provider for the directory
 1200        var physicalProvider = new PhysicalFileProvider(directory);
 1201        var fi = physicalProvider.GetFileInfo(Path.GetFileName(filePath));
 1202        var provider = new FileExtensionContentTypeProvider();
 1203        contentType ??= provider.TryGetContentType(fullPath, out var ct)
 1204                ? ct
 1205                : "application/octet-stream";
 1206        Body = fi;
 207
 208        // headers & metadata
 1209        StatusCode = statusCode;
 1210        ContentType = contentType;
 1211        Log.Debug("File response prepared: FileName={FileName}, Length={Length}, ContentType={ContentType}",
 1212            fi.Name, fi.Length, ContentType);
 1213    }
 214
 215    /// <summary>
 216    /// Writes a JSON response with the specified input object and HTTP status code.
 217    /// </summary>
 218    /// <param name="inputObject">The object to be converted to JSON.</param>
 219    /// <param name="statusCode">The HTTP status code for the response.</param>
 1220    public void WriteJsonResponse(object? inputObject, int statusCode = StatusCodes.Status200OK) => WriteJsonResponseAsy
 221
 222    /// <summary>
 223    /// Asynchronously writes a JSON response with the specified input object and HTTP status code.
 224    /// </summary>
 225    /// <param name="inputObject">The object to be converted to JSON.</param>
 226    /// <param name="statusCode">The HTTP status code for the response.</param>
 3227    public async Task WriteJsonResponseAsync(object? inputObject, int statusCode = StatusCodes.Status200OK) => await Wri
 228
 229    /// <summary>
 230    /// Writes a JSON response using the specified input object and serializer settings.
 231    /// </summary>
 232    /// <param name="inputObject">The object to be converted to JSON.</param>
 233    /// <param name="serializerSettings">The settings to use for JSON serialization.</param>
 234    /// <param name="statusCode">The HTTP status code for the response.</param>
 235    /// <param name="contentType">The MIME type of the response content.</param>
 0236    public void WriteJsonResponse(object? inputObject, JsonSerializerSettings serializerSettings, int statusCode = Statu
 237
 238    /// <summary>
 239    /// Asynchronously writes a JSON response using the specified input object and serializer settings.
 240    /// </summary>
 241    /// <param name="inputObject">The object to be converted to JSON.</param>
 242    /// <param name="serializerSettings">The settings to use for JSON serialization.</param>
 243    /// <param name="statusCode">The HTTP status code for the response.</param>
 244    /// <param name="contentType">The MIME type of the response content.</param>
 245    public async Task WriteJsonResponseAsync(object? inputObject, JsonSerializerSettings serializerSettings, int statusC
 246    {
 4247        if (Log.IsEnabled(LogEventLevel.Debug))
 248        {
 4249            Log.Debug("Writing JSON response (async), StatusCode={StatusCode}, ContentType={ContentType}", statusCode, c
 250        }
 251
 8252        Body = await Task.Run(() => JsonConvert.SerializeObject(inputObject, serializerSettings));
 4253        ContentType = string.IsNullOrEmpty(contentType) ? $"application/json; charset={Encoding.WebName}" : contentType;
 4254        StatusCode = statusCode;
 4255    }
 256    /// <summary>
 257    /// Writes a JSON response with the specified input object, serialization depth, compression option, status code, an
 258    /// </summary>
 259    /// <param name="inputObject">The object to be converted to JSON.</param>
 260    /// <param name="depth">The maximum depth for JSON serialization.</param>
 261    /// <param name="compress">Whether to compress the JSON output (no indentation).</param>
 262    /// <param name="statusCode">The HTTP status code for the response.</param>
 263    /// <param name="contentType">The MIME type of the response content.</param>
 0264    public void WriteJsonResponse(object? inputObject, int depth, bool compress, int statusCode = StatusCodes.Status200O
 265
 266    /// <summary>
 267    /// Asynchronously writes a JSON response with the specified input object, serialization depth, compression option, 
 268    /// </summary>
 269    /// <param name="inputObject">The object to be converted to JSON.</param>
 270    /// <param name="depth">The maximum depth for JSON serialization.</param>
 271    /// <param name="compress">Whether to compress the JSON output (no indentation).</param>
 272    /// <param name="statusCode">The HTTP status code for the response.</param>
 273    /// <param name="contentType">The MIME type of the response content.</param>
 274    public async Task WriteJsonResponseAsync(object? inputObject, int depth, bool compress, int statusCode = StatusCodes
 275    {
 4276        if (Log.IsEnabled(LogEventLevel.Debug))
 277        {
 4278            Log.Debug("Writing JSON response (async), StatusCode={StatusCode}, ContentType={ContentType}, Depth={Depth},
 4279                statusCode, contentType, depth, compress);
 280        }
 281
 4282        var serializerSettings = new JsonSerializerSettings
 4283        {
 4284            Formatting = compress ? Formatting.None : Formatting.Indented,
 4285            ContractResolver = new CamelCasePropertyNamesContractResolver(),
 4286            ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
 4287            NullValueHandling = NullValueHandling.Ignore,
 4288            DefaultValueHandling = DefaultValueHandling.Ignore,
 4289            MaxDepth = depth
 4290        };
 4291        await WriteJsonResponseAsync(inputObject, serializerSettings: serializerSettings, statusCode: statusCode, conten
 4292    }
 293    /// <summary>
 294    /// Writes a CBOR response (binary, efficient, not human-readable).
 295    /// </summary>
 296    public async Task WriteCborResponseAsync(object? inputObject, int statusCode = StatusCodes.Status200OK, string? cont
 297    {
 2298        if (Log.IsEnabled(LogEventLevel.Debug))
 299        {
 2300            Log.Debug("Writing CBOR response, StatusCode={StatusCode}, ContentType={ContentType}", statusCode, contentTy
 301        }
 302
 303        // Serialize to CBOR using PeterO.Cbor
 4304        Body = await Task.Run(() => inputObject != null
 4305            ? PeterO.Cbor.CBORObject.FromObject(inputObject).EncodeToBytes()
 4306            : []);
 2307        ContentType = string.IsNullOrEmpty(contentType) ? "application/cbor" : contentType;
 2308        StatusCode = statusCode;
 2309    }
 310
 311    /// <summary>
 312    /// Writes a CBOR response (binary, efficient, not human-readable).
 313    /// </summary>
 314    /// <param name="inputObject">The object to be converted to CBOR.</param>
 315    /// <param name="statusCode">The HTTP status code for the response.</param>
 316    /// <param name="contentType">The MIME type of the response content.</param>
 0317    public void WriteCborResponse(object? inputObject, int statusCode = StatusCodes.Status200OK, string? contentType = n
 318
 319    /// <summary>
 320    /// Asynchronously writes a BSON response with the specified input object, status code, and content type.
 321    /// </summary>
 322    /// <param name="inputObject">The object to be converted to BSON.</param>
 323    /// <param name="statusCode">The HTTP status code for the response.</param>
 324    /// <param name="contentType">The MIME type of the response content.</param>
 325    public async Task WriteBsonResponseAsync(object? inputObject, int statusCode = StatusCodes.Status200OK, string? cont
 326    {
 1327        if (Log.IsEnabled(LogEventLevel.Debug))
 328        {
 1329            Log.Debug("Writing BSON response, StatusCode={StatusCode}, ContentType={ContentType}", statusCode, contentTy
 330        }
 331
 332        // Serialize to BSON (as byte[])
 2333        Body = await Task.Run(() => inputObject != null ? inputObject.ToBson() : []);
 1334        ContentType = string.IsNullOrEmpty(contentType) ? "application/bson" : contentType;
 1335        StatusCode = statusCode;
 1336    }
 337
 338    /// <summary>
 339    /// Writes a BSON response with the specified input object, status code, and content type.
 340    /// </summary>
 341    /// <param name="inputObject">The object to be converted to BSON.</param>
 342    /// <param name="statusCode">The HTTP status code for the response.</param>
 343    /// <param name="contentType">The MIME type of the response content.</param>
 0344    public void WriteBsonResponse(object? inputObject, int statusCode = StatusCodes.Status200OK, string? contentType = n
 345
 346    /// <summary>
 347    /// Asynchronously writes a response with the specified input object and HTTP status code.
 348    /// Chooses the response format based on the Accept header or defaults to text/plain.
 349    /// </summary>
 350    /// <param name="inputObject">The object to be sent in the response body.</param>
 351    /// <param name="statusCode">The HTTP status code for the response.</param>
 352    public async Task WriteResponseAsync(object? inputObject, int statusCode = StatusCodes.Status200OK)
 353    {
 1354        if (Log.IsEnabled(LogEventLevel.Debug))
 355        {
 1356            Log.Debug("Writing response, StatusCode={StatusCode}", statusCode);
 357        }
 358
 1359        Body = inputObject;
 1360        ContentType = DetermineContentType(contentType: string.Empty); // Ensure ContentType is set based on Accept head
 361
 1362        if (ContentType.Contains("json"))
 363        {
 1364            await WriteJsonResponseAsync(inputObject: inputObject, statusCode: statusCode);
 365        }
 0366        else if (ContentType.Contains("yaml") || ContentType.Contains("yml"))
 367        {
 0368            await WriteYamlResponseAsync(inputObject: inputObject, statusCode: statusCode);
 369        }
 0370        else if (ContentType.Contains("xml"))
 371        {
 0372            await WriteXmlResponseAsync(inputObject: inputObject, statusCode: statusCode);
 373        }
 374        else
 375        {
 0376            await WriteTextResponseAsync(inputObject: inputObject, statusCode: statusCode);
 377        }
 1378    }
 379
 380    /// <summary>
 381    /// Writes a response with the specified input object and HTTP status code.
 382    /// Chooses the response format based on the Accept header or defaults to text/plain.
 383    /// </summary>
 384    /// <param name="inputObject">The object to be sent in the response body.</param>
 385    /// <param name="statusCode">The HTTP status code for the response.</param>
 0386    public void WriteResponse(object? inputObject, int statusCode = StatusCodes.Status200OK) => WriteResponseAsync(input
 387
 388    /// <summary>
 389    /// Writes a CSV response with the specified input object, status code, content type, and optional CsvConfiguration.
 390    /// </summary>
 391    /// <param name="inputObject">The object to be converted to CSV.</param>
 392    /// <param name="statusCode">The HTTP status code for the response.</param>
 393    /// <param name="contentType">The MIME type of the response content.</param>
 394    /// <param name="config">An optional CsvConfiguration to customize CSV output.</param>
 395    public void WriteCsvResponse(
 396            object? inputObject,
 397            int statusCode = StatusCodes.Status200OK,
 398            string? contentType = null,
 399            CsvConfiguration? config = null)
 400    {
 2401        Action<CsvConfiguration>? tweaker = null;
 402
 2403        if (config is not null)
 404        {
 1405            tweaker = target =>
 1406            {
 90407                foreach (var prop in typeof(CsvConfiguration)
 1408                     .GetProperties(BindingFlags.Public | BindingFlags.Instance))
 1409                {
 44410                    if (prop.CanRead && prop.CanWrite)
 1411                    {
 44412                        var value = prop.GetValue(config);
 44413                        prop.SetValue(target, value);
 1414                    }
 1415                }
 2416            };
 417        }
 2418        WriteCsvResponseAsync(inputObject, statusCode, contentType, tweaker).GetAwaiter().GetResult();
 2419    }
 420
 421    /// <summary>
 422    /// Asynchronously writes a CSV response with the specified input object, status code, content type, and optional co
 423    /// </summary>
 424    /// <param name="inputObject">The object to be converted to CSV.</param>
 425    /// <param name="statusCode">The HTTP status code for the response.</param>
 426    /// <param name="contentType">The MIME type of the response content.</param>
 427    /// <param name="tweak">An optional action to tweak the CsvConfiguration.</param>
 428    public async Task WriteCsvResponseAsync(
 429        object? inputObject,
 430        int statusCode = StatusCodes.Status200OK,
 431        string? contentType = null,
 432        Action<CsvConfiguration>? tweak = null)
 433    {
 3434        if (Log.IsEnabled(LogEventLevel.Debug))
 435        {
 3436            Log.Debug("Writing CSV response (async), StatusCode={StatusCode}, ContentType={ContentType}",
 3437                      statusCode, contentType);
 438        }
 439
 440        // Serialize inside a background task so heavy reflection never blocks the caller
 3441        Body = await Task.Run(() =>
 3442        {
 3443            var cfg = new CsvConfiguration(CultureInfo.InvariantCulture)
 3444            {
 3445                HasHeaderRecord = true,
 3446                NewLine = Environment.NewLine
 3447            };
 3448            tweak?.Invoke(cfg);                         // let the caller flirt with the config
 3449
 3450            using var sw = new StringWriter();
 3451            using var csv = new CsvWriter(sw, cfg);
 3452
 3453            // CsvHelper insists on an enumerable; wrap single objects so it stays happy
 3454            if (inputObject is IEnumerable records and not string)
 3455            {
 3456                csv.WriteRecords(records);              // whole collections (IEnumerable<T>)
 3457            }
 0458            else if (inputObject is not null)
 3459            {
 0460                csv.WriteRecords([inputObject]); // lone POCO
 3461            }
 3462            else
 3463            {
 0464                csv.WriteHeader<object>();              // nothing? write only headers for an empty file
 3465            }
 3466
 3467            return sw.ToString();
 6468        }).ConfigureAwait(false);
 469
 3470        ContentType = string.IsNullOrEmpty(contentType)
 3471            ? $"text/csv; charset={Encoding.WebName}"
 3472            : contentType;
 3473        StatusCode = statusCode;
 3474    }
 475    /// <summary>
 476    /// Writes a YAML response with the specified input object, status code, and content type.
 477    /// </summary>
 478    /// <param name="inputObject">The object to be converted to YAML.</param>
 479    /// <param name="statusCode">The HTTP status code for the response.</param>
 480    /// <param name="contentType">The MIME type of the response content.</param>
 1481    public void WriteYamlResponse(object? inputObject, int statusCode = StatusCodes.Status200OK, string? contentType = n
 482
 483    /// <summary>
 484    /// Asynchronously writes a YAML response with the specified input object, status code, and content type.
 485    /// </summary>
 486    /// <param name="inputObject">The object to be converted to YAML.</param>
 487    /// <param name="statusCode">The HTTP status code for the response.</param>
 488    /// <param name="contentType">The MIME type of the response content.</param>
 489    public async Task WriteYamlResponseAsync(object? inputObject, int statusCode = StatusCodes.Status200OK, string? cont
 490    {
 3491        if (Log.IsEnabled(LogEventLevel.Debug))
 492        {
 3493            Log.Debug("Writing YAML response (async), StatusCode={StatusCode}, ContentType={ContentType}", statusCode, c
 494        }
 495
 6496        Body = await Task.Run(() => YamlHelper.ToYaml(inputObject));
 3497        ContentType = string.IsNullOrEmpty(contentType) ? $"application/yaml; charset={Encoding.WebName}" : contentType;
 3498        StatusCode = statusCode;
 3499    }
 500
 501    /// <summary>
 502    /// Writes an XML response with the specified input object, status code, and content type.
 503    /// </summary>
 504    /// <param name="inputObject">The object to be converted to XML.</param>
 505    /// <param name="statusCode">The HTTP status code for the response.</param>
 506    /// <param name="contentType">The MIME type of the response content.</param>
 1507    public void WriteXmlResponse(object? inputObject, int statusCode = StatusCodes.Status200OK, string? contentType = nu
 508
 509    /// <summary>
 510    /// Asynchronously writes an XML response with the specified input object, status code, and content type.
 511    /// </summary>
 512    /// <param name="inputObject">The object to be converted to XML.</param>
 513    /// <param name="statusCode">The HTTP status code for the response.</param>
 514    /// <param name="contentType">The MIME type of the response content.</param>
 515    public async Task WriteXmlResponseAsync(object? inputObject, int statusCode = StatusCodes.Status200OK, string? conte
 516    {
 3517        if (Log.IsEnabled(LogEventLevel.Debug))
 518        {
 3519            Log.Debug("Writing XML response (async), StatusCode={StatusCode}, ContentType={ContentType}", statusCode, co
 520        }
 521
 6522        var xml = await Task.Run(() => XmlHelper.ToXml("Response", inputObject));
 6523        Body = await Task.Run(() => xml.ToString(SaveOptions.DisableFormatting));
 3524        ContentType = string.IsNullOrEmpty(contentType) ? $"application/xml; charset={Encoding.WebName}" : contentType;
 3525        StatusCode = statusCode;
 3526    }
 527    /// <summary>
 528    /// Writes a text response with the specified input object, status code, and content type.
 529    /// </summary>
 530    /// <param name="inputObject">The object to be converted to a text response.</param>
 531    /// <param name="statusCode">The HTTP status code for the response.</param>
 532    /// <param name="contentType">The MIME type of the response content.</param>
 4533    public void WriteTextResponse(object? inputObject, int statusCode = StatusCodes.Status200OK, string? contentType = n
 534
 535    /// <summary>
 536    /// Asynchronously writes a text response with the specified input object, status code, and content type.
 537    /// </summary>
 538    /// <param name="inputObject">The object to be converted to a text response.</param>
 539    /// <param name="statusCode">The HTTP status code for the response.</param>
 540    /// <param name="contentType">The MIME type of the response content.</param>
 541    public async Task WriteTextResponseAsync(object? inputObject, int statusCode = StatusCodes.Status200OK, string? cont
 542    {
 17543        if (Log.IsEnabled(LogEventLevel.Debug))
 544        {
 17545            Log.Debug("Writing text response (async), StatusCode={StatusCode}, ContentType={ContentType}", statusCode, c
 546        }
 547
 17548        if (inputObject is null)
 549        {
 0550            throw new ArgumentNullException(nameof(inputObject), "Input object cannot be null for text response.");
 551        }
 552
 34553        Body = await Task.Run(() => inputObject?.ToString() ?? string.Empty);
 17554        ContentType = string.IsNullOrEmpty(contentType) ? $"text/plain; charset={Encoding.WebName}" : contentType;
 17555        StatusCode = statusCode;
 17556    }
 557
 558    /// <summary>
 559    /// Writes an HTTP redirect response with the specified URL and optional message.
 560    /// </summary>
 561    /// <param name="url">The URL to redirect to.</param>
 562    /// <param name="message">An optional message to include in the response body.</param>
 563    public void WriteRedirectResponse(string url, string? message = null)
 564    {
 5565        if (Log.IsEnabled(LogEventLevel.Debug))
 566        {
 5567            Log.Debug("Writing redirect response, StatusCode={StatusCode}, Location={Location}", StatusCode, url);
 568        }
 569
 5570        if (string.IsNullOrEmpty(url))
 571        {
 0572            throw new ArgumentNullException(nameof(url), "URL cannot be null for redirect response.");
 573        }
 574        // framework hook
 5575        RedirectUrl = url;
 576
 577        // HTTP status + Location header
 5578        StatusCode = StatusCodes.Status302Found;
 5579        Headers["Location"] = url;
 580
 5581        if (message is not null)
 582        {
 583            // include a body
 1584            Body = message;
 1585            ContentType = $"text/plain; charset={Encoding.WebName}";
 586        }
 587        else
 588        {
 589            // no body: clear any existing body/headers
 4590            Body = null;
 591            //ContentType = null;
 4592            _ = Headers.Remove("Content-Length");
 593        }
 4594    }
 595
 596    /// <summary>
 597    /// Writes a binary response with the specified data, status code, and content type.
 598    /// </summary>
 599    /// <param name="data">The binary data to send in the response.</param>
 600    /// <param name="statusCode">The HTTP status code for the response.</param>
 601    /// <param name="contentType">The MIME type of the response content.</param>
 602    public void WriteBinaryResponse(byte[] data, int statusCode = StatusCodes.Status200OK, string contentType = "applica
 603    {
 1604        if (Log.IsEnabled(LogEventLevel.Debug))
 605        {
 1606            Log.Debug("Writing binary response, StatusCode={StatusCode}, ContentType={ContentType}", statusCode, content
 607        }
 608
 1609        Body = data ?? throw new ArgumentNullException(nameof(data), "Data cannot be null for binary response.");
 1610        ContentType = contentType;
 1611        StatusCode = statusCode;
 1612    }
 613    /// <summary>
 614    /// Writes a stream response with the specified stream, status code, and content type.
 615    /// </summary>
 616    /// <param name="stream">The stream to send in the response.</param>
 617    /// <param name="statusCode">The HTTP status code for the response.</param>
 618    /// <param name="contentType">The MIME type of the response content.</param>
 619    public void WriteStreamResponse(Stream stream, int statusCode = StatusCodes.Status200OK, string contentType = "appli
 620    {
 3621        if (Log.IsEnabled(LogEventLevel.Debug))
 622        {
 3623            Log.Debug("Writing stream response, StatusCode={StatusCode}, ContentType={ContentType}", statusCode, content
 624        }
 625
 3626        Body = stream;
 3627        ContentType = contentType;
 3628        StatusCode = statusCode;
 3629    }
 630    #endregion
 631
 632    #region Error Responses
 633    /// <summary>
 634    /// Structured payload for error responses.
 635    /// </summary>
 636    internal record ErrorPayload
 637    {
 24638        public string Error { get; init; } = default!;
 25639        public string? Details { get; init; }
 27640        public string? Exception { get; init; }
 26641        public string? StackTrace { get; init; }
 48642        public int Status { get; init; }
 24643        public string Reason { get; init; } = default!;
 24644        public string Timestamp { get; init; } = default!;
 18645        public string? Path { get; init; }
 18646        public string? Method { get; init; }
 647    }
 648
 649    /// <summary>
 650    /// Write an error response with a custom message.
 651    /// Chooses JSON/YAML/XML/plain-text based on override → Accept → default JSON.
 652    /// </summary>
 653    public async Task WriteErrorResponseAsync(
 654        string message,
 655        int statusCode = StatusCodes.Status500InternalServerError,
 656        string? contentType = null,
 657        string? details = null)
 658    {
 9659        if (Log.IsEnabled(LogEventLevel.Debug))
 660        {
 9661            Log.Debug("Writing error response, StatusCode={StatusCode}, ContentType={ContentType}, Message={Message}",
 9662                statusCode, contentType, message);
 663        }
 664
 9665        if (string.IsNullOrWhiteSpace(message))
 666        {
 0667            throw new ArgumentNullException(nameof(message));
 668        }
 669
 9670        Log.Warning("Writing error response with status {StatusCode}: {Message}", statusCode, message);
 671
 9672        var payload = new ErrorPayload
 9673        {
 9674            Error = message,
 9675            Details = details,
 9676            Exception = null,
 9677            StackTrace = null,
 9678            Status = statusCode,
 9679            Reason = ReasonPhrases.GetReasonPhrase(statusCode),
 9680            Timestamp = DateTime.UtcNow.ToString("o"),
 9681            Path = Request?.Path,
 9682            Method = Request?.Method
 9683        };
 684
 9685        await WriteFormattedErrorResponseAsync(payload, contentType);
 9686    }
 687
 688    /// <summary>
 689    /// Writes an error response with a custom message.
 690    /// Chooses JSON/YAML/XML/plain-text based on override → Accept → default JSON.
 691    /// </summary>
 692    /// <param name="message">The error message to include in the response.</param>
 693    /// <param name="statusCode">The HTTP status code for the response.</param>
 694    /// <param name="contentType">The MIME type of the response content.</param>
 695    /// <param name="details">Optional details to include in the response.</param>
 696    public void WriteErrorResponse(
 697      string message,
 698      int statusCode = StatusCodes.Status500InternalServerError,
 699      string? contentType = null,
 1700      string? details = null) => WriteErrorResponseAsync(message, statusCode, contentType, details).GetAwaiter().GetResu
 701
 702
 703    /// <summary>
 704    /// Asynchronously writes an error response based on an exception.
 705    /// Chooses JSON/YAML/XML/plain-text based on override → Accept → default JSON.
 706    /// </summary>
 707    /// <param name="ex">The exception to report.</param>
 708    /// <param name="statusCode">The HTTP status code for the response.</param>
 709    /// <param name="contentType">The MIME type of the response content.</param>
 710    /// <param name="includeStack">Whether to include the stack trace in the response.</param>
 711    public async Task WriteErrorResponseAsync(
 712        Exception ex,
 713        int statusCode = StatusCodes.Status500InternalServerError,
 714        string? contentType = null,
 715        bool includeStack = true)
 716    {
 3717        if (Log.IsEnabled(LogEventLevel.Debug))
 718        {
 3719            Log.Debug("Writing error response from exception, StatusCode={StatusCode}, ContentType={ContentType}, Includ
 3720                statusCode, contentType, includeStack);
 721        }
 722
 3723        ArgumentNullException.ThrowIfNull(ex);
 724
 3725        Log.Warning(ex, "Writing error response with status {StatusCode}", statusCode);
 726
 3727        var payload = new ErrorPayload
 3728        {
 3729            Error = ex.Message,
 3730            Details = null,
 3731            Exception = ex.GetType().Name,
 3732            StackTrace = includeStack ? ex.ToString() : null,
 3733            Status = statusCode,
 3734            Reason = ReasonPhrases.GetReasonPhrase(statusCode),
 3735            Timestamp = DateTime.UtcNow.ToString("o"),
 3736            Path = Request?.Path,
 3737            Method = Request?.Method
 3738        };
 739
 3740        await WriteFormattedErrorResponseAsync(payload, contentType);
 3741    }
 742    /// <summary>
 743    /// Writes an error response based on an exception.
 744    /// Chooses JSON/YAML/XML/plain-text based on override → Accept → default JSON.
 745    /// </summary>
 746    /// <param name="ex">The exception to report.</param>
 747    /// <param name="statusCode">The HTTP status code for the response.</param>
 748    /// <param name="contentType">The MIME type of the response content.</param>
 749    /// <param name="includeStack">Whether to include the stack trace in the response.</param>
 750    public void WriteErrorResponse(
 751            Exception ex,
 752            int statusCode = StatusCodes.Status500InternalServerError,
 753            string? contentType = null,
 1754            bool includeStack = true) => WriteErrorResponseAsync(ex, statusCode, contentType, includeStack).GetAwaiter()
 755
 756    /// <summary>
 757    /// Internal dispatcher: serializes the payload according to the chosen content-type.
 758    /// </summary>
 759    private async Task WriteFormattedErrorResponseAsync(ErrorPayload payload, string? contentType = null)
 760    {
 12761        if (Log.IsEnabled(LogEventLevel.Debug))
 762        {
 12763            Log.Debug("Writing formatted error response, ContentType={ContentType}, Status={Status}", contentType, paylo
 764        }
 765
 12766        if (string.IsNullOrWhiteSpace(contentType))
 767        {
 10768            _ = Request.Headers.TryGetValue("Accept", out var acceptHeader);
 10769            contentType = (acceptHeader ?? "text/plain")
 10770                                 .ToLowerInvariant();
 771        }
 12772        if (contentType.Contains("json"))
 773        {
 2774            await WriteJsonResponseAsync(payload, payload.Status);
 775        }
 10776        else if (contentType.Contains("yaml") || contentType.Contains("yml"))
 777        {
 2778            await WriteYamlResponseAsync(payload, payload.Status);
 779        }
 8780        else if (contentType.Contains("xml"))
 781        {
 2782            await WriteXmlResponseAsync(payload, payload.Status);
 783        }
 784        else
 785        {
 786            // Plain-text fallback
 6787            var lines = new List<string>
 6788                {
 6789                    $"Status: {payload.Status} ({payload.Reason})",
 6790                    $"Error: {payload.Error}",
 6791                    $"Time: {payload.Timestamp}"
 6792                };
 793
 6794            if (!string.IsNullOrWhiteSpace(payload.Details))
 795            {
 1796                lines.Add("Details:\n" + payload.Details);
 797            }
 798
 6799            if (!string.IsNullOrWhiteSpace(payload.Exception))
 800            {
 3801                lines.Add($"Exception: {payload.Exception}");
 802            }
 803
 6804            if (!string.IsNullOrWhiteSpace(payload.StackTrace))
 805            {
 2806                lines.Add("StackTrace:\n" + payload.StackTrace);
 807            }
 808
 6809            var text = string.Join("\n", lines);
 6810            await WriteTextResponseAsync(text, payload.Status, "text/plain");
 811        }
 12812    }
 813
 814    #endregion
 815    #region HTML Response Helpers
 816
 817    /// <summary>
 818    /// Renders a template string by replacing placeholders in the format {{key}} with corresponding values from the pro
 819    /// </summary>
 820    /// <param name="template">The template string containing placeholders.</param>
 821    /// <param name="vars">A dictionary of variables to replace in the template.</param>
 822    /// <returns>The rendered string with placeholders replaced by variable values.</returns>
 823    private static string RenderInlineTemplate(
 824     string template,
 825     IReadOnlyDictionary<string, object?> vars)
 826    {
 2827        if (Log.IsEnabled(LogEventLevel.Debug))
 828        {
 2829            Log.Debug("Rendering inline template, TemplateLength={TemplateLength}, VarsCount={VarsCount}",
 2830                      template?.Length ?? 0, vars?.Count ?? 0);
 831        }
 832
 2833        if (string.IsNullOrEmpty(template))
 834        {
 0835            return string.Empty;
 836        }
 837
 2838        if (vars is null || vars.Count == 0)
 839        {
 0840            return template;
 841        }
 842
 2843        var render = RenderInline(template, vars);
 844
 2845        if (Log.IsEnabled(LogEventLevel.Debug))
 846        {
 2847            Log.Debug("Rendered template length: {RenderedLength}", render.Length);
 848        }
 849
 2850        return render;
 851    }
 852
 853    /// <summary>
 854    /// Renders a template string by replacing placeholders in the format {{key}} with corresponding values from the pro
 855    /// </summary>
 856    /// <param name="template">The template string containing placeholders.</param>
 857    /// <param name="vars">A dictionary of variables to replace in the template.</param>
 858    /// <returns>The rendered string with placeholders replaced by variable values.</returns>
 859    private static string RenderInline(string template, IReadOnlyDictionary<string, object?> vars)
 860    {
 2861        var sb = new StringBuilder(template.Length);
 862
 863        // Iterate through the template
 2864        var i = 0;
 39865        while (i < template.Length)
 866        {
 867            // opening “{{”
 37868            if (template[i] == '{' && i + 1 < template.Length && template[i + 1] == '{')
 869            {
 3870                var start = i + 2;                                        // after “{{”
 3871                var end = template.IndexOf("}}", start, StringComparison.Ordinal);
 872
 3873                if (end > start)                                          // found closing “}}”
 874                {
 3875                    var rawKey = template[start..end].Trim();
 876
 3877                    if (TryResolveValue(rawKey, vars, out var value) && value is not null)
 878                    {
 3879                        _ = sb.Append(value); // append resolved value
 880                    }
 881                    else
 882                    {
 0883                        _ = sb.Append("{{").Append(rawKey).Append("}}");      // leave it as-is if unknown
 884                    }
 885
 3886                    i = end + 2;    // jump past the “}}”
 3887                    continue;
 888                }
 889            }
 890
 891            // ordinary character
 34892            _ = sb.Append(template[i]);
 34893            i++; // move to the next character
 894        }
 2895        return sb.ToString();
 896    }
 897
 898
 899
 900    /// <summary>
 901    /// Resolves a dotted path like “Request.Path” through nested dictionaries
 902    /// and/or object properties (case-insensitive).
 903    /// </summary>
 904    private static bool TryResolveValue(
 905        string path,
 906        IReadOnlyDictionary<string, object?> root,
 907        out object? value)
 908    {
 3909        value = null;
 910
 3911        if (string.IsNullOrWhiteSpace(path))
 912        {
 0913            return false;
 914        }
 915
 3916        object? current = root;
 16917        foreach (var segment in path.Split('.'))
 918        {
 5919            if (current is null)
 920            {
 0921                return false;
 922            }
 923
 924            // ① Handle dictionary look-ups (IReadOnlyDictionary or IDictionary)
 5925            if (current is IReadOnlyDictionary<string, object?> roDict)
 926            {
 3927                if (!roDict.TryGetValue(segment, out current))
 928                {
 0929                    return false;
 930                }
 931
 932                continue;
 933            }
 934
 2935            if (current is IDictionary dict)
 936            {
 0937                if (!dict.Contains(segment))
 938                {
 0939                    return false;
 940                }
 941
 0942                current = dict[segment];
 0943                continue;
 944            }
 945
 946            // ② Handle property look-ups via reflection
 2947            var prop = current.GetType().GetProperty(
 2948                segment,
 2949                BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
 950
 2951            if (prop is null)
 952            {
 0953                return false;
 954            }
 955
 2956            current = prop.GetValue(current);
 957        }
 958
 3959        value = current;
 3960        return true;
 961    }
 962
 963
 964    /// <summary>
 965    /// Asynchronously writes an HTML response, rendering the provided template string and replacing placeholders with v
 966    /// </summary>
 967    /// <param name="template">The HTML template string containing placeholders.</param>
 968    /// <param name="vars">A dictionary of variables to replace in the template.</param>
 969    /// <param name="statusCode">The HTTP status code for the response.</param>
 970    public async Task WriteHtmlResponseAsync(
 971        string template,
 972        IReadOnlyDictionary<string, object?>? vars,
 973        int statusCode = 200)
 974    {
 2975        if (Log.IsEnabled(LogEventLevel.Debug))
 976        {
 2977            Log.Debug("Writing HTML response (async), StatusCode={StatusCode}, TemplateLength={TemplateLength}", statusC
 978        }
 979
 2980        if (vars is null || vars.Count == 0)
 981        {
 0982            await WriteTextResponseAsync(template, statusCode, "text/html");
 983        }
 984        else
 985        {
 2986            await WriteTextResponseAsync(RenderInlineTemplate(template, vars), statusCode, "text/html");
 987        }
 2988    }
 989
 990    /// <summary>
 991    /// Asynchronously reads an HTML file, merges in placeholders from the provided dictionary, and writes the result as
 992    /// </summary>
 993    /// <param name="filePath">The path to the HTML file to read.</param>
 994    /// <param name="vars">A dictionary of variables to replace in the template.</param>
 995    /// <param name="statusCode">The HTTP status code for the response.</param>
 996    public async Task WriteHtmlResponseFromFileAsync(
 997        string filePath,
 998        IReadOnlyDictionary<string, object?> vars,
 999        int statusCode = 200)
 1000    {
 11001        if (Log.IsEnabled(LogEventLevel.Debug))
 1002        {
 11003            Log.Debug("Writing HTML response from file (async), FilePath={FilePath}, StatusCode={StatusCode}", filePath,
 1004        }
 1005
 11006        if (!File.Exists(filePath))
 1007        {
 01008            WriteTextResponse($"<!-- File not found: {filePath} -->", 404, "text/html");
 01009            return;
 1010        }
 1011
 11012        var template = await File.ReadAllTextAsync(filePath);
 11013        WriteHtmlResponseAsync(template, vars, statusCode).GetAwaiter().GetResult();
 11014    }
 1015
 1016
 1017    /// <summary>
 1018    /// Renders the given HTML string with placeholders and writes it as a response.
 1019    /// </summary>
 1020    /// <param name="template">The HTML template string containing placeholders.</param>
 1021    /// <param name="vars">A dictionary of variables to replace in the template.</param>
 1022    /// <param name="statusCode">The HTTP status code for the response.</param>
 1023    public void WriteHtmlResponse(
 1024        string template,
 1025        IReadOnlyDictionary<string, object?>? vars,
 01026        int statusCode = 200) => WriteHtmlResponseAsync(template, vars, statusCode).GetAwaiter().GetResult();
 1027
 1028    /// <summary>
 1029    /// Reads an .html file, merges in placeholders, and writes it.
 1030    /// </summary>
 1031    public void WriteHtmlResponseFromFile(
 1032        string filePath,
 1033        IReadOnlyDictionary<string, object?> vars,
 01034        int statusCode = 200) => WriteHtmlResponseFromFileAsync(filePath, vars, statusCode).GetAwaiter().GetResult();
 1035
 1036    #endregion
 1037
 1038    #region Apply to HttpResponse
 1039    /// <summary>
 1040    /// Applies the current KestrunResponse to the specified HttpResponse, setting status, headers, cookies, and writing
 1041    /// </summary>
 1042    /// <param name="response">The HttpResponse to apply the response to.</param>
 1043    /// <returns>A task representing the asynchronous operation.</returns>
 1044    public async Task ApplyTo(HttpResponse response)
 1045    {
 161046        if (Log.IsEnabled(LogEventLevel.Debug))
 1047        {
 161048            Log.Debug("Applying KestrunResponse to HttpResponse, StatusCode={StatusCode}, ContentType={ContentType}, Bod
 161049                StatusCode, ContentType, Body?.GetType().Name ?? "null");
 1050        }
 1051
 161052        if (!string.IsNullOrEmpty(RedirectUrl))
 1053        {
 11054            response.Redirect(RedirectUrl);
 11055            return;
 1056        }
 1057
 1058        try
 1059        {
 151060            EnsureStatusAndContentType(response);
 151061            ApplyContentDispositionHeader(response);
 151062            ApplyHeadersAndCookies(response);
 151063            if (Body is not null)
 1064            {
 131065                await WriteBodyAsync(response).ConfigureAwait(false);
 1066            }
 151067        }
 01068        catch (Exception ex)
 1069        {
 01070            Console.WriteLine($"Error applying response: {ex.Message}");
 1071            // Optionally, you can log the exception or handle it as needed
 01072            throw;
 1073        }
 161074    }
 1075
 1076    /// <summary>
 1077    /// Ensures the HTTP response has the correct status code and content type.
 1078    /// </summary>
 1079    /// <param name="response">The HTTP response to apply the status and content type to.</param>
 1080    private void EnsureStatusAndContentType(HttpResponse response)
 1081    {
 151082        response.StatusCode = StatusCode;
 151083        if (!string.IsNullOrEmpty(ContentType) &&
 151084            IsTextBasedContentType(ContentType) &&
 151085            !ContentType.Contains("charset=", StringComparison.OrdinalIgnoreCase))
 1086        {
 41087            ContentType = ContentType.TrimEnd(';') + $"; charset={AcceptCharset.WebName}";
 1088        }
 151089        response.ContentType = ContentType;
 151090    }
 1091
 1092    /// <summary>
 1093    /// Applies the Content-Disposition header to the HTTP response.
 1094    /// </summary>
 1095    /// <param name="response">The HTTP response to apply the header to.</param>
 1096    private void ApplyContentDispositionHeader(HttpResponse response)
 1097    {
 151098        if (ContentDisposition.Type == ContentDispositionType.NoContentDisposition)
 1099        {
 131100            return;
 1101        }
 1102
 21103        if (Log.IsEnabled(LogEventLevel.Debug))
 1104        {
 21105            Log.Debug("Setting Content-Disposition header, Type={Type}, FileName={FileName}",
 21106                      ContentDisposition.Type, ContentDisposition.FileName);
 1107        }
 1108
 21109        var dispositionValue = ContentDisposition.Type switch
 21110        {
 21111            ContentDispositionType.Attachment => "attachment",
 01112            ContentDispositionType.Inline => "inline",
 01113            _ => throw new InvalidOperationException("Invalid Content-Disposition type")
 21114        };
 1115
 21116        if (string.IsNullOrEmpty(ContentDisposition.FileName) && Body is IFileInfo fi)
 1117        {
 1118            // default filename: use the file's name
 11119            ContentDisposition.FileName = fi.Name;
 1120        }
 1121
 21122        if (!string.IsNullOrEmpty(ContentDisposition.FileName))
 1123        {
 21124            var escapedFileName = WebUtility.UrlEncode(ContentDisposition.FileName);
 21125            dispositionValue += $"; filename=\"{escapedFileName}\"";
 1126        }
 1127
 21128        response.Headers.Append("Content-Disposition", dispositionValue);
 21129    }
 1130
 1131    /// <summary>
 1132    /// Applies headers and cookies to the HTTP response.
 1133    /// </summary>
 1134    /// <param name="response">The HTTP response to apply the headers and cookies to.</param>
 1135    private void ApplyHeadersAndCookies(HttpResponse response)
 1136    {
 151137        if (Headers is not null)
 1138        {
 301139            foreach (var kv in Headers)
 1140            {
 01141                response.Headers[kv.Key] = kv.Value;
 1142            }
 1143        }
 151144        if (Cookies is not null)
 1145        {
 01146            foreach (var cookie in Cookies)
 1147            {
 01148                response.Headers.Append("Set-Cookie", cookie);
 1149            }
 1150        }
 151151    }
 1152
 1153    /// <summary>
 1154    /// Writes the response body to the HTTP response.
 1155    /// </summary>
 1156    /// <param name="response">The HTTP response to write to.</param>
 1157    /// <returns>A task representing the asynchronous operation.</returns>
 1158    private async Task WriteBodyAsync(HttpResponse response)
 1159    {
 131160        var bodyValue = Body; // capture to avoid nullability warnings when mutated in default
 1161        switch (bodyValue)
 1162        {
 1163            case IFileInfo fileInfo:
 11164                Log.Debug("Sending file {FileName} (Length={Length})", fileInfo.Name, fileInfo.Length);
 11165                response.ContentLength = fileInfo.Length;
 11166                response.Headers.LastModified = fileInfo.LastModified.ToString("R");
 11167                await response.SendFileAsync(
 11168                    file: fileInfo,
 11169                    offset: 0,
 11170                    count: fileInfo.Length,
 11171                    cancellationToken: response.HttpContext.RequestAborted
 11172                );
 11173                break;
 1174
 1175            case byte[] bytes:
 11176                response.ContentLength = bytes.LongLength;
 11177                await response.Body.WriteAsync(bytes, response.HttpContext.RequestAborted);
 11178                await response.Body.FlushAsync(response.HttpContext.RequestAborted);
 11179                break;
 1180
 1181            case Stream stream:
 21182                var seekable = stream.CanSeek;
 21183                Log.Debug("Sending stream (seekable={Seekable}, len={Len})",
 21184                          seekable, seekable ? stream.Length : -1);
 1185
 21186                if (seekable)
 1187                {
 11188                    response.ContentLength = stream.Length;
 11189                    stream.Position = 0;
 1190                }
 1191                else
 1192                {
 11193                    response.ContentLength = null;
 1194                }
 1195
 1196                const int BufferSize = 64 * 1024; // 64 KB
 21197                var buffer = ArrayPool<byte>.Shared.Rent(BufferSize);
 1198                try
 1199                {
 1200                    int bytesRead;
 41201                    while ((bytesRead = await stream.ReadAsync(buffer.AsMemory(0, BufferSize), response.HttpContext.Requ
 1202                    {
 21203                        await response.Body.WriteAsync(buffer.AsMemory(0, bytesRead), response.HttpContext.RequestAborte
 1204                    }
 21205                }
 1206                finally
 1207                {
 21208                    ArrayPool<byte>.Shared.Return(buffer);
 1209                }
 21210                await response.Body.FlushAsync(response.HttpContext.RequestAborted);
 21211                break;
 1212
 1213            case string str:
 91214                var data = AcceptCharset.GetBytes(str);
 91215                response.ContentLength = data.Length;
 91216                await response.Body.WriteAsync(data, response.HttpContext.RequestAborted);
 91217                await response.Body.FlushAsync(response.HttpContext.RequestAborted);
 91218                break;
 1219
 1220            default:
 01221                var bodyType = bodyValue?.GetType().Name ?? "null";
 01222                Body = "Unsupported body type: " + bodyType;
 01223                Log.Warning("Unsupported body type: {BodyType}", bodyType);
 01224                response.StatusCode = StatusCodes.Status500InternalServerError;
 01225                response.ContentType = "text/plain; charset=utf-8";
 01226                response.ContentLength = Body.ToString()?.Length ?? null;
 1227                break;
 1228        }
 131229    }
 1230    #endregion
 1231}

Methods/Properties

.ctor(Kestrun.Models.KestrunRequest,System.Int32)
.cctor()
get_StatusCode()
get_Headers()
get_ContentType()
get_Body()
get_RedirectUrl()
get_Cookies()
get_Encoding()
get_ContentDisposition()
get_Request()
get_AcceptCharset()
get_BodyAsyncThreshold()
GetHeader(System.String)
DetermineContentType(System.String,System.String)
IsTextBasedContentType(System.String)
WriteFileResponse(System.String,System.String,System.Int32)
WriteJsonResponse(System.Object,System.Int32)
WriteJsonResponseAsync()
WriteJsonResponse(System.Object,Newtonsoft.Json.JsonSerializerSettings,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)
WriteResponseAsync()
WriteResponse(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)
WriteXmlResponseAsync()
WriteTextResponse(System.Object,System.Int32,System.String)
WriteTextResponseAsync()
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&)
WriteHtmlResponseAsync()
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)
ApplyTo()
EnsureStatusAndContentType(Microsoft.AspNetCore.Http.HttpResponse)
ApplyContentDispositionHeader(Microsoft.AspNetCore.Http.HttpResponse)
ApplyHeadersAndCookies(Microsoft.AspNetCore.Http.HttpResponse)
WriteBodyAsync()