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/6] 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/6] 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/6] 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/6] 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/6] 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/6] 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); }