From 89fe09b6e630fb99b03b2e49b1584ea36535248a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 06:57:22 +0000 Subject: [PATCH 1/8] Initial plan From 4e9b2f6c8fe0116ec7ebcf9f16302dffbe36f52e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 07:02:17 +0000 Subject: [PATCH 2/8] Fix ChatController streaming cancellation handling Agent-Logs-Url: https://github.com/IntelliTect/EssentialCSharp.Web/sessions/c4364690-d679-41d6-a271-69d6c45343af Co-authored-by: BenjaminMichaelis <22186029+BenjaminMichaelis@users.noreply.github.com> --- .../Controllers/ChatController.cs | 55 ++++++++++--------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/EssentialCSharp.Web/Controllers/ChatController.cs b/EssentialCSharp.Web/Controllers/ChatController.cs index 0fcd8577..72b03d88 100644 --- a/EssentialCSharp.Web/Controllers/ChatController.cs +++ b/EssentialCSharp.Web/Controllers/ChatController.cs @@ -15,13 +15,13 @@ namespace EssentialCSharp.Web.Controllers; [IgnoreAntiforgeryToken] public partial class ChatController : ControllerBase { - private readonly AIChatService _AiChatService; + private readonly AIChatService _AIChatService; private readonly ResponseIdValidationService _ResponseIdValidationService; private readonly ILogger _Logger; public ChatController(ILogger logger, AIChatService aiChatService, ResponseIdValidationService responseIdValidationService) { - _AiChatService = aiChatService; + _AIChatService = aiChatService; _ResponseIdValidationService = responseIdValidationService; _Logger = logger; } @@ -46,7 +46,7 @@ public async Task SendMessage([FromBody] ChatMessageRequest reque try { - var (response, responseId) = await _AiChatService.GetChatCompletion( + var (response, responseId) = await _AIChatService.GetChatCompletion( prompt: request.Message, previousResponseId: previousResponseId, enableContextualSearch: request.EnableContextualSearch, @@ -75,7 +75,7 @@ public async Task StreamMessage([FromBody] ChatMessageRequest request, Cancellat if (string.IsNullOrEmpty(request.Message)) { Response.StatusCode = 400; - await Response.WriteAsJsonAsync(new { error = "Message cannot be empty." }, CancellationToken.None); + await Response.WriteAsJsonAsync(new { error = "Message cannot be empty." }, cancellationToken); return; } @@ -83,7 +83,7 @@ public async Task StreamMessage([FromBody] ChatMessageRequest request, Cancellat if (string.IsNullOrEmpty(userId)) { Response.StatusCode = 401; - await Response.WriteAsJsonAsync(new { error = "Unauthorized." }, CancellationToken.None); + await Response.WriteAsJsonAsync(new { error = "Unauthorized." }, cancellationToken); return; } @@ -94,7 +94,7 @@ public async Task StreamMessage([FromBody] ChatMessageRequest request, Cancellat if (!_ResponseIdValidationService.ValidateResponseId(userId, previousResponseId)) { Response.StatusCode = 400; - await Response.WriteAsJsonAsync(new { error = "Invalid conversation context." }, CancellationToken.None); + await Response.WriteAsJsonAsync(new { error = "Invalid conversation context." }, cancellationToken); return; } @@ -104,7 +104,7 @@ public async Task StreamMessage([FromBody] ChatMessageRequest request, Cancellat try { - await foreach (var (text, responseId) in _AiChatService.GetChatCompletionStream( + await foreach (var (text, responseId) in _AIChatService.GetChatCompletionStream( prompt: request.Message, previousResponseId: previousResponseId, enableContextualSearch: request.EnableContextualSearch, @@ -133,30 +133,33 @@ public async Task StreamMessage([FromBody] ChatMessageRequest request, Cancellat await Response.WriteAsync("data: [DONE]\n\n", cancellationToken); await Response.Body.FlushAsync(cancellationToken); } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested || HttpContext.RequestAborted.IsCancellationRequested) + catch (OperationCanceledException) { LogChatStreamCancelled(_Logger, User.Identity?.Name); } - catch (ConversationContextLimitExceededException) when (!Response.HasStarted) - { - Response.StatusCode = 400; - Response.ContentType = "application/json"; - await Response.WriteAsJsonAsync(new { error = "This conversation has grown too long. Please start a new one.", errorCode = "context_limit_exceeded" }, CancellationToken.None); - } catch (ConversationContextLimitExceededException ex) { - LogChatStreamErrorMidStream(_Logger, ex, User.Identity?.Name); - try + if (!Response.HasStarted) { - await Response.WriteAsync("data: {\"type\":\"error\",\"message\":\"This conversation has grown too long. Please start a new one.\",\"errorCode\":\"context_limit_exceeded\"}\n\n", CancellationToken.None); - await Response.Body.FlushAsync(CancellationToken.None); + Response.StatusCode = 400; + Response.ContentType = "application/json"; + await Response.WriteAsJsonAsync(new { error = "This conversation has grown too long. Please start a new one.", errorCode = "context_limit_exceeded" }, cancellationToken); } - catch (Exception) + else { - // Best-effort write to an already-streaming response. Kestrel can throw - // IOException (connection reset), OperationCanceledException, or - // ObjectDisposedException on abrupt client disconnect — swallow all to - // avoid masking the original exception. + LogChatStreamErrorMidStream(_Logger, ex, User.Identity?.Name); + try + { + await Response.WriteAsync("data: {\"type\":\"error\",\"message\":\"This conversation has grown too long. Please start a new one.\",\"errorCode\":\"context_limit_exceeded\"}\n\n", cancellationToken); + await Response.Body.FlushAsync(cancellationToken); + } + catch (Exception) + { + // Best-effort write to an already-streaming response. Kestrel can throw + // IOException (connection reset), OperationCanceledException, or + // ObjectDisposedException on abrupt client disconnect — swallow all to + // avoid masking the original exception. + } } } catch (Exception ex) when (!Response.HasStarted) @@ -164,15 +167,15 @@ public async Task StreamMessage([FromBody] ChatMessageRequest request, Cancellat LogChatStreamErrorBeforeResponseStarted(_Logger, ex, User.Identity?.Name); Response.StatusCode = 500; Response.ContentType = "application/json"; - await Response.WriteAsJsonAsync(new { error = "Chat service unavailable" }, CancellationToken.None); + await Response.WriteAsJsonAsync(new { error = "Chat service unavailable" }, cancellationToken); } catch (Exception ex) { LogChatStreamErrorMidStream(_Logger, ex, User.Identity?.Name); try { - await Response.WriteAsync("data: {\"type\":\"error\",\"message\":\"Stream interrupted\"}\n\n", CancellationToken.None); - await Response.Body.FlushAsync(CancellationToken.None); + await Response.WriteAsync("data: {\"type\":\"error\",\"message\":\"Stream interrupted\"}\n\n", cancellationToken); + await Response.Body.FlushAsync(cancellationToken); } catch (Exception) { From 8397a8da7c4b01db5186e6bf46b8efd62874d386 Mon Sep 17 00:00:00 2001 From: Benjamin Michaelis Date: Wed, 13 May 2026 01:39:54 -0700 Subject: [PATCH 3/8] Potential fix for pull request finding 'Generic catch clause' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- EssentialCSharp.Web/Controllers/ChatController.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/EssentialCSharp.Web/Controllers/ChatController.cs b/EssentialCSharp.Web/Controllers/ChatController.cs index 72b03d88..57a9a9d7 100644 --- a/EssentialCSharp.Web/Controllers/ChatController.cs +++ b/EssentialCSharp.Web/Controllers/ChatController.cs @@ -1,3 +1,4 @@ +using System.IO; using System.Security.Claims; using System.Text.Json; using EssentialCSharp.Chat.Common.Services; @@ -153,12 +154,12 @@ public async Task StreamMessage([FromBody] ChatMessageRequest request, Cancellat await Response.WriteAsync("data: {\"type\":\"error\",\"message\":\"This conversation has grown too long. Please start a new one.\",\"errorCode\":\"context_limit_exceeded\"}\n\n", cancellationToken); await Response.Body.FlushAsync(cancellationToken); } - catch (Exception) + catch (Exception ex) when (ex is IOException or OperationCanceledException or ObjectDisposedException) { // Best-effort write to an already-streaming response. Kestrel can throw // IOException (connection reset), OperationCanceledException, or - // ObjectDisposedException on abrupt client disconnect — swallow all to - // avoid masking the original exception. + // ObjectDisposedException on abrupt client disconnect — swallow expected + // transport/disconnect exceptions to avoid masking the original exception. } } } From 223a9bfc4b235c601dd35b54f2dedfbdd7ade7b3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 09:46:42 +0000 Subject: [PATCH 4/8] Fix chat stream cancellation and write error handling Agent-Logs-Url: https://github.com/IntelliTect/EssentialCSharp.Web/sessions/ca7b0afb-1731-4a50-ac2e-b4d7152c972a Co-authored-by: BenjaminMichaelis <22186029+BenjaminMichaelis@users.noreply.github.com> --- .../Controllers/ChatController.cs | 44 ++++++++++++++++--- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/EssentialCSharp.Web/Controllers/ChatController.cs b/EssentialCSharp.Web/Controllers/ChatController.cs index 57a9a9d7..0130f99a 100644 --- a/EssentialCSharp.Web/Controllers/ChatController.cs +++ b/EssentialCSharp.Web/Controllers/ChatController.cs @@ -134,7 +134,7 @@ public async Task StreamMessage([FromBody] ChatMessageRequest request, Cancellat await Response.WriteAsync("data: [DONE]\n\n", cancellationToken); await Response.Body.FlushAsync(cancellationToken); } - catch (OperationCanceledException) + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested || HttpContext.RequestAborted.IsCancellationRequested) { LogChatStreamCancelled(_Logger, User.Identity?.Name); } @@ -142,9 +142,24 @@ public async Task StreamMessage([FromBody] ChatMessageRequest request, Cancellat { if (!Response.HasStarted) { + if (cancellationToken.IsCancellationRequested || HttpContext.RequestAborted.IsCancellationRequested) + return; + Response.StatusCode = 400; Response.ContentType = "application/json"; - await Response.WriteAsJsonAsync(new { error = "This conversation has grown too long. Please start a new one.", errorCode = "context_limit_exceeded" }, cancellationToken); + try + { + await Response.WriteAsJsonAsync(new { error = "This conversation has grown too long. Please start a new one.", errorCode = "context_limit_exceeded" }, cancellationToken); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested || HttpContext.RequestAborted.IsCancellationRequested) + { + } + catch (IOException) when (HttpContext.RequestAborted.IsCancellationRequested) + { + } + catch (ObjectDisposedException) when (HttpContext.RequestAborted.IsCancellationRequested) + { + } } else { @@ -154,7 +169,7 @@ public async Task StreamMessage([FromBody] ChatMessageRequest request, Cancellat await Response.WriteAsync("data: {\"type\":\"error\",\"message\":\"This conversation has grown too long. Please start a new one.\",\"errorCode\":\"context_limit_exceeded\"}\n\n", cancellationToken); await Response.Body.FlushAsync(cancellationToken); } - catch (Exception ex) when (ex is IOException or OperationCanceledException or ObjectDisposedException) + catch (Exception writeException) when (writeException is IOException or OperationCanceledException or ObjectDisposedException) { // Best-effort write to an already-streaming response. Kestrel can throw // IOException (connection reset), OperationCanceledException, or @@ -166,9 +181,24 @@ public async Task StreamMessage([FromBody] ChatMessageRequest request, Cancellat catch (Exception ex) when (!Response.HasStarted) { LogChatStreamErrorBeforeResponseStarted(_Logger, ex, User.Identity?.Name); + if (cancellationToken.IsCancellationRequested || HttpContext.RequestAborted.IsCancellationRequested) + return; + Response.StatusCode = 500; Response.ContentType = "application/json"; - await Response.WriteAsJsonAsync(new { error = "Chat service unavailable" }, cancellationToken); + try + { + await Response.WriteAsJsonAsync(new { error = "Chat service unavailable" }, cancellationToken); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested || HttpContext.RequestAborted.IsCancellationRequested) + { + } + catch (IOException) when (HttpContext.RequestAborted.IsCancellationRequested) + { + } + catch (ObjectDisposedException) when (HttpContext.RequestAborted.IsCancellationRequested) + { + } } catch (Exception ex) { @@ -178,12 +208,12 @@ public async Task StreamMessage([FromBody] ChatMessageRequest request, Cancellat await Response.WriteAsync("data: {\"type\":\"error\",\"message\":\"Stream interrupted\"}\n\n", cancellationToken); await Response.Body.FlushAsync(cancellationToken); } - catch (Exception) + catch (Exception writeException) when (writeException is IOException or OperationCanceledException or ObjectDisposedException) { // Best-effort write to an already-streaming response. Kestrel can throw // IOException (connection reset), OperationCanceledException, or - // ObjectDisposedException on abrupt client disconnect — swallow all to - // avoid masking the original exception. + // ObjectDisposedException on abrupt client disconnect — swallow expected + // transport/disconnect exceptions to avoid masking the original exception. } } } From ae6bf1037e81cde5459d460b8d37067e8804aa41 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 09:51:37 +0000 Subject: [PATCH 5/8] Document expected chat response write suppression Agent-Logs-Url: https://github.com/IntelliTect/EssentialCSharp.Web/sessions/ca7b0afb-1731-4a50-ac2e-b4d7152c972a Co-authored-by: BenjaminMichaelis <22186029+BenjaminMichaelis@users.noreply.github.com> --- EssentialCSharp.Web/Controllers/ChatController.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/EssentialCSharp.Web/Controllers/ChatController.cs b/EssentialCSharp.Web/Controllers/ChatController.cs index 0130f99a..1219c284 100644 --- a/EssentialCSharp.Web/Controllers/ChatController.cs +++ b/EssentialCSharp.Web/Controllers/ChatController.cs @@ -153,12 +153,15 @@ public async Task StreamMessage([FromBody] ChatMessageRequest request, Cancellat } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested || HttpContext.RequestAborted.IsCancellationRequested) { + // Best-effort write during an aborted request — no response body can be delivered. } catch (IOException) when (HttpContext.RequestAborted.IsCancellationRequested) { + // Expected client disconnect while attempting a best-effort error response write. } catch (ObjectDisposedException) when (HttpContext.RequestAborted.IsCancellationRequested) { + // Response stream can already be disposed after an abrupt client disconnect. } } else @@ -192,12 +195,15 @@ public async Task StreamMessage([FromBody] ChatMessageRequest request, Cancellat } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested || HttpContext.RequestAborted.IsCancellationRequested) { + // Best-effort write during an aborted request — no response body can be delivered. } catch (IOException) when (HttpContext.RequestAborted.IsCancellationRequested) { + // Expected client disconnect while attempting a best-effort error response write. } catch (ObjectDisposedException) when (HttpContext.RequestAborted.IsCancellationRequested) { + // Response stream can already be disposed after an abrupt client disconnect. } } catch (Exception ex) From f2ca026bb5221af6ebcab3b7752c6eb2651a32b9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 00:44:45 +0000 Subject: [PATCH 6/8] Relax stream cancellation filter after response start Agent-Logs-Url: https://github.com/IntelliTect/EssentialCSharp.Web/sessions/c0627de3-aa52-4d77-a845-1420ecbe2416 Co-authored-by: BenjaminMichaelis <22186029+BenjaminMichaelis@users.noreply.github.com> --- EssentialCSharp.Web/Controllers/ChatController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EssentialCSharp.Web/Controllers/ChatController.cs b/EssentialCSharp.Web/Controllers/ChatController.cs index 1219c284..754a7b56 100644 --- a/EssentialCSharp.Web/Controllers/ChatController.cs +++ b/EssentialCSharp.Web/Controllers/ChatController.cs @@ -134,7 +134,7 @@ public async Task StreamMessage([FromBody] ChatMessageRequest request, Cancellat await Response.WriteAsync("data: [DONE]\n\n", cancellationToken); await Response.Body.FlushAsync(cancellationToken); } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested || HttpContext.RequestAborted.IsCancellationRequested) + catch (OperationCanceledException) when (Response.HasStarted || cancellationToken.IsCancellationRequested || HttpContext.RequestAborted.IsCancellationRequested) { LogChatStreamCancelled(_Logger, User.Identity?.Name); } From 9038dbd0a37d03ad6023887b4519402b548cd863 Mon Sep 17 00:00:00 2001 From: Benjamin Michaelis Date: Fri, 15 May 2026 23:06:14 -0700 Subject: [PATCH 7/8] Harden forwarded headers trust configuration Move forwarded headers proxy/CIDR parsing into service extensions and enforce fail-closed behavior outside Development when trusted proxies are not configured. Add ForwardedHeaders config section with TrustedProxyCidrs/TrustedProxies defaults. --- .../IServiceCollectionExtensions.cs | 78 ++++++++++++++++++- EssentialCSharp.Web/Program.cs | 46 +++++------ EssentialCSharp.Web/appsettings.json | 6 +- 3 files changed, 100 insertions(+), 30 deletions(-) diff --git a/EssentialCSharp.Web/Extensions/IServiceCollectionExtensions.cs b/EssentialCSharp.Web/Extensions/IServiceCollectionExtensions.cs index 9ef4a7b1..74967040 100644 --- a/EssentialCSharp.Web/Extensions/IServiceCollectionExtensions.cs +++ b/EssentialCSharp.Web/Extensions/IServiceCollectionExtensions.cs @@ -1,4 +1,7 @@ -using EssentialCSharp.Web.Services; +using System.Net; +using System.Net.Sockets; +using EssentialCSharp.Web.Services; +using Microsoft.AspNetCore.HttpOverrides; namespace EssentialCSharp.Web.Extensions; @@ -13,4 +16,77 @@ public static void AddCaptchaService(this IServiceCollection services, IConfigur c.BaseAddress = new Uri("https://api.hcaptcha.com"); }); } + + public static void AddTrustedForwardedHeaders(this IServiceCollection services, IConfiguration configuration, IHostEnvironment environment) + { + services.Configure(options => + { + options.ForwardedHeaders = + ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; + options.ForwardLimit = 1; + + var trustedProxyCidrs = configuration + .GetSection("ForwardedHeaders:TrustedProxyCidrs") + .Get() ?? []; + var trustedProxies = configuration + .GetSection("ForwardedHeaders:TrustedProxies") + .Get() ?? []; + + if (trustedProxyCidrs.Length == 0 && trustedProxies.Length == 0) + { + if (!environment.IsDevelopment()) + { + throw new InvalidOperationException( + "Forwarded headers are enabled but no trusted proxies are configured. " + + "Set ForwardedHeaders:TrustedProxyCidrs or ForwardedHeaders:TrustedProxies."); + } + return; + } + + options.KnownIPNetworks.Clear(); + options.KnownProxies.Clear(); + + foreach (var cidr in trustedProxyCidrs) + { + if (!TryParseCidr(cidr, out var network)) + throw new InvalidOperationException($"Invalid ForwardedHeaders:TrustedProxyCidrs entry '{cidr}'. Use CIDR notation, e.g. '10.0.0.0/8'."); + + options.KnownIPNetworks.Add(network); + } + + foreach (var proxy in trustedProxies) + { + if (!IPAddress.TryParse(proxy, out var proxyAddress)) + throw new InvalidOperationException($"Invalid ForwardedHeaders:TrustedProxies entry '{proxy}'. Use a valid IP address."); + + options.KnownProxies.Add(proxyAddress); + } + }); + } + + private static bool TryParseCidr(string cidr, out System.Net.IPNetwork network) + { + network = default!; + if (string.IsNullOrWhiteSpace(cidr)) + return false; + + string[] parts = cidr.Split('/', 2, StringSplitOptions.TrimEntries); + if (parts.Length != 2 + || !IPAddress.TryParse(parts[0], out var networkAddress) + || !int.TryParse(parts[1], out var prefixLength)) + return false; + + int maxPrefixLength = networkAddress.AddressFamily switch + { + AddressFamily.InterNetwork => 32, + AddressFamily.InterNetworkV6 => 128, + _ => -1 + }; + + if (maxPrefixLength < 0 || prefixLength < 0 || prefixLength > maxPrefixLength) + return false; + + network = new System.Net.IPNetwork(networkAddress, prefixLength); + return true; + } } diff --git a/EssentialCSharp.Web/Program.cs b/EssentialCSharp.Web/Program.cs index 10829c7a..51fd9e1a 100644 --- a/EssentialCSharp.Web/Program.cs +++ b/EssentialCSharp.Web/Program.cs @@ -107,17 +107,7 @@ private static void Main(string[] args) - builder.Services.Configure(options => - { - options.ForwardedHeaders = - ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; - - // Only loopback proxies are allowed by default. - // Clear that restriction because forwarders are enabled by explicit - // configuration. - options.KnownIPNetworks.Clear(); - options.KnownProxies.Clear(); - }); + builder.Services.AddTrustedForwardedHeaders(builder.Configuration, builder.Environment); ConfigurationManager configuration = builder.Configuration; string connectionString = builder.Configuration.GetConnectionString("EssentialCSharpWebContextConnection") ?? throw new InvalidOperationException("Connection string 'EssentialCSharpWebContextConnection' not found."); @@ -511,7 +501,7 @@ await McpJsonRpcResponseWriter.WriteErrorAsync( } app.UseStaticFiles(); - app.UseRouting(); + app.UseRouting(); app.UseWhen( context => context.Request.Path.StartsWithSegments("/mcp"), @@ -542,10 +532,10 @@ await McpJsonRpcResponseWriter.WriteErrorAsync( await next(context); })); - app.UseRateLimiter(); - - app.UseAuthorization(); - app.UseOutputCache(); + app.UseRateLimiter(); + + app.UseAuthorization(); + app.UseOutputCache(); app.UseMiddleware(); @@ -584,13 +574,13 @@ await McpJsonRpcResponseWriter.WriteErrorAsync( try { SitemapXmlHelpers.EnsureSitemapHealthy(siteMappingService.SiteMappings.ToList()); - LogSitemapValidationSucceeded(logger); - } - catch (Exception ex) - { - LogSitemapValidationFailed(logger, ex); - // Continue startup even if sitemap validation fails - } + LogSitemapValidationSucceeded(logger); + } + catch (Exception ex) + { + LogSitemapValidationFailed(logger, ex); + // Continue startup even if sitemap validation fails + } app.Run(); } @@ -604,11 +594,11 @@ private static bool IsMcpTransportRequest(HttpRequest request) => [LoggerMessage(Level = LogLevel.Error, Message = "Unhandled exception on {Path}")] private static partial void LogUnhandledException(ILogger logger, Exception? exception, PathString path); - [LoggerMessage(Level = LogLevel.Information, Message = "Sitemap validation completed successfully during application startup")] - private static partial void LogSitemapValidationSucceeded(ILogger logger); - - [LoggerMessage(Level = LogLevel.Error, Message = "Failed to validate sitemap during application startup")] - private static partial void LogSitemapValidationFailed(ILogger logger, Exception exception); + [LoggerMessage(Level = LogLevel.Information, Message = "Sitemap validation completed successfully during application startup")] + private static partial void LogSitemapValidationSucceeded(ILogger logger); + + [LoggerMessage(Level = LogLevel.Error, Message = "Failed to validate sitemap during application startup")] + private static partial void LogSitemapValidationFailed(ILogger logger, Exception exception); [LoggerMessage(Level = LogLevel.Warning, Message = "Ignoring invalid TryDotNet origin in CSP: {Origin}")] private static partial void LogIgnoringInvalidTryDotNetOrigin(ILogger logger, string origin); diff --git a/EssentialCSharp.Web/appsettings.json b/EssentialCSharp.Web/appsettings.json index fe9a4e35..881213e4 100644 --- a/EssentialCSharp.Web/appsettings.json +++ b/EssentialCSharp.Web/appsettings.json @@ -9,6 +9,10 @@ } }, "AllowedHosts": "*", + "ForwardedHeaders": { + "TrustedProxyCidrs": [], + "TrustedProxies": [] + }, "HCaptcha": { "SecretKey": "0x0000000000000000000000000000000000000000", "SiteKey": "10000000-ffff-ffff-ffff-000000000001" @@ -47,4 +51,4 @@ "TryDotNet": { "Origin": "" } -} \ No newline at end of file +} From 7e45baca9ee8a376562847c8be7b8f2fcf367741 Mon Sep 17 00:00:00 2001 From: Benjamin Michaelis Date: Fri, 15 May 2026 23:29:25 -0700 Subject: [PATCH 8/8] Refine chat stream cancellation handling Handle request-abort cancellations explicitly while rethrowing non-request OperationCanceledException cases. Also harden pre-start context-limit error writes to avoid inheriting an already-canceled token for best-effort JSON responses. --- .../Controllers/ChatController.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/EssentialCSharp.Web/Controllers/ChatController.cs b/EssentialCSharp.Web/Controllers/ChatController.cs index 754a7b56..66a2866c 100644 --- a/EssentialCSharp.Web/Controllers/ChatController.cs +++ b/EssentialCSharp.Web/Controllers/ChatController.cs @@ -134,9 +134,15 @@ public async Task StreamMessage([FromBody] ChatMessageRequest request, Cancellat await Response.WriteAsync("data: [DONE]\n\n", cancellationToken); await Response.Body.FlushAsync(cancellationToken); } - catch (OperationCanceledException) when (Response.HasStarted || cancellationToken.IsCancellationRequested || HttpContext.RequestAborted.IsCancellationRequested) + catch (OperationCanceledException) { - LogChatStreamCancelled(_Logger, User.Identity?.Name); + if (cancellationToken.IsCancellationRequested || HttpContext.RequestAborted.IsCancellationRequested) + { + LogChatStreamCancelled(_Logger, User.Identity?.Name); + return; + } + + throw; } catch (ConversationContextLimitExceededException ex) { @@ -149,7 +155,11 @@ public async Task StreamMessage([FromBody] ChatMessageRequest request, Cancellat Response.ContentType = "application/json"; try { - await Response.WriteAsJsonAsync(new { error = "This conversation has grown too long. Please start a new one.", errorCode = "context_limit_exceeded" }, cancellationToken); + var writeCancellationToken = + cancellationToken.IsCancellationRequested || HttpContext.RequestAborted.IsCancellationRequested + ? CancellationToken.None + : cancellationToken; + await Response.WriteAsJsonAsync(new { error = "This conversation has grown too long. Please start a new one.", errorCode = "context_limit_exceeded" }, writeCancellationToken); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested || HttpContext.RequestAborted.IsCancellationRequested) {