Skip to content

Commit 12e17ae

Browse files
committed
Fix race condition in SSE GET request initialization
The StreamableHttpHandler was flushing HTTP response headers before calling HandleGetRequestAsync, which sets _getHttpRequestStarted = true. This allowed a race where SendNotificationAsync could be called after the client received headers but before _getHttpRequestStarted was set, causing the notification to be silently dropped. Move the flush inside HandleGetRequestAsync so it occurs after _getHttpRequestStarted is set, while still holding the lock. This ensures SendNotificationAsync will either: - Block on the lock until initialization completes, or - See _getHttpRequestStarted = true after acquiring the lock The priming write path already flushes via SseEventWriter.WriteAsync, so the explicit flush is only needed in the non-priming case.
1 parent 7c85ac6 commit 12e17ae

2 files changed

Lines changed: 6 additions & 5 deletions

File tree

src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -154,11 +154,6 @@ await WriteJsonRpcErrorAsync(context,
154154
{
155155
await using var _ = await session.AcquireReferenceAsync(cancellationToken);
156156
InitializeSseResponse(context);
157-
158-
// We should flush headers to indicate a 200 success quickly, because the initialization response
159-
// will be sent in response to a different POST request. It might be a while before we send a message
160-
// over this response body.
161-
await context.Response.Body.FlushAsync(cancellationToken);
162157
await session.Transport.HandleGetRequestAsync(context.Response.Body, cancellationToken);
163158
}
164159
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)

src/ModelContextProtocol.Core/Server/StreamableHttpServerTransport.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,12 @@ public async Task HandleGetRequestAsync(Stream sseResponseStream, CancellationTo
125125
var primingItem = await _storeSseWriter.WriteEventAsync(SseItem.Prime<JsonRpcMessage>(), cancellationToken).ConfigureAwait(false);
126126
await _httpSseWriter.WriteAsync(primingItem, cancellationToken).ConfigureAwait(false);
127127
}
128+
else
129+
{
130+
// If there's no priming write, flush the stream to ensure HTTP response headers are
131+
// sent to the client now that the transport is ready to accept messages via SendMessageAsync.
132+
await sseResponseStream.FlushAsync(cancellationToken).ConfigureAwait(false);
133+
}
128134
}
129135

130136
// Wait for the response to be written before returning from the handler.

0 commit comments

Comments
 (0)