Skip to content

Commit 223a9bf

Browse files
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>
1 parent 8397a8d commit 223a9bf

1 file changed

Lines changed: 37 additions & 7 deletions

File tree

EssentialCSharp.Web/Controllers/ChatController.cs

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -134,17 +134,32 @@ public async Task StreamMessage([FromBody] ChatMessageRequest request, Cancellat
134134
await Response.WriteAsync("data: [DONE]\n\n", cancellationToken);
135135
await Response.Body.FlushAsync(cancellationToken);
136136
}
137-
catch (OperationCanceledException)
137+
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested || HttpContext.RequestAborted.IsCancellationRequested)
138138
{
139139
LogChatStreamCancelled(_Logger, User.Identity?.Name);
140140
}
141141
catch (ConversationContextLimitExceededException ex)
142142
{
143143
if (!Response.HasStarted)
144144
{
145+
if (cancellationToken.IsCancellationRequested || HttpContext.RequestAborted.IsCancellationRequested)
146+
return;
147+
145148
Response.StatusCode = 400;
146149
Response.ContentType = "application/json";
147-
await Response.WriteAsJsonAsync(new { error = "This conversation has grown too long. Please start a new one.", errorCode = "context_limit_exceeded" }, cancellationToken);
150+
try
151+
{
152+
await Response.WriteAsJsonAsync(new { error = "This conversation has grown too long. Please start a new one.", errorCode = "context_limit_exceeded" }, cancellationToken);
153+
}
154+
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested || HttpContext.RequestAborted.IsCancellationRequested)
155+
{
156+
}
157+
catch (IOException) when (HttpContext.RequestAborted.IsCancellationRequested)
158+
{
159+
}
160+
catch (ObjectDisposedException) when (HttpContext.RequestAborted.IsCancellationRequested)
161+
{
162+
}
148163
}
149164
else
150165
{
@@ -154,7 +169,7 @@ public async Task StreamMessage([FromBody] ChatMessageRequest request, Cancellat
154169
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);
155170
await Response.Body.FlushAsync(cancellationToken);
156171
}
157-
catch (Exception ex) when (ex is IOException or OperationCanceledException or ObjectDisposedException)
172+
catch (Exception writeException) when (writeException is IOException or OperationCanceledException or ObjectDisposedException)
158173
{
159174
// Best-effort write to an already-streaming response. Kestrel can throw
160175
// IOException (connection reset), OperationCanceledException, or
@@ -166,9 +181,24 @@ public async Task StreamMessage([FromBody] ChatMessageRequest request, Cancellat
166181
catch (Exception ex) when (!Response.HasStarted)
167182
{
168183
LogChatStreamErrorBeforeResponseStarted(_Logger, ex, User.Identity?.Name);
184+
if (cancellationToken.IsCancellationRequested || HttpContext.RequestAborted.IsCancellationRequested)
185+
return;
186+
169187
Response.StatusCode = 500;
170188
Response.ContentType = "application/json";
171-
await Response.WriteAsJsonAsync(new { error = "Chat service unavailable" }, cancellationToken);
189+
try
190+
{
191+
await Response.WriteAsJsonAsync(new { error = "Chat service unavailable" }, cancellationToken);
192+
}
193+
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested || HttpContext.RequestAborted.IsCancellationRequested)
194+
{
195+
}
196+
catch (IOException) when (HttpContext.RequestAborted.IsCancellationRequested)
197+
{
198+
}
199+
catch (ObjectDisposedException) when (HttpContext.RequestAborted.IsCancellationRequested)
200+
{
201+
}
172202
}
173203
catch (Exception ex)
174204
{
@@ -178,12 +208,12 @@ public async Task StreamMessage([FromBody] ChatMessageRequest request, Cancellat
178208
await Response.WriteAsync("data: {\"type\":\"error\",\"message\":\"Stream interrupted\"}\n\n", cancellationToken);
179209
await Response.Body.FlushAsync(cancellationToken);
180210
}
181-
catch (Exception)
211+
catch (Exception writeException) when (writeException is IOException or OperationCanceledException or ObjectDisposedException)
182212
{
183213
// Best-effort write to an already-streaming response. Kestrel can throw
184214
// IOException (connection reset), OperationCanceledException, or
185-
// ObjectDisposedException on abrupt client disconnect — swallow all to
186-
// avoid masking the original exception.
215+
// ObjectDisposedException on abrupt client disconnect — swallow expected
216+
// transport/disconnect exceptions to avoid masking the original exception.
187217
}
188218
}
189219
}

0 commit comments

Comments
 (0)