Skip to content

Commit 4e9b2f6

Browse files
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>
1 parent 89fe09b commit 4e9b2f6

1 file changed

Lines changed: 29 additions & 26 deletions

File tree

EssentialCSharp.Web/Controllers/ChatController.cs

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ namespace EssentialCSharp.Web.Controllers;
1515
[IgnoreAntiforgeryToken]
1616
public partial class ChatController : ControllerBase
1717
{
18-
private readonly AIChatService _AiChatService;
18+
private readonly AIChatService _AIChatService;
1919
private readonly ResponseIdValidationService _ResponseIdValidationService;
2020
private readonly ILogger<ChatController> _Logger;
2121

2222
public ChatController(ILogger<ChatController> logger, AIChatService aiChatService, ResponseIdValidationService responseIdValidationService)
2323
{
24-
_AiChatService = aiChatService;
24+
_AIChatService = aiChatService;
2525
_ResponseIdValidationService = responseIdValidationService;
2626
_Logger = logger;
2727
}
@@ -46,7 +46,7 @@ public async Task<IActionResult> SendMessage([FromBody] ChatMessageRequest reque
4646

4747
try
4848
{
49-
var (response, responseId) = await _AiChatService.GetChatCompletion(
49+
var (response, responseId) = await _AIChatService.GetChatCompletion(
5050
prompt: request.Message,
5151
previousResponseId: previousResponseId,
5252
enableContextualSearch: request.EnableContextualSearch,
@@ -75,15 +75,15 @@ public async Task StreamMessage([FromBody] ChatMessageRequest request, Cancellat
7575
if (string.IsNullOrEmpty(request.Message))
7676
{
7777
Response.StatusCode = 400;
78-
await Response.WriteAsJsonAsync(new { error = "Message cannot be empty." }, CancellationToken.None);
78+
await Response.WriteAsJsonAsync(new { error = "Message cannot be empty." }, cancellationToken);
7979
return;
8080
}
8181

8282
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
8383
if (string.IsNullOrEmpty(userId))
8484
{
8585
Response.StatusCode = 401;
86-
await Response.WriteAsJsonAsync(new { error = "Unauthorized." }, CancellationToken.None);
86+
await Response.WriteAsJsonAsync(new { error = "Unauthorized." }, cancellationToken);
8787
return;
8888
}
8989

@@ -94,7 +94,7 @@ public async Task StreamMessage([FromBody] ChatMessageRequest request, Cancellat
9494
if (!_ResponseIdValidationService.ValidateResponseId(userId, previousResponseId))
9595
{
9696
Response.StatusCode = 400;
97-
await Response.WriteAsJsonAsync(new { error = "Invalid conversation context." }, CancellationToken.None);
97+
await Response.WriteAsJsonAsync(new { error = "Invalid conversation context." }, cancellationToken);
9898
return;
9999
}
100100

@@ -104,7 +104,7 @@ public async Task StreamMessage([FromBody] ChatMessageRequest request, Cancellat
104104

105105
try
106106
{
107-
await foreach (var (text, responseId) in _AiChatService.GetChatCompletionStream(
107+
await foreach (var (text, responseId) in _AIChatService.GetChatCompletionStream(
108108
prompt: request.Message,
109109
previousResponseId: previousResponseId,
110110
enableContextualSearch: request.EnableContextualSearch,
@@ -133,46 +133,49 @@ public async Task StreamMessage([FromBody] ChatMessageRequest request, Cancellat
133133
await Response.WriteAsync("data: [DONE]\n\n", cancellationToken);
134134
await Response.Body.FlushAsync(cancellationToken);
135135
}
136-
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested || HttpContext.RequestAborted.IsCancellationRequested)
136+
catch (OperationCanceledException)
137137
{
138138
LogChatStreamCancelled(_Logger, User.Identity?.Name);
139139
}
140-
catch (ConversationContextLimitExceededException) when (!Response.HasStarted)
141-
{
142-
Response.StatusCode = 400;
143-
Response.ContentType = "application/json";
144-
await Response.WriteAsJsonAsync(new { error = "This conversation has grown too long. Please start a new one.", errorCode = "context_limit_exceeded" }, CancellationToken.None);
145-
}
146140
catch (ConversationContextLimitExceededException ex)
147141
{
148-
LogChatStreamErrorMidStream(_Logger, ex, User.Identity?.Name);
149-
try
142+
if (!Response.HasStarted)
150143
{
151-
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);
152-
await Response.Body.FlushAsync(CancellationToken.None);
144+
Response.StatusCode = 400;
145+
Response.ContentType = "application/json";
146+
await Response.WriteAsJsonAsync(new { error = "This conversation has grown too long. Please start a new one.", errorCode = "context_limit_exceeded" }, cancellationToken);
153147
}
154-
catch (Exception)
148+
else
155149
{
156-
// Best-effort write to an already-streaming response. Kestrel can throw
157-
// IOException (connection reset), OperationCanceledException, or
158-
// ObjectDisposedException on abrupt client disconnect — swallow all to
159-
// avoid masking the original exception.
150+
LogChatStreamErrorMidStream(_Logger, ex, User.Identity?.Name);
151+
try
152+
{
153+
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);
154+
await Response.Body.FlushAsync(cancellationToken);
155+
}
156+
catch (Exception)
157+
{
158+
// Best-effort write to an already-streaming response. Kestrel can throw
159+
// IOException (connection reset), OperationCanceledException, or
160+
// ObjectDisposedException on abrupt client disconnect — swallow all to
161+
// avoid masking the original exception.
162+
}
160163
}
161164
}
162165
catch (Exception ex) when (!Response.HasStarted)
163166
{
164167
LogChatStreamErrorBeforeResponseStarted(_Logger, ex, User.Identity?.Name);
165168
Response.StatusCode = 500;
166169
Response.ContentType = "application/json";
167-
await Response.WriteAsJsonAsync(new { error = "Chat service unavailable" }, CancellationToken.None);
170+
await Response.WriteAsJsonAsync(new { error = "Chat service unavailable" }, cancellationToken);
168171
}
169172
catch (Exception ex)
170173
{
171174
LogChatStreamErrorMidStream(_Logger, ex, User.Identity?.Name);
172175
try
173176
{
174-
await Response.WriteAsync("data: {\"type\":\"error\",\"message\":\"Stream interrupted\"}\n\n", CancellationToken.None);
175-
await Response.Body.FlushAsync(CancellationToken.None);
177+
await Response.WriteAsync("data: {\"type\":\"error\",\"message\":\"Stream interrupted\"}\n\n", cancellationToken);
178+
await Response.Body.FlushAsync(cancellationToken);
176179
}
177180
catch (Exception)
178181
{

0 commit comments

Comments
 (0)